Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +2 -0
- README.md +21 -7
- app.py +340 -0
- examples/bridgev2_obs.jpg +3 -0
- examples/umi_sock_obs.jpg +3 -0
- examples/xtrainer_garment_obs.jpg +0 -0
- inference_utils.py +60 -0
- interleave_inference.py +345 -0
- model/__init__.py +64 -0
- model/attention_mot_packed.py +435 -0
- model/config_unified_mot.py +103 -0
- model/flow_matching_modules.py +135 -0
- model/model_utils.py +159 -0
- model/modeling_decoder_mot.py +263 -0
- model/modeling_text_model_mot.py +246 -0
- model/modeling_unified_mot.py +523 -0
- model/mot_init_utils.py +366 -0
- multiframe_inference.py +391 -0
- requirements.txt +9 -0
- text2image_inference.py +267 -0
- vae_model/__init__.py +1 -0
- vae_model/autoencoder.py +360 -0
- vqa_inference.py +113 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/bridgev2_obs.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/umi_sock_obs.jpg filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,13 +1,27 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.20.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: RxBrain Embodied Cognition
|
| 3 |
+
emoji: 🔮
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.20.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Embodied reasoning + visual imagination (Hy-RxBrain 1.0)
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 1h
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# RxBrain — Embodied Cognition Foundation Model
|
| 15 |
+
|
| 16 |
+
Interactive demo of **[tencent/Hy-Embodied-RxBrain-1.0](https://huggingface.co/tencent/Hy-Embodied-RxBrain-1.0)**,
|
| 17 |
+
a unified Mixture-of-Transformers model that couples language reasoning with visual imagination.
|
| 18 |
+
|
| 19 |
+
Three capabilities:
|
| 20 |
+
|
| 21 |
+
- **Visual QA** — question answering over images.
|
| 22 |
+
- **Text-to-Image** — imagine an image via the flow-matching head (decoded through a frozen FLUX VAE).
|
| 23 |
+
- **Embodied Planning** — decompose a task into interleaved steps, emitting both the action (text)
|
| 24 |
+
and the imagined goal frame (image) per step.
|
| 25 |
+
|
| 26 |
+
Runs on ZeroGPU. Imagination requires the external FLUX VAE (`ae.safetensors`), fetched from
|
| 27 |
+
`black-forest-labs/FLUX.1-schnell`.
|
app.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RxBrain — Embodied Cognition Foundation Model demo (ZeroGPU).
|
| 2 |
+
|
| 3 |
+
tencent/Hy-Embodied-RxBrain-1.0 : a unified Mixture-of-Transformers model that
|
| 4 |
+
couples language reasoning with visual imagination. This Space exposes three
|
| 5 |
+
capabilities, each mapped 1:1 onto the authors' official inference scripts:
|
| 6 |
+
|
| 7 |
+
* Visual Question Answering (vqa_inference.answer)
|
| 8 |
+
* Text-to-Image imagination (text2image_inference.generate_image)
|
| 9 |
+
* Interleaved embodied planning (interleave_inference.generate_step_joint)
|
| 10 |
+
"""
|
| 11 |
+
import os
|
| 12 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 13 |
+
|
| 14 |
+
import spaces # noqa: E402 — must precede torch / CUDA-touching imports
|
| 15 |
+
import tempfile # noqa: E402
|
| 16 |
+
import traceback # noqa: E402
|
| 17 |
+
|
| 18 |
+
import torch # noqa: E402
|
| 19 |
+
import numpy as np # noqa: E402
|
| 20 |
+
import gradio as gr # noqa: E402
|
| 21 |
+
from PIL import Image # noqa: E402
|
| 22 |
+
from huggingface_hub import snapshot_download, hf_hub_download # noqa: E402
|
| 23 |
+
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
# Model / VAE loading (module scope, eager .to("cuda") for ZeroGPU)
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
MODEL_ID = "tencent/Hy-Embodied-RxBrain-1.0"
|
| 28 |
+
VAE_REPO = "black-forest-labs/FLUX.1-schnell"
|
| 29 |
+
DTYPE = torch.bfloat16
|
| 30 |
+
|
| 31 |
+
print("Downloading RxBrain checkpoint …")
|
| 32 |
+
CKPT_DIR = snapshot_download(MODEL_ID)
|
| 33 |
+
print(f"Checkpoint at {CKPT_DIR}")
|
| 34 |
+
|
| 35 |
+
print("Downloading FLUX VAE (ae.safetensors) …")
|
| 36 |
+
VAE_PATH = hf_hub_download(VAE_REPO, "ae.safetensors")
|
| 37 |
+
print(f"VAE at {VAE_PATH}")
|
| 38 |
+
|
| 39 |
+
from transformers.models.hunyuan_vl_mot import HunYuanVLMoTProcessor # noqa: E402
|
| 40 |
+
from model import UnifiedMoTForConditionalGeneration, maybe_init_generation_path # noqa: E402
|
| 41 |
+
from vae_model.autoencoder import load_ae # noqa: E402
|
| 42 |
+
|
| 43 |
+
# Inference helpers ported straight from the official scripts.
|
| 44 |
+
from vqa_inference import answer as _vqa_answer # noqa: E402
|
| 45 |
+
from text2image_inference import generate_image as _t2i_generate # noqa: E402
|
| 46 |
+
from interleave_inference import generate_step_joint as _interleave_step # noqa: E402
|
| 47 |
+
|
| 48 |
+
print("Loading processor …")
|
| 49 |
+
processor = HunYuanVLMoTProcessor.from_pretrained(CKPT_DIR, trust_remote_code=True)
|
| 50 |
+
|
| 51 |
+
print("Loading model …")
|
| 52 |
+
model = UnifiedMoTForConditionalGeneration.from_pretrained(CKPT_DIR, dtype=DTYPE)
|
| 53 |
+
maybe_init_generation_path(model, model_load_path=CKPT_DIR)
|
| 54 |
+
model.to("cuda").eval()
|
| 55 |
+
|
| 56 |
+
print("Loading VAE …")
|
| 57 |
+
vae, _ = load_ae(VAE_PATH)
|
| 58 |
+
vae.requires_grad_(False)
|
| 59 |
+
vae.eval()
|
| 60 |
+
vae.to("cuda", dtype=DTYPE)
|
| 61 |
+
|
| 62 |
+
DEVICE = torch.device("cuda")
|
| 63 |
+
print("All components ready.")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _tensor_to_pil(img):
|
| 67 |
+
"""(3, H, W) tensor in [-1, 1] -> PIL.Image."""
|
| 68 |
+
arr = ((img.cpu().permute(1, 2, 0).numpy() + 1.0) * 127.5).clip(0, 255).astype("uint8")
|
| 69 |
+
return Image.fromarray(arr)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _save_input_image(image):
|
| 73 |
+
"""Persist a Gradio image (PIL or path) to a temp jpg, return the path."""
|
| 74 |
+
if image is None:
|
| 75 |
+
return None
|
| 76 |
+
if isinstance(image, str):
|
| 77 |
+
return image
|
| 78 |
+
if isinstance(image, np.ndarray):
|
| 79 |
+
image = Image.fromarray(image)
|
| 80 |
+
f = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
|
| 81 |
+
image.convert("RGB").save(f.name, quality=95)
|
| 82 |
+
return f.name
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ---------------------------------------------------------------------------
|
| 86 |
+
# 1. Visual Question Answering
|
| 87 |
+
# ---------------------------------------------------------------------------
|
| 88 |
+
@spaces.GPU(duration=120)
|
| 89 |
+
def vqa(image, question, max_new_tokens=256):
|
| 90 |
+
"""Answer a question about an image using RxBrain's understanding path.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
image: the input image to reason over.
|
| 94 |
+
question: a natural-language question about the image.
|
| 95 |
+
max_new_tokens: maximum number of answer tokens to decode.
|
| 96 |
+
Returns:
|
| 97 |
+
The model's answer text.
|
| 98 |
+
"""
|
| 99 |
+
if image is None:
|
| 100 |
+
return "Please provide an input image."
|
| 101 |
+
if not question or not question.strip():
|
| 102 |
+
return "Please enter a question."
|
| 103 |
+
path = _save_input_image(image)
|
| 104 |
+
text = _vqa_answer(
|
| 105 |
+
model, processor, image_paths=[path], question=question.strip(),
|
| 106 |
+
device=DEVICE, dtype=DTYPE, max_new_tokens=int(max_new_tokens),
|
| 107 |
+
)
|
| 108 |
+
return text.strip()
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# ---------------------------------------------------------------------------
|
| 112 |
+
# 2. Text-to-Image imagination
|
| 113 |
+
# ---------------------------------------------------------------------------
|
| 114 |
+
def _t2i_duration(prompt, height=256, width=256, num_steps=25, cfg_scale=1.0, seed=0):
|
| 115 |
+
base = 40
|
| 116 |
+
per_step = 1.5 * (2 if cfg_scale != 1.0 else 1)
|
| 117 |
+
return int(min(180, base + num_steps * per_step))
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@spaces.GPU(duration=_t2i_duration)
|
| 121 |
+
def text_to_image(prompt, height=256, width=256, num_steps=25, cfg_scale=1.0, seed=0):
|
| 122 |
+
"""Imagine an image from a text prompt via the flow-matching head.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
prompt: the text description to imagine.
|
| 126 |
+
height: output image height in pixels (multiple of 16).
|
| 127 |
+
width: output image width in pixels (multiple of 16).
|
| 128 |
+
num_steps: number of Euler-ODE flow-matching steps.
|
| 129 |
+
cfg_scale: classifier-free guidance scale (>1 enables CFG).
|
| 130 |
+
seed: RNG seed for reproducibility.
|
| 131 |
+
Returns:
|
| 132 |
+
The generated PIL image.
|
| 133 |
+
"""
|
| 134 |
+
if not prompt or not prompt.strip():
|
| 135 |
+
raise gr.Error("Please enter a prompt.")
|
| 136 |
+
torch.manual_seed(int(seed))
|
| 137 |
+
h = int(height) // 16 * 16
|
| 138 |
+
w = int(width) // 16 * 16
|
| 139 |
+
img = _t2i_generate(
|
| 140 |
+
model, vae, processor, prompt.strip(),
|
| 141 |
+
height=h, width=w, num_steps=int(num_steps),
|
| 142 |
+
device=DEVICE, dtype=DTYPE, cfg_scale=float(cfg_scale),
|
| 143 |
+
)
|
| 144 |
+
return _tensor_to_pil(img)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ---------------------------------------------------------------------------
|
| 148 |
+
# 3. Interleaved Embodied Planning (text plan + goal images, step by step)
|
| 149 |
+
# ---------------------------------------------------------------------------
|
| 150 |
+
def _plan_duration(image, task, num_frames=3, num_steps=50, height=144, width=256, seed=0):
|
| 151 |
+
return int(min(230, 30 + num_frames * (25 + num_steps * 1.3)))
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@spaces.GPU(duration=_plan_duration)
|
| 155 |
+
def embodied_plan(image, task, num_frames=3, num_steps=50, height=144, width=256, seed=0):
|
| 156 |
+
"""Decompose an embodied task into steps, emitting both the next action
|
| 157 |
+
(text) and the imagined goal frame (image) for each step.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
image: an observation frame of the current scene.
|
| 161 |
+
task: the embodied task to plan for.
|
| 162 |
+
num_frames: number of interleaved plan steps to roll out.
|
| 163 |
+
num_steps: number of flow-matching ODE steps per imagined frame.
|
| 164 |
+
height: imagined-frame height in pixels (multiple of 16).
|
| 165 |
+
width: imagined-frame width in pixels (multiple of 16).
|
| 166 |
+
seed: RNG seed for reproducibility.
|
| 167 |
+
Yields:
|
| 168 |
+
(gallery_of_goal_frames, plan_markdown) as each step completes.
|
| 169 |
+
"""
|
| 170 |
+
if image is None:
|
| 171 |
+
raise gr.Error("Please provide an observation image.")
|
| 172 |
+
if not task or not task.strip():
|
| 173 |
+
raise gr.Error("Please enter a task description.")
|
| 174 |
+
torch.manual_seed(int(seed))
|
| 175 |
+
obs_path = _save_input_image(image)
|
| 176 |
+
h = int(height) // 16 * 16
|
| 177 |
+
w = int(width) // 16 * 16
|
| 178 |
+
|
| 179 |
+
prior_steps = [] # (text, frame_path) accumulated across steps
|
| 180 |
+
gallery = [] # (image, caption) for the gr.Gallery
|
| 181 |
+
plan_md_lines = [f"**Task:** {task.strip()}", ""]
|
| 182 |
+
|
| 183 |
+
for k in range(int(num_frames)):
|
| 184 |
+
text, img = _interleave_step(
|
| 185 |
+
model, vae, processor,
|
| 186 |
+
input_image_paths=[obs_path], task_text=task.strip(),
|
| 187 |
+
height=h, width=w, num_steps=int(num_steps),
|
| 188 |
+
device=DEVICE, dtype=DTYPE, max_text_tokens=96,
|
| 189 |
+
prior_steps=list(prior_steps),
|
| 190 |
+
)
|
| 191 |
+
pil = _tensor_to_pil(img)
|
| 192 |
+
frame_f = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
| 193 |
+
pil.save(frame_f.name)
|
| 194 |
+
prior_steps.append((text, frame_f.name))
|
| 195 |
+
gallery.append((pil, f"Step {k + 1}"))
|
| 196 |
+
plan_md_lines.append(f"**Step {k + 1}:** {text.strip() or '(imagined goal frame only)'}")
|
| 197 |
+
yield gallery, "\n\n".join(plan_md_lines)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# ---------------------------------------------------------------------------
|
| 201 |
+
# UI
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
CSS = """
|
| 204 |
+
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 205 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
INTRO = """# 🔮 RxBrain — Embodied Cognition Foundation Model
|
| 209 |
+
|
| 210 |
+
**[tencent/Hy-Embodied-RxBrain-1.0](https://huggingface.co/tencent/Hy-Embodied-RxBrain-1.0)** — a
|
| 211 |
+
unified Mixture-of-Transformers model that couples **language reasoning** with **visual imagination**.
|
| 212 |
+
It answers questions about scenes, imagines images from text, and decomposes embodied tasks into
|
| 213 |
+
interleaved step-by-step *plans + goal images*.
|
| 214 |
+
|
| 215 |
+
*Tencent Robotics X × Futian Laboratory × Tencent Hunyuan · imagination decoded through a frozen FLUX VAE.*
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="RxBrain") as demo:
|
| 219 |
+
with gr.Column(elem_id="col-container"):
|
| 220 |
+
gr.Markdown(INTRO)
|
| 221 |
+
|
| 222 |
+
with gr.Tabs():
|
| 223 |
+
# ---------------- VQA ----------------
|
| 224 |
+
with gr.Tab("🧠 Visual QA"):
|
| 225 |
+
with gr.Row():
|
| 226 |
+
with gr.Column():
|
| 227 |
+
vqa_image = gr.Image(label="Image", type="pil", height=320)
|
| 228 |
+
vqa_question = gr.Textbox(
|
| 229 |
+
label="Question",
|
| 230 |
+
placeholder="What objects are on the stovetop?",
|
| 231 |
+
)
|
| 232 |
+
with gr.Accordion("Advanced", open=False):
|
| 233 |
+
vqa_max_tokens = gr.Slider(
|
| 234 |
+
16, 512, value=256, step=16, label="Max new tokens")
|
| 235 |
+
vqa_btn = gr.Button("Answer", variant="primary")
|
| 236 |
+
with gr.Column():
|
| 237 |
+
vqa_out = gr.Textbox(label="Answer", lines=10)
|
| 238 |
+
|
| 239 |
+
vqa_btn.click(
|
| 240 |
+
vqa, inputs=[vqa_image, vqa_question, vqa_max_tokens],
|
| 241 |
+
outputs=vqa_out, api_name="vqa",
|
| 242 |
+
)
|
| 243 |
+
gr.Examples(
|
| 244 |
+
examples=[
|
| 245 |
+
["examples/bridgev2_obs.jpg",
|
| 246 |
+
"What objects are on the stovetop, and where is the green toy?"],
|
| 247 |
+
["examples/umi_sock_obs.jpg",
|
| 248 |
+
"Describe the scene and the objects the robot could manipulate."],
|
| 249 |
+
["examples/xtrainer_garment_obs.jpg",
|
| 250 |
+
"What garment is on the surface and what is its current state?"],
|
| 251 |
+
],
|
| 252 |
+
inputs=[vqa_image, vqa_question],
|
| 253 |
+
outputs=vqa_out, fn=vqa,
|
| 254 |
+
cache_examples=False, run_on_click=True,
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# ---------------- T2I ----------------
|
| 258 |
+
with gr.Tab("🎨 Text-to-Image"):
|
| 259 |
+
with gr.Row():
|
| 260 |
+
with gr.Column():
|
| 261 |
+
t2i_prompt = gr.Textbox(
|
| 262 |
+
label="Prompt",
|
| 263 |
+
placeholder="a watercolor painting of a cat",
|
| 264 |
+
)
|
| 265 |
+
with gr.Accordion("Advanced", open=False):
|
| 266 |
+
with gr.Row():
|
| 267 |
+
t2i_h = gr.Slider(128, 384, value=256, step=16, label="Height")
|
| 268 |
+
t2i_w = gr.Slider(128, 384, value=256, step=16, label="Width")
|
| 269 |
+
t2i_steps = gr.Slider(10, 50, value=25, step=1, label="ODE steps")
|
| 270 |
+
t2i_cfg = gr.Slider(1.0, 7.0, value=1.0, step=0.5,
|
| 271 |
+
label="CFG scale (>1 enables guidance)")
|
| 272 |
+
t2i_seed = gr.Number(value=0, precision=0, label="Seed")
|
| 273 |
+
t2i_btn = gr.Button("Imagine", variant="primary")
|
| 274 |
+
with gr.Column():
|
| 275 |
+
t2i_out = gr.Image(label="Imagined image", height=320)
|
| 276 |
+
|
| 277 |
+
t2i_btn.click(
|
| 278 |
+
text_to_image,
|
| 279 |
+
inputs=[t2i_prompt, t2i_h, t2i_w, t2i_steps, t2i_cfg, t2i_seed],
|
| 280 |
+
outputs=t2i_out, api_name="text_to_image",
|
| 281 |
+
)
|
| 282 |
+
gr.Examples(
|
| 283 |
+
examples=[
|
| 284 |
+
["a watercolor painting of a cat"],
|
| 285 |
+
["a red apple on a wooden table, photorealistic"],
|
| 286 |
+
["a robot arm gripping a colorful toy on a kitchen counter"],
|
| 287 |
+
],
|
| 288 |
+
inputs=[t2i_prompt],
|
| 289 |
+
outputs=t2i_out, fn=text_to_image,
|
| 290 |
+
cache_examples=False, run_on_click=True,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# ---------------- Interleaved Planning ----------------
|
| 294 |
+
with gr.Tab("🧩 Embodied Planning"):
|
| 295 |
+
gr.Markdown(
|
| 296 |
+
"Give an observation frame + a task; RxBrain rolls out an interleaved "
|
| 297 |
+
"plan — each step emits an **action (text)** and the **imagined goal frame**."
|
| 298 |
+
)
|
| 299 |
+
with gr.Row():
|
| 300 |
+
with gr.Column():
|
| 301 |
+
plan_image = gr.Image(label="Observation", type="pil", height=280)
|
| 302 |
+
plan_task = gr.Textbox(label="Task", lines=3,
|
| 303 |
+
placeholder="Move the green toy over the metal pot.")
|
| 304 |
+
with gr.Accordion("Advanced", open=False):
|
| 305 |
+
plan_nframes = gr.Slider(1, 5, value=3, step=1, label="Plan steps")
|
| 306 |
+
plan_steps = gr.Slider(10, 50, value=50, step=1, label="ODE steps/frame")
|
| 307 |
+
with gr.Row():
|
| 308 |
+
plan_h = gr.Slider(128, 256, value=144, step=16, label="Frame height")
|
| 309 |
+
plan_w = gr.Slider(128, 384, value=256, step=16, label="Frame width")
|
| 310 |
+
plan_seed = gr.Number(value=0, precision=0, label="Seed")
|
| 311 |
+
plan_btn = gr.Button("Plan", variant="primary")
|
| 312 |
+
with gr.Column():
|
| 313 |
+
plan_gallery = gr.Gallery(label="Imagined goal frames", columns=3, height=280)
|
| 314 |
+
plan_text = gr.Markdown(label="Plan")
|
| 315 |
+
|
| 316 |
+
plan_btn.click(
|
| 317 |
+
embodied_plan,
|
| 318 |
+
inputs=[plan_image, plan_task, plan_nframes, plan_steps,
|
| 319 |
+
plan_h, plan_w, plan_seed],
|
| 320 |
+
outputs=[plan_gallery, plan_text], api_name="embodied_plan",
|
| 321 |
+
)
|
| 322 |
+
gr.Examples(
|
| 323 |
+
examples=[
|
| 324 |
+
["examples/bridgev2_obs.jpg",
|
| 325 |
+
"Move the green toy from the left side of the stovetop and hold it over "
|
| 326 |
+
"the metal pot on the left burner. Please give the full plan as concrete "
|
| 327 |
+
"physical actions, one step per action, and imagine the state once each "
|
| 328 |
+
"step is completed. If only one step remains, provide only that final step."],
|
| 329 |
+
["examples/umi_sock_obs.jpg",
|
| 330 |
+
"Fold the purple sock in half. Please lay out the high-level subgoal plan "
|
| 331 |
+
"and imagine the resulting frame after each major stage. If only one "
|
| 332 |
+
"subgoal remains, provide only that final subgoal."],
|
| 333 |
+
],
|
| 334 |
+
inputs=[plan_image, plan_task],
|
| 335 |
+
outputs=[plan_gallery, plan_text], fn=embodied_plan,
|
| 336 |
+
cache_examples=False, run_on_click=True,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
if __name__ == "__main__":
|
| 340 |
+
demo.queue(max_size=12).launch(mcp_server=True)
|
examples/bridgev2_obs.jpg
ADDED
|
Git LFS Details
|
examples/umi_sock_obs.jpg
ADDED
|
Git LFS Details
|
examples/xtrainer_garment_obs.jpg
ADDED
|
inference_utils.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inference-time utilities for HY-Unified.
|
| 2 |
+
|
| 3 |
+
Self-contained slice of the pieces the inference scripts need — the image
|
| 4 |
+
transform used to derive train/inference resolution parity, and the
|
| 5 |
+
task-instruction prompts that cue each generation mode.
|
| 6 |
+
|
| 7 |
+
The SAME task strings are used at train and inference (parity is load-bearing:
|
| 8 |
+
``interleave_inference`` / ``multiframe_inference`` build the prompt the same
|
| 9 |
+
way the model was trained). Single source of truth, imported by the scripts.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from PIL import Image
|
| 14 |
+
import torchvision.transforms as T
|
| 15 |
+
import torchvision.transforms.functional as TF
|
| 16 |
+
from torchvision.transforms import InterpolationMode
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# Task-instruction prompts appended to the user turn so each generation MODE is
|
| 20 |
+
# explicitly cued.
|
| 21 |
+
TASK_INSTRUCTION_SINGLE_FRAME = "Generate future image of the goal"
|
| 22 |
+
TASK_INSTRUCTION_MULTI_FRAME = "Generate future video of the goal"
|
| 23 |
+
TASK_INSTRUCTION_INTERLEAVE = "Generate interleave goal planning"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class VAEImageTransform:
|
| 27 |
+
"""Aspect-preserving resize + normalize to [-1, 1] for VAE encoding.
|
| 28 |
+
|
| 29 |
+
Sides are clamped to be ``stride``-divisible and within ``[min_size,
|
| 30 |
+
max_size]``; extreme aspect ratios are re-scaled to stay inside ``max_size``
|
| 31 |
+
(otherwise latent position IDs could go out of bounds).
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, max_size=512, min_size=256, stride=16):
|
| 35 |
+
self.max_size = max_size
|
| 36 |
+
self.min_size = min_size
|
| 37 |
+
self.stride = stride
|
| 38 |
+
|
| 39 |
+
def _make_divisible(self, val):
|
| 40 |
+
return max(self.stride, round(val / self.stride) * self.stride)
|
| 41 |
+
|
| 42 |
+
def __call__(self, img: Image.Image) -> torch.Tensor:
|
| 43 |
+
w, h = img.size
|
| 44 |
+
scale = min(self.max_size / max(w, h), 1.0)
|
| 45 |
+
scale = max(scale, self.min_size / min(w, h))
|
| 46 |
+
new_w = self._make_divisible(round(w * scale))
|
| 47 |
+
new_h = self._make_divisible(round(h * scale))
|
| 48 |
+
# Clamp to max_size to handle extreme aspect ratios where the min_size
|
| 49 |
+
# constraint overrides the max_size constraint (e.g. w=1000,h=50 would
|
| 50 |
+
# produce new_w=5120 which causes position ID out-of-bounds).
|
| 51 |
+
max_div = self._make_divisible(self.max_size)
|
| 52 |
+
if new_w > max_div or new_h > max_div:
|
| 53 |
+
# Re-scale to fit within max_size while preserving aspect ratio.
|
| 54 |
+
shrink = min(max_div / new_w, max_div / new_h)
|
| 55 |
+
new_w = self._make_divisible(round(new_w * shrink))
|
| 56 |
+
new_h = self._make_divisible(round(new_h * shrink))
|
| 57 |
+
img = TF.resize(img, (new_h, new_w), InterpolationMode.BICUBIC, antialias=True)
|
| 58 |
+
tensor = TF.to_tensor(img) # [0, 1]
|
| 59 |
+
tensor = T.Normalize([0.5] * 3, [0.5] * 3)(tensor) # [-1, 1]
|
| 60 |
+
return tensor
|
interleave_inference.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Interleaved multi-image inference with a SigLIP-context handoff.
|
| 2 |
+
|
| 3 |
+
Generates a sequence of frames autoregressively:
|
| 4 |
+
|
| 5 |
+
for k in 1..Y:
|
| 6 |
+
seq_k = chat_template(user[obs + decoded f_1..f_{k-1}] + text) + <Image>+LAT*N+</Image>+EOS
|
| 7 |
+
x_0 = Euler-ODE(seq_k) # prior frames condition via SigLIP
|
| 8 |
+
f_k = VAE.decode(x_0) # decode to pixels
|
| 9 |
+
# f_k is fed (as a SigLIP image) into seq_{k+1}
|
| 10 |
+
|
| 11 |
+
The crucial property: a previously generated frame enters the next step's context
|
| 12 |
+
as a **SigLIP-encoded image in a user turn** — NOT as its VAE latent — so the
|
| 13 |
+
context representation is identical at train and test (clean pixels either way),
|
| 14 |
+
avoiding the noised-VAE-latent train/test mismatch of single-sequence interleave.
|
| 15 |
+
|
| 16 |
+
`build_conditioned_sequence` is the single sequence constructor; training builds
|
| 17 |
+
the prompt the same way (apply_chat_template([user(images)+text, assistant:""])
|
| 18 |
+
-> strip EOS -> <Image>+LAT*N+</Image>+EOS), so train and infer token layouts
|
| 19 |
+
match exactly.
|
| 20 |
+
|
| 21 |
+
Run:
|
| 22 |
+
python interleave_inference.py --ckpt <ckpt> --vae <vae> \
|
| 23 |
+
--frames obs.jpg --task "put the cup on the shelf" \
|
| 24 |
+
--max_frames 3 --num_steps 50 --out_dir out_interleave
|
| 25 |
+
"""
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import argparse
|
| 29 |
+
import os
|
| 30 |
+
from typing import List, Optional
|
| 31 |
+
|
| 32 |
+
import torch
|
| 33 |
+
from PIL import Image
|
| 34 |
+
from transformers.models.hunyuan_vl_mot import HunYuanVLMoTProcessor
|
| 35 |
+
|
| 36 |
+
# NOTE: heavy imports (model package -> flash_attn, vae_model) are done lazily
|
| 37 |
+
# inside the functions that need them, so `build_conditioned_sequence` can be
|
| 38 |
+
# imported with only a processor.
|
| 39 |
+
|
| 40 |
+
# Input-image placeholder token ids the upstream HunYuanVL processor scatters
|
| 41 |
+
# ViT features into.
|
| 42 |
+
INPUT_IMAGE_PLACEHOLDER_IDS = (120687, 120688)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def build_conditioned_sequence(processor, input_image_paths: List[str], prompt_text: str,
|
| 46 |
+
prior_steps=None, task_instruction: Optional[str] = None):
|
| 47 |
+
"""Build the prompt token ids + processor vision tensors for a
|
| 48 |
+
(multi-image + text) -> 1-generated-image sample.
|
| 49 |
+
|
| 50 |
+
apply_chat_template([user(...), assistant:""]) then strip trailing EOS.
|
| 51 |
+
|
| 52 |
+
prior_steps: optional list of (text, frame_path) for already-produced steps.
|
| 53 |
+
When given, the user turn is the interleaved context the JOINT converter
|
| 54 |
+
emits — [obs frames] + task + [text_1, frame_1, ..., text_m, frame_m] —
|
| 55 |
+
so prior plan texts stay in context and prior frames enter as SigLIP inputs.
|
| 56 |
+
input_image_paths are then the OBSERVATION frames only.
|
| 57 |
+
When None, the legacy [all images] + text layout is used.
|
| 58 |
+
task_instruction: optional mode instruction appended as the LAST user-text item,
|
| 59 |
+
e.g. "Generate interleave goal planning".
|
| 60 |
+
|
| 61 |
+
Returns (prompt_ids: list[int], proc_inputs: dict).
|
| 62 |
+
"""
|
| 63 |
+
user_content = [{"type": "image", "image": p} for p in input_image_paths]
|
| 64 |
+
user_content.append({"type": "text", "text": prompt_text})
|
| 65 |
+
for t, f in (prior_steps or []):
|
| 66 |
+
if t:
|
| 67 |
+
user_content.append({"type": "text", "text": t})
|
| 68 |
+
user_content.append({"type": "image", "image": f})
|
| 69 |
+
if task_instruction:
|
| 70 |
+
user_content.append({"type": "text", "text": task_instruction})
|
| 71 |
+
prompt_messages = [
|
| 72 |
+
{"role": "user", "content": user_content},
|
| 73 |
+
{"role": "assistant", "content": ""},
|
| 74 |
+
]
|
| 75 |
+
proc_inputs = processor.apply_chat_template(
|
| 76 |
+
prompt_messages, return_dict=True, tokenize=True, add_generation_prompt=False,
|
| 77 |
+
)
|
| 78 |
+
eos = processor.tokenizer.eos_token_id
|
| 79 |
+
prompt_ids = list(proc_inputs["input_ids"][0])
|
| 80 |
+
while prompt_ids and prompt_ids[-1] == eos:
|
| 81 |
+
prompt_ids.pop()
|
| 82 |
+
return prompt_ids, proc_inputs
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@torch.no_grad()
|
| 86 |
+
def generate_step_joint(
|
| 87 |
+
model, vae, processor,
|
| 88 |
+
input_image_paths: List[str],
|
| 89 |
+
task_text: str,
|
| 90 |
+
height: int, width: int, num_steps: int,
|
| 91 |
+
device, dtype,
|
| 92 |
+
max_text_tokens: int = 64,
|
| 93 |
+
prior_steps=None,
|
| 94 |
+
):
|
| 95 |
+
"""v2 joint step: autoregressively decode the plan TEXT, then (on <Image>)
|
| 96 |
+
flow-match the FRAME. Returns (text_str, image_tensor).
|
| 97 |
+
|
| 98 |
+
prior_steps: list of (text, frame_path) already produced — threaded into the
|
| 99 |
+
context so the rollout matches the JOINT training layout (prior texts kept,
|
| 100 |
+
prior frames as SigLIP). When given, input_image_paths = observation frames only.
|
| 101 |
+
|
| 102 |
+
The prefix is built by the SAME `build_conditioned_sequence` used at train
|
| 103 |
+
time, so the text the model emits after `</answer>` and the <Image> trigger
|
| 104 |
+
match what training taught."""
|
| 105 |
+
from model.flow_matching_modules import unpatchify_latent
|
| 106 |
+
from text2image_inference import get_2d_position_ids
|
| 107 |
+
|
| 108 |
+
cfg = model.config
|
| 109 |
+
eos = cfg.eos_token_id
|
| 110 |
+
image_start = cfg.image_start_token_id
|
| 111 |
+
latent_ph = cfg.flow_latent_placeholder_id
|
| 112 |
+
inner = model.model
|
| 113 |
+
|
| 114 |
+
# Append the interleave mode instruction (train==infer parity).
|
| 115 |
+
from inference_utils import TASK_INSTRUCTION_INTERLEAVE
|
| 116 |
+
prompt_ids, proc = build_conditioned_sequence(processor, input_image_paths, task_text,
|
| 117 |
+
prior_steps=prior_steps,
|
| 118 |
+
task_instruction=TASK_INSTRUCTION_INTERLEAVE)
|
| 119 |
+
pixel_values = proc.get("pixel_values")
|
| 120 |
+
image_grid_thw = proc.get("image_grid_thw")
|
| 121 |
+
if pixel_values is not None:
|
| 122 |
+
pixel_values = pixel_values.to(device=device, dtype=dtype)
|
| 123 |
+
if image_grid_thw is not None:
|
| 124 |
+
image_grid_thw = image_grid_thw.to(device=device)
|
| 125 |
+
|
| 126 |
+
# ---- 1. autoregressive text decode until <Image> (KV-cached: exact, ~2.8x) ----
|
| 127 |
+
# prefill the prompt once (use_cache), then feed one token/step against the growing
|
| 128 |
+
# KV cache — the model's native past_key_values plumbing (Mode C handles the decode
|
| 129 |
+
# shape). Validated bit-identical to the non-cached per-token full-forward decode.
|
| 130 |
+
from transformers.cache_utils import DynamicCache
|
| 131 |
+
|
| 132 |
+
def _dmask(ids):
|
| 133 |
+
seq_len = len(ids)
|
| 134 |
+
inp = torch.tensor(ids, dtype=torch.long, device=device).unsqueeze(0)
|
| 135 |
+
mod = torch.zeros(1, seq_len, dtype=torch.long, device=device)
|
| 136 |
+
iim = torch.zeros(1, seq_len, dtype=torch.bool, device=device)
|
| 137 |
+
for pid in INPUT_IMAGE_PLACEHOLDER_IDS:
|
| 138 |
+
m = inp[0] == pid
|
| 139 |
+
mod[0, m] = 1
|
| 140 |
+
iim[0, m] = True
|
| 141 |
+
return inp, mod, iim
|
| 142 |
+
|
| 143 |
+
_empty_g = torch.zeros((0, 2), dtype=torch.int32, device=device)
|
| 144 |
+
pkv = DynamicCache()
|
| 145 |
+
prompt_len = len(prompt_ids)
|
| 146 |
+
inp, mod, iim = _dmask(list(prompt_ids))
|
| 147 |
+
out = inner(input_ids=inp, position_ids=torch.arange(prompt_len, device=device).unsqueeze(0),
|
| 148 |
+
pixel_values=pixel_values, image_grid_thw=image_grid_thw,
|
| 149 |
+
cu_seqlens=torch.tensor([0, prompt_len], dtype=torch.int32, device=device),
|
| 150 |
+
sample_ids=torch.zeros(1, prompt_len, dtype=torch.int32, device=device),
|
| 151 |
+
modality_mask=mod, input_image_mask=iim,
|
| 152 |
+
flow_embeds=None, flow_positions=None, g_seqlens=_empty_g,
|
| 153 |
+
use_cache=True, past_key_values=pkv, cache_position=torch.arange(prompt_len, device=device))
|
| 154 |
+
pkv = out.past_key_values
|
| 155 |
+
nxt = int(out.logits[0, -1].argmax().item())
|
| 156 |
+
cur = prompt_len
|
| 157 |
+
text_ids = []
|
| 158 |
+
for _ in range(max_text_tokens):
|
| 159 |
+
if nxt == image_start or nxt == eos:
|
| 160 |
+
break
|
| 161 |
+
text_ids.append(nxt)
|
| 162 |
+
out = inner(input_ids=torch.tensor([[nxt]], dtype=torch.long, device=device),
|
| 163 |
+
position_ids=torch.tensor([[cur]], device=device),
|
| 164 |
+
pixel_values=None, image_grid_thw=None,
|
| 165 |
+
cu_seqlens=torch.tensor([0, cur + 1], dtype=torch.int32, device=device),
|
| 166 |
+
sample_ids=torch.zeros(1, 1, dtype=torch.int32, device=device),
|
| 167 |
+
modality_mask=torch.zeros(1, 1, dtype=torch.long, device=device),
|
| 168 |
+
input_image_mask=torch.zeros(1, 1, dtype=torch.bool, device=device),
|
| 169 |
+
flow_embeds=None, flow_positions=None, g_seqlens=_empty_g,
|
| 170 |
+
use_cache=True, past_key_values=pkv, cache_position=torch.tensor([cur], device=device))
|
| 171 |
+
pkv = out.past_key_values
|
| 172 |
+
nxt = int(out.logits[0, -1].argmax().item())
|
| 173 |
+
cur += 1
|
| 174 |
+
seq = list(prompt_ids) + text_ids
|
| 175 |
+
text_str = processor.tokenizer.decode(text_ids, skip_special_tokens=True)
|
| 176 |
+
|
| 177 |
+
# ---- 2. flow-match the frame — diffusion PREFIX-CACHE: prefill (prompt+text+IMAGE_START)
|
| 178 |
+
# once, then each denoise step forwards ONLY the n latent tokens against the cached prefix
|
| 179 |
+
# (Mode C causal=False -> latents attend bidirectionally to prefix+latents, == the gen-block).
|
| 180 |
+
# Validated equivalent to the full-forward loop within ROCm non-determinism. ----
|
| 181 |
+
p = model.latent_patch_size
|
| 182 |
+
ds = cfg.vae_image_downsample
|
| 183 |
+
h_lat, w_lat = height // ds, width // ds
|
| 184 |
+
n_latent = h_lat * w_lat
|
| 185 |
+
prefix = seq + [image_start]
|
| 186 |
+
prefix_len = len(prefix) # = latent_start
|
| 187 |
+
inp_p, mod_p, iim_p = _dmask(prefix)
|
| 188 |
+
pkv_f = DynamicCache()
|
| 189 |
+
inner(input_ids=inp_p, position_ids=torch.arange(prefix_len, device=device).unsqueeze(0),
|
| 190 |
+
pixel_values=pixel_values, image_grid_thw=image_grid_thw,
|
| 191 |
+
cu_seqlens=torch.tensor([0, prefix_len], dtype=torch.int32, device=device),
|
| 192 |
+
sample_ids=torch.zeros(1, prefix_len, dtype=torch.int32, device=device),
|
| 193 |
+
modality_mask=mod_p, input_image_mask=iim_p,
|
| 194 |
+
flow_embeds=None, flow_positions=None, g_seqlens=_empty_g,
|
| 195 |
+
use_cache=True, past_key_values=pkv_f, cache_position=torch.arange(prefix_len, device=device))
|
| 196 |
+
|
| 197 |
+
latent_pos_ids = get_2d_position_ids(h_lat, w_lat, cfg.max_latent_size).to(device)
|
| 198 |
+
latent_pos_emb = model.latent_pos_embed(latent_pos_ids).to(dtype)
|
| 199 |
+
lat_ids = torch.tensor([[latent_ph] * n_latent], dtype=torch.long, device=device)
|
| 200 |
+
fp_rel = torch.tensor([[0, n_latent]], dtype=torch.int32, device=device) # rel to the n-token input
|
| 201 |
+
mod2 = torch.full((1, n_latent), 2, dtype=torch.long, device=device)
|
| 202 |
+
iim2 = torch.zeros(1, n_latent, dtype=torch.bool, device=device)
|
| 203 |
+
pos_l = torch.arange(prefix_len, prefix_len + n_latent, device=device).unsqueeze(0)
|
| 204 |
+
cu_l = torch.tensor([0, prefix_len + n_latent], dtype=torch.int32, device=device)
|
| 205 |
+
sid_l = torch.zeros(1, n_latent, dtype=torch.int32, device=device)
|
| 206 |
+
|
| 207 |
+
x = torch.randn(n_latent, p * p * cfg.vae_z_channels, device=device, dtype=dtype)
|
| 208 |
+
ts = torch.linspace(1.0, 0.0, num_steps + 1, device=device, dtype=dtype)
|
| 209 |
+
for i in range(num_steps):
|
| 210 |
+
t, dt = ts[i], ts[i] - ts[i + 1]
|
| 211 |
+
fe = (model.vae2llm(x.to(model.vae2llm.weight.dtype)).to(dtype)
|
| 212 |
+
+ model.time_embedder(t.expand(n_latent)).to(dtype) + latent_pos_emb)
|
| 213 |
+
out = inner(input_ids=lat_ids, inputs_embeds=None, attention_mask=None,
|
| 214 |
+
position_ids=pos_l, pixel_values=None, image_grid_thw=None,
|
| 215 |
+
cu_seqlens=cu_l, sample_ids=sid_l, modality_mask=mod2, input_image_mask=iim2,
|
| 216 |
+
flow_embeds=fe, flow_positions=fp_rel, g_seqlens=_empty_g,
|
| 217 |
+
use_cache=True, past_key_values=pkv_f, cache_position=pos_l[0])
|
| 218 |
+
v = model.llm2vae(out.hidden_states[0, 0:n_latent]).to(dtype)
|
| 219 |
+
x = x - dt * v
|
| 220 |
+
pkv_f.crop(prefix_len) # drop the latents; keep the fixed prefix for next step
|
| 221 |
+
x_lat = unpatchify_latent(x.float(), h_lat, w_lat, p, cfg.vae_z_channels)
|
| 222 |
+
x_lat = x_lat.unsqueeze(0).to(device=device, dtype=next(vae.parameters()).dtype)
|
| 223 |
+
img = vae.decode(x_lat)
|
| 224 |
+
if hasattr(img, "sample"):
|
| 225 |
+
img = img.sample
|
| 226 |
+
return text_str, img.squeeze(0).float().clamp(-1, 1)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@torch.no_grad()
|
| 230 |
+
def interleave_generate_joint(
|
| 231 |
+
model, vae, processor,
|
| 232 |
+
obs_frames: List[str], task_text: str, num_frames: int,
|
| 233 |
+
height: int, width: int, num_steps: int, device, dtype, out_dir: str,
|
| 234 |
+
max_text_tokens: int = 64,
|
| 235 |
+
):
|
| 236 |
+
"""v2 rollout: at each step the model emits plan text + a frame; the decoded
|
| 237 |
+
frame AND its plan text are fed back as context for the next step — prior
|
| 238 |
+
frames via SigLIP, prior texts kept — matching the JOINT training layout."""
|
| 239 |
+
import os
|
| 240 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 241 |
+
gen_paths, texts = [], []
|
| 242 |
+
prior_steps = [] # (text, frame_path) accumulated across steps
|
| 243 |
+
for k in range(num_frames):
|
| 244 |
+
text, img = generate_step_joint(
|
| 245 |
+
model, vae, processor, list(obs_frames), task_text,
|
| 246 |
+
height, width, num_steps, device, dtype, max_text_tokens,
|
| 247 |
+
prior_steps=list(prior_steps),
|
| 248 |
+
)
|
| 249 |
+
arr = ((img.cpu().permute(1, 2, 0).numpy() + 1.0) * 127.5).clip(0, 255).astype("uint8")
|
| 250 |
+
path = os.path.join(out_dir, f"joint_step{k+1}.png")
|
| 251 |
+
Image.fromarray(arr).save(path)
|
| 252 |
+
gen_paths.append(path)
|
| 253 |
+
texts.append(text)
|
| 254 |
+
prior_steps.append((text, path))
|
| 255 |
+
print(f" [joint step {k+1}/{num_frames}] prior={len(prior_steps)-1} | TEXT: {text[:80]!r} -> {path}")
|
| 256 |
+
return gen_paths, texts
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def _row(paths, w, h):
|
| 260 |
+
"""Horizontal strip of images (resized to w x h, 4px gaps)."""
|
| 261 |
+
imgs = [Image.open(p).convert("RGB").resize((w, h)) for p in paths]
|
| 262 |
+
n = len(imgs)
|
| 263 |
+
strip = Image.new("RGB", (w * n + 4 * (n - 1), h), (20, 20, 20))
|
| 264 |
+
for i, im in enumerate(imgs):
|
| 265 |
+
strip.paste(im, (i * (w + 4), 0))
|
| 266 |
+
return strip
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def main():
|
| 270 |
+
ap = argparse.ArgumentParser(description="Joint interleaved rollout: decode ALL plan text + ALL frames.")
|
| 271 |
+
ap.add_argument("--ckpt", required=True)
|
| 272 |
+
ap.add_argument("--vae", required=True)
|
| 273 |
+
ap.add_argument("--frames", nargs="+", required=True, help="observation frame path(s)")
|
| 274 |
+
ap.add_argument("--task", required=True, help="overall task text")
|
| 275 |
+
ap.add_argument("--max_frames", type=int, default=None, help="cap #frames to generate")
|
| 276 |
+
ap.add_argument("--out_dir", default="out_interleave")
|
| 277 |
+
ap.add_argument("--height", type=int, default=144)
|
| 278 |
+
ap.add_argument("--width", type=int, default=256)
|
| 279 |
+
ap.add_argument("--num_steps", type=int, default=50, help="ODE steps per frame")
|
| 280 |
+
ap.add_argument("--max_text_tokens", type=int, default=96)
|
| 281 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 282 |
+
ap.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16", "float32"])
|
| 283 |
+
ap.add_argument("--understanding_max_pixels", type=int, default=524288,
|
| 284 |
+
help="Cap obs/prior-frame ViT input pixels to MATCH training. "
|
| 285 |
+
"Default 524288 (~494 tok/frame); the model default 4194304 (~4050 tok/frame) "
|
| 286 |
+
"is an 8x train/infer resolution mismatch that degrades eval. Set 0 to disable.")
|
| 287 |
+
args = ap.parse_args()
|
| 288 |
+
|
| 289 |
+
from model import UnifiedMoTForConditionalGeneration, maybe_init_generation_path
|
| 290 |
+
from vae_model.autoencoder import load_ae
|
| 291 |
+
|
| 292 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 293 |
+
dtype = {"bfloat16": torch.bfloat16, "float16": torch.float16, "float32": torch.float32}[args.dtype]
|
| 294 |
+
torch.manual_seed(args.seed)
|
| 295 |
+
|
| 296 |
+
# ---- resolve obs frames + task ----
|
| 297 |
+
obs, task, n_frames = args.frames, args.task, (args.max_frames or 1)
|
| 298 |
+
|
| 299 |
+
processor = HunYuanVLMoTProcessor.from_pretrained(args.ckpt, trust_remote_code=True)
|
| 300 |
+
# train==infer parity: cap obs/prior-frame ViT pixels to the SAME value training
|
| 301 |
+
# used. Prior frames re-enter via SigLIP at rollout, so this must match training too.
|
| 302 |
+
if args.understanding_max_pixels and args.understanding_max_pixels > 0:
|
| 303 |
+
ip = processor.image_processor
|
| 304 |
+
ip.max_pixels = args.understanding_max_pixels
|
| 305 |
+
if isinstance(getattr(ip, "size", None), dict) and "longest_edge" in ip.size:
|
| 306 |
+
ip.size["longest_edge"] = args.understanding_max_pixels
|
| 307 |
+
model = UnifiedMoTForConditionalGeneration.from_pretrained(args.ckpt, dtype=dtype)
|
| 308 |
+
maybe_init_generation_path(model, model_load_path=args.ckpt)
|
| 309 |
+
model.to(device).eval()
|
| 310 |
+
vae, _ = load_ae(args.vae)
|
| 311 |
+
vae.requires_grad_(False)
|
| 312 |
+
vae.eval()
|
| 313 |
+
vae.to(device, dtype=dtype)
|
| 314 |
+
|
| 315 |
+
os.makedirs(args.out_dir, exist_ok=True)
|
| 316 |
+
print(f"JOINT interleaved rollout | obs={len(obs)} frame(s) | steps={n_frames}")
|
| 317 |
+
print(f"TASK: {task}\n")
|
| 318 |
+
|
| 319 |
+
gen_paths, gen_texts = interleave_generate_joint(
|
| 320 |
+
model, vae, processor, obs, task, n_frames,
|
| 321 |
+
args.height, args.width, args.num_steps, device, dtype, args.out_dir,
|
| 322 |
+
max_text_tokens=args.max_text_tokens,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
# ---- write the FULL decoded output (text) + montages (image) ----
|
| 326 |
+
lines = [f"TASK: {task}", ""]
|
| 327 |
+
for k in range(len(gen_texts)):
|
| 328 |
+
lines.append(f"--- step {k+1} ---")
|
| 329 |
+
lines.append(f"GEN text: {gen_texts[k]}")
|
| 330 |
+
lines.append("")
|
| 331 |
+
txt_path = os.path.join(args.out_dir, "result.txt")
|
| 332 |
+
open(txt_path, "w").write("\n".join(lines))
|
| 333 |
+
print("\n".join(lines))
|
| 334 |
+
|
| 335 |
+
# image montage: GEN row
|
| 336 |
+
width, height = args.width, args.height
|
| 337 |
+
gen_row = _row(gen_paths, width, height)
|
| 338 |
+
gen_row.save(os.path.join(args.out_dir, "rollout_GEN.png"))
|
| 339 |
+
print(f"\nDecoded {len(gen_paths)} frames + {len(gen_texts)} texts -> {args.out_dir}")
|
| 340 |
+
print(f" text : {txt_path}")
|
| 341 |
+
print(f" images: {args.out_dir}/joint_step*.png + rollout montage")
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
if __name__ == "__main__":
|
| 345 |
+
main()
|
model/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HY-Unified model package.
|
| 2 |
+
|
| 3 |
+
New entry points — built on transformers 4.57's upstream
|
| 4 |
+
hunyuan_vl_mot base model, extended with the third (`_g`) generation MoT path
|
| 5 |
+
and Flow Matching modules.
|
| 6 |
+
|
| 7 |
+
Importing this package triggers the module-level replacement of upstream's
|
| 8 |
+
`_flash_attention_forward_mot` with our packed/padded/decode three-stage
|
| 9 |
+
version (see `attention_mot_packed`).
|
| 10 |
+
|
| 11 |
+
Public surface:
|
| 12 |
+
UnifiedMoTConfig, UnifiedMoTTextConfig
|
| 13 |
+
UnifiedMoTForConditionalGeneration, UnifiedMoTModel, UnifiedMoTOutput
|
| 14 |
+
MoTDecoderLayer
|
| 15 |
+
mask_apply_3way
|
| 16 |
+
maybe_init_generation_path
|
| 17 |
+
TimestepEmbedder, PositionEmbedding
|
| 18 |
+
pack_latents, sample_timesteps, build_noisy_latent, patch_boundary_loss
|
| 19 |
+
"""
|
| 20 |
+
# Order matters: attention rebind must happen before any HunYuanVLMoT* instantiation.
|
| 21 |
+
from . import attention_mot_packed # noqa: F401 (side effect)
|
| 22 |
+
|
| 23 |
+
from .config_unified_mot import UnifiedMoTConfig, UnifiedMoTTextConfig
|
| 24 |
+
from .modeling_decoder_mot import MoTDecoderLayer, mask_apply_3way
|
| 25 |
+
from .modeling_text_model_mot import MoTTextModel, MoTTextForCausalLM
|
| 26 |
+
from .modeling_unified_mot import (
|
| 27 |
+
UnifiedMoTForConditionalGeneration,
|
| 28 |
+
UnifiedMoTModel,
|
| 29 |
+
UnifiedMoTOutput,
|
| 30 |
+
HY_VL_MOT_IMAGE_TOKEN_ID,
|
| 31 |
+
HY_VL_MOT_VIDEO_TOKEN_ID,
|
| 32 |
+
)
|
| 33 |
+
from .mot_init_utils import (
|
| 34 |
+
maybe_init_generation_path,
|
| 35 |
+
init_mlp_g_from_mlp_v_net2wider,
|
| 36 |
+
verify_mlp_g_equals_mlp_v,
|
| 37 |
+
checkpoint_has_g_keys,
|
| 38 |
+
)
|
| 39 |
+
from .flow_matching_modules import (
|
| 40 |
+
TimestepEmbedder,
|
| 41 |
+
PositionEmbedding,
|
| 42 |
+
pack_latents,
|
| 43 |
+
sample_timesteps,
|
| 44 |
+
build_noisy_latent,
|
| 45 |
+
patch_boundary_loss,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
__all__ = [
|
| 50 |
+
# Config
|
| 51 |
+
"UnifiedMoTConfig", "UnifiedMoTTextConfig",
|
| 52 |
+
# Models
|
| 53 |
+
"UnifiedMoTForConditionalGeneration", "UnifiedMoTModel", "UnifiedMoTOutput",
|
| 54 |
+
"MoTDecoderLayer", "MoTTextModel", "MoTTextForCausalLM",
|
| 55 |
+
"mask_apply_3way",
|
| 56 |
+
# Token IDs
|
| 57 |
+
"HY_VL_MOT_IMAGE_TOKEN_ID", "HY_VL_MOT_VIDEO_TOKEN_ID",
|
| 58 |
+
# Initialization
|
| 59 |
+
"maybe_init_generation_path", "init_mlp_g_from_mlp_v_net2wider",
|
| 60 |
+
"verify_mlp_g_equals_mlp_v", "checkpoint_has_g_keys",
|
| 61 |
+
# Flow Matching utils
|
| 62 |
+
"TimestepEmbedder", "PositionEmbedding",
|
| 63 |
+
"pack_latents", "sample_timesteps", "build_noisy_latent", "patch_boundary_loss",
|
| 64 |
+
]
|
model/attention_mot_packed.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Three-stage packed/padded MoT flash attention for UnifiedMoT.
|
| 2 |
+
|
| 3 |
+
This module replaces the upstream `_flash_attention_forward_mot` (a module-level
|
| 4 |
+
function in `transformers.models.hunyuan_vl_mot.modeling_hunyuan_vl_mot`) with a
|
| 5 |
+
version that supports three modes:
|
| 6 |
+
|
| 7 |
+
A) Packed prefill (P-Pack training)
|
| 8 |
+
attention_mask is a dict carrying `cu_seqlens` (N+1,), `sample_ids` (1, T),
|
| 9 |
+
and `g_seqlens` (M, 2). Inputs are (1, T, H, D) — a single packed row of
|
| 10 |
+
concatenated samples. Three-pass attention:
|
| 11 |
+
1) Causal varlen — `cu_seqlens` enforces sample boundaries
|
| 12 |
+
2) Visual bidirectional override on each `v_seqlens` segment (absolute coords)
|
| 13 |
+
3) Generation-block override on each `g_seqlens` segment, with KV range
|
| 14 |
+
`[sample_start, g_e]` so KV NEVER crosses sample boundaries
|
| 15 |
+
|
| 16 |
+
B) Padded prefill (legacy / inference fallback)
|
| 17 |
+
attention_mask dict has `padding_mask` (B, S). Same as upstream:
|
| 18 |
+
unpad → varlen → repad. Visual override layered on top. Generation override
|
| 19 |
+
not used in this mode.
|
| 20 |
+
|
| 21 |
+
C) Decode (KV-cache active, S_q != S_k)
|
| 22 |
+
Simple (B, 1, H, D) path with no varlen.
|
| 23 |
+
|
| 24 |
+
The replacement is **module-scope rebinding** of upstream's
|
| 25 |
+
`_flash_attention_forward_mot` — surgical and reversible. Apply by importing
|
| 26 |
+
this module before instantiating any HunYuanVLMoT* class:
|
| 27 |
+
|
| 28 |
+
from model import attention_mot_packed # noqa: F401
|
| 29 |
+
"""
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import logging
|
| 33 |
+
from typing import Optional
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
import torch.nn as nn
|
| 37 |
+
|
| 38 |
+
from flash_attn import flash_attn_varlen_func
|
| 39 |
+
from transformers.models.hunyuan_vl_mot import modeling_hunyuan_vl_mot as _upstream
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
# Helpers
|
| 46 |
+
# ---------------------------------------------------------------------------
|
| 47 |
+
|
| 48 |
+
def _has_ranges(obj) -> bool:
|
| 49 |
+
if obj is None:
|
| 50 |
+
return False
|
| 51 |
+
if torch.is_tensor(obj):
|
| 52 |
+
return obj.numel() > 0 and obj.shape[-1] in (2, 3)
|
| 53 |
+
if isinstance(obj, list):
|
| 54 |
+
if not obj:
|
| 55 |
+
return False
|
| 56 |
+
if torch.is_tensor(obj[0]):
|
| 57 |
+
return any(t.numel() > 0 for t in obj)
|
| 58 |
+
return True
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _iter_g_ranges(obj):
|
| 63 |
+
"""Yield (g_start, g_end, batch_idx) triples from `g_seqlens`.
|
| 64 |
+
|
| 65 |
+
Accepts:
|
| 66 |
+
- Tensor of shape (M, 2) — absolute packed coords (b_idx implicit = 0)
|
| 67 |
+
- Tensor of shape (M, 3) — (s, e, b_idx)
|
| 68 |
+
- List of tuples — same as above
|
| 69 |
+
- List of (N_i, 2) tensors (per-sample, padded mode)
|
| 70 |
+
"""
|
| 71 |
+
if torch.is_tensor(obj):
|
| 72 |
+
for row in obj.tolist():
|
| 73 |
+
if len(row) == 2:
|
| 74 |
+
yield int(row[0]), int(row[1]), 0
|
| 75 |
+
else:
|
| 76 |
+
yield int(row[0]), int(row[1]), int(row[2])
|
| 77 |
+
elif isinstance(obj, list):
|
| 78 |
+
if obj and torch.is_tensor(obj[0]):
|
| 79 |
+
for b_idx, t in enumerate(obj):
|
| 80 |
+
if t.numel() == 0:
|
| 81 |
+
continue
|
| 82 |
+
for row in t.tolist():
|
| 83 |
+
yield int(row[0]), int(row[1]), b_idx
|
| 84 |
+
else:
|
| 85 |
+
for item in obj:
|
| 86 |
+
if torch.is_tensor(item):
|
| 87 |
+
item = item.tolist()
|
| 88 |
+
if len(item) == 2:
|
| 89 |
+
yield int(item[0]), int(item[1]), 0
|
| 90 |
+
else:
|
| 91 |
+
yield int(item[0]), int(item[1]), int(item[2])
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _find_sample_start(cu_seqlens: torch.Tensor, g_s: int) -> int:
|
| 95 |
+
"""Largest cu_seqlens[i] <= g_s. Used when sample_ids is unavailable."""
|
| 96 |
+
cu = cu_seqlens.tolist() if torch.is_tensor(cu_seqlens) else cu_seqlens
|
| 97 |
+
lo, hi = 0, len(cu) - 1
|
| 98 |
+
while lo < hi:
|
| 99 |
+
mid = (lo + hi + 1) // 2
|
| 100 |
+
if cu[mid] <= g_s:
|
| 101 |
+
lo = mid
|
| 102 |
+
else:
|
| 103 |
+
hi = mid - 1
|
| 104 |
+
return int(cu[lo])
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ---------------------------------------------------------------------------
|
| 108 |
+
# Step 2 helper: visual bidirectional override on (B, S, H, D) tensors
|
| 109 |
+
# ---------------------------------------------------------------------------
|
| 110 |
+
|
| 111 |
+
def _apply_visual_bidirectional(
|
| 112 |
+
attn_output: torch.Tensor,
|
| 113 |
+
query: torch.Tensor,
|
| 114 |
+
key: torch.Tensor,
|
| 115 |
+
value: torch.Tensor,
|
| 116 |
+
v_seqlens,
|
| 117 |
+
) -> torch.Tensor:
|
| 118 |
+
"""Override input-image segments with non-causal flash attention.
|
| 119 |
+
|
| 120 |
+
Operates on (B, S, H, D). v_seqlens may be a (M, 2) tensor (B==1) or a list
|
| 121 |
+
of (N_i, 2) tensors (per-sample for B>1 padded mode).
|
| 122 |
+
|
| 123 |
+
PERF: convert each segs tensor to CPU once (`.tolist()`) instead of
|
| 124 |
+
issuing two `.item()` syncs per row inside the inner loop. Each layer
|
| 125 |
+
calls this once during forward and once during backward; without the
|
| 126 |
+
fix, 32 layers × ~25 segments × 2 .item()/seg × 2 (fwd+bwd) ≈ 3200
|
| 127 |
+
GPU→CPU syncs per step, each blocking on the previous attention kernel.
|
| 128 |
+
"""
|
| 129 |
+
device = query.device
|
| 130 |
+
if isinstance(v_seqlens, list):
|
| 131 |
+
segs_list = v_seqlens
|
| 132 |
+
else:
|
| 133 |
+
segs_list = [v_seqlens] # B==1 path
|
| 134 |
+
|
| 135 |
+
visual_q = []
|
| 136 |
+
visual_k = []
|
| 137 |
+
visual_v = []
|
| 138 |
+
cu_v = [0]
|
| 139 |
+
max_v = 0
|
| 140 |
+
write_back = [] # (b_idx, s, e)
|
| 141 |
+
has_any = False
|
| 142 |
+
|
| 143 |
+
for b_idx, segs in enumerate(segs_list):
|
| 144 |
+
if segs is None or not torch.is_tensor(segs) or segs.numel() == 0:
|
| 145 |
+
continue
|
| 146 |
+
# PERF: one bulk D2H copy instead of per-element .item()
|
| 147 |
+
segs_cpu = segs.tolist()
|
| 148 |
+
for row in segs_cpu:
|
| 149 |
+
s = int(row[0])
|
| 150 |
+
e = int(row[1])
|
| 151 |
+
if e <= s:
|
| 152 |
+
continue
|
| 153 |
+
has_any = True
|
| 154 |
+
visual_q.append(query[b_idx, s:e])
|
| 155 |
+
visual_k.append(key[b_idx, s:e])
|
| 156 |
+
visual_v.append(value[b_idx, s:e])
|
| 157 |
+
ln = e - s
|
| 158 |
+
cu_v.append(cu_v[-1] + ln)
|
| 159 |
+
max_v = max(max_v, ln)
|
| 160 |
+
write_back.append((b_idx, s, e))
|
| 161 |
+
|
| 162 |
+
if not has_any:
|
| 163 |
+
# Preserve autograd graph topology across ranks even when this rank has
|
| 164 |
+
# no visual segments (matches upstream's `fake_visual` trick).
|
| 165 |
+
dummy = query[:1, :1].sum() * 0
|
| 166 |
+
return attn_output + dummy
|
| 167 |
+
|
| 168 |
+
vq = torch.cat(visual_q, dim=0)
|
| 169 |
+
vk = torch.cat(visual_k, dim=0)
|
| 170 |
+
vv = torch.cat(visual_v, dim=0)
|
| 171 |
+
cu_v_t = torch.tensor(cu_v, device=device, dtype=torch.int32)
|
| 172 |
+
|
| 173 |
+
vis_out = flash_attn_varlen_func(
|
| 174 |
+
vq, vk, vv,
|
| 175 |
+
cu_seqlens_q=cu_v_t, cu_seqlens_k=cu_v_t,
|
| 176 |
+
max_seqlen_q=max_v, max_seqlen_k=max_v,
|
| 177 |
+
causal=False,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
attn_output = attn_output.clone()
|
| 181 |
+
off = 0
|
| 182 |
+
for b_idx, s, e in write_back:
|
| 183 |
+
ln = e - s
|
| 184 |
+
attn_output[b_idx, s:e] = vis_out[off:off + ln]
|
| 185 |
+
off += ln
|
| 186 |
+
return attn_output
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# ---------------------------------------------------------------------------
|
| 190 |
+
# Step 3 helper: generation-block override (P-Pack only)
|
| 191 |
+
# ---------------------------------------------------------------------------
|
| 192 |
+
|
| 193 |
+
def _apply_generation_block(
|
| 194 |
+
attn_output: torch.Tensor,
|
| 195 |
+
query: torch.Tensor,
|
| 196 |
+
key: torch.Tensor,
|
| 197 |
+
value: torch.Tensor,
|
| 198 |
+
g_seqlens,
|
| 199 |
+
cu_seqlens: torch.Tensor,
|
| 200 |
+
sample_ids: Optional[torch.Tensor],
|
| 201 |
+
) -> torch.Tensor:
|
| 202 |
+
"""Q over each generation block, K/V from sample-start..g_end (no cross-sample).
|
| 203 |
+
|
| 204 |
+
PERF: previously each iteration of the per-block loop did:
|
| 205 |
+
- `sample_ids[0, g_s].item()` — D2H sync
|
| 206 |
+
- `cu_seqlens[sid].item()` — D2H sync
|
| 207 |
+
- `torch.tensor([0, q_len], device=cuda, ...)` — small tensor alloc + H2D
|
| 208 |
+
- same for cu_k_g
|
| 209 |
+
For 25 gen blocks × 32 layers × 2 (fwd+bwd) ≈ 3200 ops/step, each blocking
|
| 210 |
+
on the prior attention kernel. Now we do **one** D2H copy of cu_seqlens
|
| 211 |
+
and sample_ids[0] up front, build all `cu_q_g` / `cu_k_g` pairs as a
|
| 212 |
+
single (M, 2) tensor with one H2D, and view rows in the loop.
|
| 213 |
+
"""
|
| 214 |
+
device = query.device
|
| 215 |
+
|
| 216 |
+
# ---- One-shot metadata D2H ------------------------------------------
|
| 217 |
+
if sample_ids is not None and torch.is_tensor(sample_ids):
|
| 218 |
+
sample_ids_cpu = (
|
| 219 |
+
sample_ids[0].tolist() if sample_ids.dim() == 2 else sample_ids.tolist()
|
| 220 |
+
)
|
| 221 |
+
else:
|
| 222 |
+
sample_ids_cpu = None
|
| 223 |
+
if torch.is_tensor(cu_seqlens):
|
| 224 |
+
cu_seqlens_cpu = cu_seqlens.tolist()
|
| 225 |
+
else:
|
| 226 |
+
cu_seqlens_cpu = list(cu_seqlens)
|
| 227 |
+
|
| 228 |
+
# ---- Pre-compute per-block specs entirely on CPU --------------------
|
| 229 |
+
specs = [] # (g_s, g_e, b_idx, sample_start, q_len, kv_len)
|
| 230 |
+
for g_s, g_e, b_idx in _iter_g_ranges(g_seqlens):
|
| 231 |
+
if g_e <= g_s:
|
| 232 |
+
continue
|
| 233 |
+
if sample_ids_cpu is not None:
|
| 234 |
+
sid = sample_ids_cpu[g_s]
|
| 235 |
+
sample_start = cu_seqlens_cpu[sid]
|
| 236 |
+
else:
|
| 237 |
+
sample_start = _find_sample_start(cu_seqlens_cpu, g_s)
|
| 238 |
+
q_len = g_e - g_s
|
| 239 |
+
kv_len = g_e - sample_start
|
| 240 |
+
specs.append((g_s, g_e, b_idx, sample_start, q_len, kv_len))
|
| 241 |
+
|
| 242 |
+
if not specs:
|
| 243 |
+
# No generation blocks on this rank — preserve autograd topology.
|
| 244 |
+
dummy = query[:1, :1].sum() * 0
|
| 245 |
+
return attn_output + dummy
|
| 246 |
+
|
| 247 |
+
# ---- Single H2D for all cu pairs ------------------------------------
|
| 248 |
+
cu_q_all = torch.tensor(
|
| 249 |
+
[[0, s[4]] for s in specs], device=device, dtype=torch.int32
|
| 250 |
+
)
|
| 251 |
+
cu_k_all = torch.tensor(
|
| 252 |
+
[[0, s[5]] for s in specs], device=device, dtype=torch.int32
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
attn_output = attn_output.clone()
|
| 256 |
+
for i, (g_s, g_e, b_idx, sample_start, q_len, kv_len) in enumerate(specs):
|
| 257 |
+
gen_out = flash_attn_varlen_func(
|
| 258 |
+
query[b_idx, g_s:g_e].contiguous(),
|
| 259 |
+
key[b_idx, sample_start:g_e].contiguous(),
|
| 260 |
+
value[b_idx, sample_start:g_e].contiguous(),
|
| 261 |
+
cu_seqlens_q=cu_q_all[i], cu_seqlens_k=cu_k_all[i],
|
| 262 |
+
max_seqlen_q=q_len, max_seqlen_k=kv_len,
|
| 263 |
+
causal=False,
|
| 264 |
+
)
|
| 265 |
+
attn_output[b_idx, g_s:g_e] = gen_out
|
| 266 |
+
return attn_output
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
# ---------------------------------------------------------------------------
|
| 270 |
+
# Main entry point — drop-in replacement for upstream's _flash_attention_forward_mot
|
| 271 |
+
# ---------------------------------------------------------------------------
|
| 272 |
+
|
| 273 |
+
def flash_attention_forward_mot_packed(
|
| 274 |
+
module: nn.Module,
|
| 275 |
+
query: torch.Tensor,
|
| 276 |
+
key: torch.Tensor,
|
| 277 |
+
value: torch.Tensor,
|
| 278 |
+
attention_mask,
|
| 279 |
+
dropout: float = 0.0,
|
| 280 |
+
scaling: Optional[float] = None,
|
| 281 |
+
sliding_window: Optional[int] = None,
|
| 282 |
+
softcap: Optional[float] = None,
|
| 283 |
+
**kwargs,
|
| 284 |
+
):
|
| 285 |
+
"""Packed/padded/decode dispatcher. See module docstring."""
|
| 286 |
+
_upstream._check_flash_attn()
|
| 287 |
+
|
| 288 |
+
if kwargs.get("output_attentions", False):
|
| 289 |
+
logger.warning_once("`flash_attention_2` does not support `output_attentions=True`.")
|
| 290 |
+
|
| 291 |
+
# (B, heads, S, D) -> (B, S, heads, D)
|
| 292 |
+
query = query.transpose(1, 2)
|
| 293 |
+
key = key.transpose(1, 2)
|
| 294 |
+
value = value.transpose(1, 2)
|
| 295 |
+
|
| 296 |
+
if query.dtype == torch.float32:
|
| 297 |
+
if torch.is_autocast_enabled():
|
| 298 |
+
target_dtype = torch.get_autocast_gpu_dtype()
|
| 299 |
+
elif hasattr(module.config, "_pre_quantization_dtype"):
|
| 300 |
+
target_dtype = module.config._pre_quantization_dtype
|
| 301 |
+
else:
|
| 302 |
+
target_dtype = next(m for m in module.modules() if isinstance(m, nn.Linear)).weight.dtype
|
| 303 |
+
query, key, value = query.to(target_dtype), key.to(target_dtype), value.to(target_dtype)
|
| 304 |
+
|
| 305 |
+
bsz, s_q, n_heads, head_dim = query.shape
|
| 306 |
+
s_k = key.shape[1]
|
| 307 |
+
|
| 308 |
+
if isinstance(attention_mask, dict):
|
| 309 |
+
v_seqlens = attention_mask.get("v_seqlens", None)
|
| 310 |
+
g_seqlens = attention_mask.get("g_seqlens", None)
|
| 311 |
+
cu_seqlens_packed = attention_mask.get("cu_seqlens", None)
|
| 312 |
+
padding_mask = attention_mask.get("padding_mask", None)
|
| 313 |
+
sample_ids = attention_mask.get("sample_ids", None)
|
| 314 |
+
# PERF: pre-computed by the language_model wrapper so we avoid a D2H
|
| 315 |
+
# sync on every layer. None means we still have to compute it here.
|
| 316 |
+
max_seqlen_packed = attention_mask.get("max_seqlen_packed", None)
|
| 317 |
+
else:
|
| 318 |
+
# Dummy: allow odd callers (e.g., HF generation builders) that pass tensor masks.
|
| 319 |
+
v_seqlens = None
|
| 320 |
+
g_seqlens = None
|
| 321 |
+
cu_seqlens_packed = None
|
| 322 |
+
padding_mask = None
|
| 323 |
+
sample_ids = None
|
| 324 |
+
max_seqlen_packed = None
|
| 325 |
+
|
| 326 |
+
packed_mode = (cu_seqlens_packed is not None) and (s_q == s_k) and (bsz == 1)
|
| 327 |
+
|
| 328 |
+
# ---------------- Mode A: Packed prefill ---------------------------------
|
| 329 |
+
if packed_mode:
|
| 330 |
+
h_kv = key.shape[2]
|
| 331 |
+
q_flat = query.contiguous().view(s_q, n_heads, head_dim)
|
| 332 |
+
k_flat = key.contiguous().view(s_k, h_kv, head_dim)
|
| 333 |
+
v_flat = value.contiguous().view(s_k, h_kv, head_dim)
|
| 334 |
+
|
| 335 |
+
cu = cu_seqlens_packed.to(device=query.device, dtype=torch.int32)
|
| 336 |
+
# Use pre-computed max_seqlen if the wrapper supplied it (saves a
|
| 337 |
+
# D2H sync per layer × 32 layers).
|
| 338 |
+
if max_seqlen_packed is not None:
|
| 339 |
+
max_seqlen = max_seqlen_packed
|
| 340 |
+
else:
|
| 341 |
+
with torch.no_grad():
|
| 342 |
+
max_seqlen = int((cu[1:] - cu[:-1]).max().item())
|
| 343 |
+
|
| 344 |
+
attn_flat = flash_attn_varlen_func(
|
| 345 |
+
q_flat, k_flat, v_flat,
|
| 346 |
+
cu_seqlens_q=cu, cu_seqlens_k=cu,
|
| 347 |
+
max_seqlen_q=max_seqlen, max_seqlen_k=max_seqlen,
|
| 348 |
+
causal=True,
|
| 349 |
+
)
|
| 350 |
+
attn_output = attn_flat.reshape(1, s_q, n_heads, head_dim)
|
| 351 |
+
|
| 352 |
+
if v_seqlens is not None:
|
| 353 |
+
attn_output = _apply_visual_bidirectional(attn_output, query, key, value, v_seqlens)
|
| 354 |
+
|
| 355 |
+
if _has_ranges(g_seqlens):
|
| 356 |
+
attn_output = _apply_generation_block(
|
| 357 |
+
attn_output, query, key, value, g_seqlens, cu, sample_ids
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
return attn_output, None
|
| 361 |
+
|
| 362 |
+
# ---------------- Mode B: Padded prefill --------------------------------
|
| 363 |
+
if padding_mask is not None and bsz > 1 and s_q == s_k:
|
| 364 |
+
pad_bool = padding_mask.bool()
|
| 365 |
+
seqlens = padding_mask.sum(dim=-1).to(torch.int32)
|
| 366 |
+
cu_seqlens = torch.zeros(bsz + 1, device=query.device, dtype=torch.int32)
|
| 367 |
+
cu_seqlens[1:] = torch.cumsum(seqlens, dim=0)
|
| 368 |
+
max_seqlen = int(seqlens.max().item())
|
| 369 |
+
|
| 370 |
+
q_unpad = query[pad_bool]
|
| 371 |
+
k_unpad = key[pad_bool]
|
| 372 |
+
v_unpad = value[pad_bool]
|
| 373 |
+
|
| 374 |
+
out_unpad = flash_attn_varlen_func(
|
| 375 |
+
q_unpad, k_unpad, v_unpad,
|
| 376 |
+
cu_seqlens_q=cu_seqlens, cu_seqlens_k=cu_seqlens,
|
| 377 |
+
max_seqlen_q=max_seqlen, max_seqlen_k=max_seqlen,
|
| 378 |
+
causal=True,
|
| 379 |
+
)
|
| 380 |
+
attn_output = query.new_zeros(bsz, s_q, n_heads, head_dim)
|
| 381 |
+
attn_output[pad_bool] = out_unpad
|
| 382 |
+
|
| 383 |
+
if v_seqlens is not None:
|
| 384 |
+
attn_output = _apply_visual_bidirectional(attn_output, query, key, value, v_seqlens)
|
| 385 |
+
|
| 386 |
+
# Padded path supports generation-block override too (B>1 with per-sample triples)
|
| 387 |
+
if _has_ranges(g_seqlens):
|
| 388 |
+
# Build a synthetic cu_seqlens from padding_mask for sample_start lookup
|
| 389 |
+
cu_pad = torch.zeros(bsz + 1, device=query.device, dtype=torch.int32)
|
| 390 |
+
cu_pad[1:] = torch.cumsum(seqlens, dim=0)
|
| 391 |
+
attn_output = _apply_generation_block(
|
| 392 |
+
attn_output, query, key, value, g_seqlens, cu_pad, sample_ids
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
return attn_output, None
|
| 396 |
+
|
| 397 |
+
# ---------------- Mode C: Single-sample / decode ------------------------
|
| 398 |
+
h_kv = key.shape[2]
|
| 399 |
+
q_flat = query.contiguous().view(bsz * s_q, n_heads, head_dim)
|
| 400 |
+
k_flat = key.contiguous().view(bsz * s_k, h_kv, head_dim)
|
| 401 |
+
v_flat = value.contiguous().view(bsz * s_k, h_kv, head_dim)
|
| 402 |
+
cu_q_t = torch.arange(0, bsz + 1, dtype=torch.int32, device=query.device) * s_q
|
| 403 |
+
cu_k_t = torch.arange(0, bsz + 1, dtype=torch.int32, device=query.device) * s_k
|
| 404 |
+
|
| 405 |
+
attn_flat = flash_attn_varlen_func(
|
| 406 |
+
q_flat, k_flat, v_flat,
|
| 407 |
+
cu_seqlens_q=cu_q_t, cu_seqlens_k=cu_k_t,
|
| 408 |
+
max_seqlen_q=s_q, max_seqlen_k=s_k,
|
| 409 |
+
causal=(s_q == s_k),
|
| 410 |
+
)
|
| 411 |
+
attn_output = attn_flat.reshape(bsz, s_q, n_heads, head_dim)
|
| 412 |
+
|
| 413 |
+
# Visual override is valid during prefill (S_q == S_k) only
|
| 414 |
+
if v_seqlens is not None and s_q == s_k:
|
| 415 |
+
attn_output = _apply_visual_bidirectional(attn_output, query, key, value, v_seqlens)
|
| 416 |
+
|
| 417 |
+
if _has_ranges(g_seqlens) and s_q == s_k and bsz == 1:
|
| 418 |
+
# B==1 inference: synthetic cu_seqlens covers the whole row
|
| 419 |
+
cu_one = torch.tensor([0, s_q], device=query.device, dtype=torch.int32)
|
| 420 |
+
attn_output = _apply_generation_block(
|
| 421 |
+
attn_output, query, key, value, g_seqlens, cu_one, sample_ids
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
return attn_output, None
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
# ---------------------------------------------------------------------------
|
| 428 |
+
# Activate: rebind upstream module-level reference
|
| 429 |
+
# ---------------------------------------------------------------------------
|
| 430 |
+
|
| 431 |
+
_upstream._flash_attention_forward_mot = flash_attention_forward_mot_packed
|
| 432 |
+
logger.info("attention_mot_packed: replaced upstream _flash_attention_forward_mot")
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
__all__ = ["flash_attention_forward_mot_packed"]
|
model/config_unified_mot.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for UnifiedMoT.
|
| 2 |
+
|
| 3 |
+
Subclasses transformers.models.hunyuan_vl_mot config to add HY-Unified
|
| 4 |
+
project-level fields used by the Flow Matching path:
|
| 5 |
+
|
| 6 |
+
- mlp_g_intermediate_size / mlp_g_init_noise_std (Net2Wider on generation MLP)
|
| 7 |
+
- boundary_loss_weight (patch-boundary smoothness L1)
|
| 8 |
+
- latent_patch_size / max_latent_size (FM latent grid)
|
| 9 |
+
- vae_image_downsample / vae_z_channels (VAE encoder spec)
|
| 10 |
+
- timestep_shift (t-distribution shift for FM)
|
| 11 |
+
- use_moe (toggle moe_layers wrapper)
|
| 12 |
+
- flow_loss_weight / text_loss_weight (loss aggregation knobs)
|
| 13 |
+
|
| 14 |
+
The text-level fields (mlp_g_*) live on text_config because the proxy in
|
| 15 |
+
HunYuanVLMoTConfig surfaces them at the top level too.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
from typing import Optional
|
| 20 |
+
|
| 21 |
+
from transformers.models.hunyuan_vl_mot.configuration_hunyuan_vl_mot import (
|
| 22 |
+
HunYuanVLMoTConfig,
|
| 23 |
+
HunYuanVLMoTTextConfig,
|
| 24 |
+
HunYuanVLMoTVisionConfig,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class UnifiedMoTTextConfig(HunYuanVLMoTTextConfig):
|
| 29 |
+
"""Extends upstream text config with the three-path MoT (mlp_g) knobs."""
|
| 30 |
+
|
| 31 |
+
model_type = "unified_mot"
|
| 32 |
+
|
| 33 |
+
def __init__(
|
| 34 |
+
self,
|
| 35 |
+
mlp_g_intermediate_size: Optional[int] = None,
|
| 36 |
+
mlp_g_init_noise_std: float = 1e-4,
|
| 37 |
+
use_moe: bool = False,
|
| 38 |
+
**kwargs,
|
| 39 |
+
):
|
| 40 |
+
super().__init__(**kwargs)
|
| 41 |
+
self.mlp_g_intermediate_size = mlp_g_intermediate_size
|
| 42 |
+
self.mlp_g_init_noise_std = float(mlp_g_init_noise_std)
|
| 43 |
+
self.use_moe = bool(use_moe)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class UnifiedMoTConfig(HunYuanVLMoTConfig):
|
| 47 |
+
"""Top-level config for the unified flow-matching model.
|
| 48 |
+
|
| 49 |
+
Inherits HunYuanVLMoTConfig (text_config + vision_config + token ids) and
|
| 50 |
+
layers on Flow-Matching specific fields.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
model_type = "unified_mot"
|
| 54 |
+
sub_configs = {
|
| 55 |
+
"vision_config": HunYuanVLMoTVisionConfig,
|
| 56 |
+
"text_config": UnifiedMoTTextConfig,
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
text_config=None,
|
| 62 |
+
vision_config=None,
|
| 63 |
+
# Flow Matching geometry
|
| 64 |
+
latent_patch_size: int = 2,
|
| 65 |
+
max_latent_size: int = 32,
|
| 66 |
+
vae_image_downsample: int = 16,
|
| 67 |
+
vae_z_channels: int = 16,
|
| 68 |
+
timestep_shift: float = 1.0,
|
| 69 |
+
# Loss aggregation
|
| 70 |
+
boundary_loss_weight: float = 0.0,
|
| 71 |
+
flow_loss_weight: float = 1.0,
|
| 72 |
+
text_loss_weight: float = 1.0,
|
| 73 |
+
# Special token used as the FM latent placeholder in the input sequence.
|
| 74 |
+
# Defaults to upstream's `latent_token_id` (120690) so the same checkpoint
|
| 75 |
+
# works without changes.
|
| 76 |
+
flow_latent_placeholder_id: Optional[int] = None,
|
| 77 |
+
**kwargs,
|
| 78 |
+
):
|
| 79 |
+
# If text_config wasn't pre-built, route the **kwargs through so caller-level
|
| 80 |
+
# fields like mlp_g_intermediate_size still land on text_config.
|
| 81 |
+
if text_config is None:
|
| 82 |
+
text_config = UnifiedMoTTextConfig(**kwargs)
|
| 83 |
+
elif isinstance(text_config, dict):
|
| 84 |
+
text_config = UnifiedMoTTextConfig(**text_config)
|
| 85 |
+
|
| 86 |
+
super().__init__(text_config=text_config, vision_config=vision_config, **kwargs)
|
| 87 |
+
|
| 88 |
+
self.latent_patch_size = int(latent_patch_size)
|
| 89 |
+
self.max_latent_size = int(max_latent_size)
|
| 90 |
+
self.vae_image_downsample = int(vae_image_downsample)
|
| 91 |
+
self.vae_z_channels = int(vae_z_channels)
|
| 92 |
+
self.timestep_shift = float(timestep_shift)
|
| 93 |
+
self.boundary_loss_weight = float(boundary_loss_weight)
|
| 94 |
+
self.flow_loss_weight = float(flow_loss_weight)
|
| 95 |
+
self.text_loss_weight = float(text_loss_weight)
|
| 96 |
+
self.flow_latent_placeholder_id = (
|
| 97 |
+
int(flow_latent_placeholder_id)
|
| 98 |
+
if flow_latent_placeholder_id is not None
|
| 99 |
+
else int(self.latent_token_id)
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
__all__ = ["UnifiedMoTConfig", "UnifiedMoTTextConfig"]
|
model/flow_matching_modules.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Flow Matching helper modules for UnifiedMoT.
|
| 2 |
+
|
| 3 |
+
Provides:
|
| 4 |
+
- TimestepEmbedder, PositionEmbedding (re-exported from model_utils)
|
| 5 |
+
- patchify_latent / unpatchify_latent — reshape between (C, H_lat, W_lat)
|
| 6 |
+
and (h*w, p*p*C) packed-patch token form
|
| 7 |
+
- sample_timesteps — sigmoid(N(0,1)) → [0,1] with timestep_shift applied
|
| 8 |
+
- build_noisy_latent — linear interp x_t = (1-t)*x_0 + t*noise
|
| 9 |
+
- patch_boundary_loss — L1 across patch boundaries on predicted x_0
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from typing import List, Tuple
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
# Re-exported from model_utils for a stable import path (see __all__ below).
|
| 18 |
+
from .model_utils import ( # noqa: F401 # pylint: disable=unused-import
|
| 19 |
+
TimestepEmbedder,
|
| 20 |
+
PositionEmbedding,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
# Patchify / Unpatchify
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
|
| 28 |
+
def patchify_latent(latent: torch.Tensor, h: int, w: int, patch_size: int, channels: int) -> torch.Tensor:
|
| 29 |
+
"""(C, H_lat, W_lat) → (h*w, p*p*C) where H_lat=h*p, W_lat=w*p."""
|
| 30 |
+
p = patch_size
|
| 31 |
+
latent = latent[:, : h * p, : w * p].reshape(channels, h, p, w, p)
|
| 32 |
+
latent = torch.einsum("chpwq->hwpqc", latent).reshape(-1, p * p * channels)
|
| 33 |
+
return latent
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def unpatchify_latent(packed: torch.Tensor, h: int, w: int, patch_size: int, channels: int) -> torch.Tensor:
|
| 37 |
+
"""(h*w, p*p*C) → (C, H_lat, W_lat). Inverse of patchify_latent."""
|
| 38 |
+
p = patch_size
|
| 39 |
+
x = packed.reshape(h, w, p, p, channels)
|
| 40 |
+
x = torch.einsum("hwpqc->chpwq", x).reshape(channels, h * p, w * p)
|
| 41 |
+
return x
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def pack_latents(
|
| 45 |
+
padded_latents: List[torch.Tensor],
|
| 46 |
+
shapes: List[Tuple[int, int]],
|
| 47 |
+
patch_size: int,
|
| 48 |
+
channels: int,
|
| 49 |
+
) -> torch.Tensor:
|
| 50 |
+
"""Concatenate per-image patchified latents into one (sum_h*w, p*p*C) tensor."""
|
| 51 |
+
parts = []
|
| 52 |
+
for latent, (h, w) in zip(padded_latents, shapes):
|
| 53 |
+
parts.append(patchify_latent(latent, h, w, patch_size, channels))
|
| 54 |
+
return torch.cat(parts, dim=0)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# Timestep sampling
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
|
| 61 |
+
def sample_timesteps(
|
| 62 |
+
shapes: List[Tuple[int, int]],
|
| 63 |
+
timestep_shift: float,
|
| 64 |
+
device,
|
| 65 |
+
dtype,
|
| 66 |
+
) -> torch.Tensor:
|
| 67 |
+
"""Per-image t ~ sigmoid(N(0,1)) broadcast over each image's tokens, then shifted.
|
| 68 |
+
|
| 69 |
+
Returns a flat tensor of length sum(h*w), one t per token.
|
| 70 |
+
"""
|
| 71 |
+
raw_t = torch.randn(len(shapes), device=device, dtype=dtype)
|
| 72 |
+
pieces = []
|
| 73 |
+
for (h, w), t in zip(shapes, raw_t):
|
| 74 |
+
pieces.append(torch.sigmoid(t).expand(h * w).clone())
|
| 75 |
+
timesteps = torch.cat(pieces, dim=0)
|
| 76 |
+
timesteps = timestep_shift * timesteps / (1.0 + (timestep_shift - 1.0) * timesteps)
|
| 77 |
+
return timesteps
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def build_noisy_latent(
|
| 81 |
+
clean: torch.Tensor,
|
| 82 |
+
noise: torch.Tensor,
|
| 83 |
+
timesteps: torch.Tensor,
|
| 84 |
+
) -> torch.Tensor:
|
| 85 |
+
"""x_t = (1 - t) * x_0 + t * noise, t broadcast over feature dim."""
|
| 86 |
+
return (1.0 - timesteps[:, None]) * clean + timesteps[:, None] * noise
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ---------------------------------------------------------------------------
|
| 90 |
+
# Boundary loss
|
| 91 |
+
# ---------------------------------------------------------------------------
|
| 92 |
+
|
| 93 |
+
def patch_boundary_loss(
|
| 94 |
+
x0_pred: torch.Tensor,
|
| 95 |
+
shapes: List[Tuple[int, int]],
|
| 96 |
+
patch_size: int,
|
| 97 |
+
channels: int,
|
| 98 |
+
) -> torch.Tensor:
|
| 99 |
+
"""L1 across horizontal/vertical patch boundaries on predicted x_0.
|
| 100 |
+
|
| 101 |
+
x0_pred: (sum_h*w, p*p*C). For each image's hxw grid, compares the right
|
| 102 |
+
column of patch (i,j) to the left column of patch (i,j+1) and similarly
|
| 103 |
+
for vertical neighbours. Encourages spatial smoothness in the latent
|
| 104 |
+
that the VAE decoder will turn into pixels.
|
| 105 |
+
"""
|
| 106 |
+
p = patch_size
|
| 107 |
+
losses = []
|
| 108 |
+
offset = 0
|
| 109 |
+
for (h, w) in shapes:
|
| 110 |
+
n = h * w
|
| 111 |
+
x = x0_pred[offset : offset + n].reshape(h, w, p, p, channels)
|
| 112 |
+
if w > 1:
|
| 113 |
+
right_edge = x[:, :-1, :, p - 1, :] # (h, w-1, p, C)
|
| 114 |
+
left_edge = x[:, 1:, :, 0, :] # (h, w-1, p, C)
|
| 115 |
+
losses.append((right_edge - left_edge).abs().mean())
|
| 116 |
+
if h > 1:
|
| 117 |
+
bottom_edge = x[:-1, :, p - 1, :, :] # (h-1, w, p, C)
|
| 118 |
+
top_edge = x[1:, :, 0, :, :] # (h-1, w, p, C)
|
| 119 |
+
losses.append((bottom_edge - top_edge).abs().mean())
|
| 120 |
+
offset += n
|
| 121 |
+
if not losses:
|
| 122 |
+
return x0_pred.sum() * 0
|
| 123 |
+
return torch.stack(losses).mean()
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
__all__ = [
|
| 127 |
+
"TimestepEmbedder",
|
| 128 |
+
"PositionEmbedding",
|
| 129 |
+
"patchify_latent",
|
| 130 |
+
"unpatchify_latent",
|
| 131 |
+
"pack_latents",
|
| 132 |
+
"sample_timesteps",
|
| 133 |
+
"build_noisy_latent",
|
| 134 |
+
"patch_boundary_loss",
|
| 135 |
+
]
|
model/model_utils.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
#
|
| 3 |
+
# Independent reimplementation of standard sinusoidal position- and
|
| 4 |
+
# timestep-embedding utilities, written from their public mathematical
|
| 5 |
+
# definitions (see the per-symbol references below). This file is NOT derived
|
| 6 |
+
# from the DiT source tree and carries no third-party (CC BY-NC) copyright; it
|
| 7 |
+
# is released under Apache-2.0 like the rest of this package.
|
| 8 |
+
#
|
| 9 |
+
# References for the underlying formulas (math only, no code reuse):
|
| 10 |
+
# * Sinusoidal position encoding — Vaswani et al., "Attention Is All You
|
| 11 |
+
# Need" (2017), Section 3.5.
|
| 12 |
+
# * Sinusoidal timestep embedding — Ho et al., "Denoising Diffusion
|
| 13 |
+
# Probabilistic Models" (2020); the same closed form is reused across
|
| 14 |
+
# diffusion and flow-matching models.
|
| 15 |
+
#
|
| 16 |
+
# The module layout, parameter names, and numerical outputs are intentionally
|
| 17 |
+
# identical to the previous version so that existing checkpoints load unchanged
|
| 18 |
+
# and inference results are bit-for-bit reproducible.
|
| 19 |
+
|
| 20 |
+
import math
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
from torch import nn
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# --------------------------------------------------------------------------- #
|
| 28 |
+
# Sinusoidal (sin-cos) position embeddings
|
| 29 |
+
#
|
| 30 |
+
# For a position p and channel index i the embedding interleaves
|
| 31 |
+
# sin(p / 10000^(2i/d)) and cos(p / 10000^(2i/d)),
|
| 32 |
+
# i.e. the classic Transformer positional encoding. The 2D variant encodes the
|
| 33 |
+
# height and width axes with half the channels each and concatenates them.
|
| 34 |
+
# --------------------------------------------------------------------------- #
|
| 35 |
+
def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
|
| 36 |
+
"""1D sin-cos embedding for a flat array of positions.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
embed_dim: even output dimension per position.
|
| 40 |
+
pos: array of positions, any shape; flattened to (M,).
|
| 41 |
+
Returns:
|
| 42 |
+
(M, embed_dim) array, [sin | cos] halves concatenated.
|
| 43 |
+
"""
|
| 44 |
+
assert embed_dim % 2 == 0
|
| 45 |
+
# Inverse frequencies 1 / 10000^(k/(embed_dim/2)) for k = 0 .. embed_dim/2-1.
|
| 46 |
+
inv_freq = np.arange(embed_dim // 2, dtype=np.float64)
|
| 47 |
+
inv_freq /= embed_dim / 2.0
|
| 48 |
+
inv_freq = 1.0 / 10000**inv_freq # (embed_dim/2,)
|
| 49 |
+
|
| 50 |
+
angles = np.einsum("m,d->md", pos.reshape(-1), inv_freq) # outer product (M, D/2)
|
| 51 |
+
return np.concatenate([np.sin(angles), np.cos(angles)], axis=1) # (M, D)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
|
| 55 |
+
"""2D sin-cos embedding: half the channels encode each spatial axis."""
|
| 56 |
+
assert embed_dim % 2 == 0
|
| 57 |
+
emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
|
| 58 |
+
emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
|
| 59 |
+
return np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0):
|
| 63 |
+
"""Sin-cos table for a ``grid_size x grid_size`` patch grid.
|
| 64 |
+
|
| 65 |
+
Returns an ``(grid_size**2 [+ extra_tokens], embed_dim)`` array. When
|
| 66 |
+
``cls_token`` is set and ``extra_tokens > 0``, that many zero rows are
|
| 67 |
+
prepended.
|
| 68 |
+
"""
|
| 69 |
+
axis_h = np.arange(grid_size, dtype=np.float32)
|
| 70 |
+
axis_w = np.arange(grid_size, dtype=np.float32)
|
| 71 |
+
grid = np.meshgrid(axis_w, axis_h) # width varies fastest
|
| 72 |
+
grid = np.stack(grid, axis=0).reshape([2, 1, grid_size, grid_size])
|
| 73 |
+
|
| 74 |
+
pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
|
| 75 |
+
if cls_token and extra_tokens > 0:
|
| 76 |
+
pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0)
|
| 77 |
+
return pos_embed
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# --------------------------------------------------------------------------- #
|
| 81 |
+
# Timestep embedding
|
| 82 |
+
# --------------------------------------------------------------------------- #
|
| 83 |
+
class TimestepEmbedder(nn.Module):
|
| 84 |
+
"""Embed scalar (possibly fractional) timesteps into vectors.
|
| 85 |
+
|
| 86 |
+
Sinusoidal frequency features are fed through a two-layer MLP with a SiLU
|
| 87 |
+
non-linearity. This is the standard timestep conditioning used by diffusion
|
| 88 |
+
and flow-matching models.
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
def __init__(self, hidden_size, frequency_embedding_size=256):
|
| 92 |
+
super().__init__()
|
| 93 |
+
self.mlp = nn.Sequential(
|
| 94 |
+
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
|
| 95 |
+
nn.SiLU(),
|
| 96 |
+
nn.Linear(hidden_size, hidden_size, bias=True),
|
| 97 |
+
)
|
| 98 |
+
self.frequency_embedding_size = frequency_embedding_size
|
| 99 |
+
|
| 100 |
+
@staticmethod
|
| 101 |
+
def timestep_embedding(t, dim, max_period=10000):
|
| 102 |
+
"""Sinusoidal features for a 1D tensor of (possibly fractional) timesteps.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
t: (N,) tensor of timestep values.
|
| 106 |
+
dim: output feature dimension.
|
| 107 |
+
max_period: lowest angular frequency (longest period).
|
| 108 |
+
Returns:
|
| 109 |
+
(N, dim) tensor of [cos | sin] features (zero-padded if ``dim`` is odd).
|
| 110 |
+
"""
|
| 111 |
+
half = dim // 2
|
| 112 |
+
# Geometrically spaced frequencies over [1, 1/max_period].
|
| 113 |
+
freqs = torch.exp(
|
| 114 |
+
-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
|
| 115 |
+
).to(device=t.device)
|
| 116 |
+
args = t[:, None].float() * freqs[None]
|
| 117 |
+
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
| 118 |
+
if dim % 2:
|
| 119 |
+
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
| 120 |
+
return embedding
|
| 121 |
+
|
| 122 |
+
def forward(self, t):
|
| 123 |
+
freq_features = self.timestep_embedding(t, self.frequency_embedding_size)
|
| 124 |
+
freq_features = freq_features.to(next(self.mlp.parameters()).dtype)
|
| 125 |
+
return self.mlp(freq_features)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class PositionEmbedding(nn.Module):
|
| 129 |
+
"""Fixed 2D sin-cos position table exposed as a lookup by position id."""
|
| 130 |
+
|
| 131 |
+
def __init__(self, max_num_patch_per_side, hidden_size):
|
| 132 |
+
super().__init__()
|
| 133 |
+
self.max_num_patch_per_side = max_num_patch_per_side
|
| 134 |
+
self.hidden_size = hidden_size
|
| 135 |
+
# Build the (non-trainable) table eagerly. If it were created lazily it
|
| 136 |
+
# could stay on the meta device through transformers'
|
| 137 |
+
# from_pretrained(dtype=...) path and later materialize as uninitialized
|
| 138 |
+
# memory when moved to the GPU.
|
| 139 |
+
table = get_2d_sincos_pos_embed(hidden_size, max_num_patch_per_side)
|
| 140 |
+
self.pos_embed = nn.Parameter(
|
| 141 |
+
torch.from_numpy(table).float(),
|
| 142 |
+
requires_grad=False,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
def _reset_parameters(self):
|
| 146 |
+
"""Recompute the table after a meta-init path (call post-from_pretrained)."""
|
| 147 |
+
table = get_2d_sincos_pos_embed(self.hidden_size, self.max_num_patch_per_side)
|
| 148 |
+
on_meta = self.pos_embed.is_meta or self.pos_embed.device.type == "meta"
|
| 149 |
+
materialized = torch.from_numpy(table).to(
|
| 150 |
+
device="cpu" if on_meta else self.pos_embed.device,
|
| 151 |
+
dtype=torch.float32 if on_meta else self.pos_embed.dtype,
|
| 152 |
+
)
|
| 153 |
+
if on_meta:
|
| 154 |
+
self.pos_embed = nn.Parameter(materialized.float(), requires_grad=False)
|
| 155 |
+
else:
|
| 156 |
+
self.pos_embed.data.copy_(materialized.to(self.pos_embed.dtype))
|
| 157 |
+
|
| 158 |
+
def forward(self, position_ids):
|
| 159 |
+
return self.pos_embed[position_ids]
|
model/modeling_decoder_mot.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MoT decoder layer with three-path routing (text / vision-input / vision-gen).
|
| 2 |
+
|
| 3 |
+
Subclasses `HunYuanVLMoTDecoderLayer` from upstream to add the third (`_g`)
|
| 4 |
+
path used by Flow Matching. Upstream provides `mlp / mlp_v` and matching
|
| 5 |
+
LayerNorms; we add `mlp_g / input_layernorm_g / post_attention_layernorm_g`.
|
| 6 |
+
|
| 7 |
+
`modality_mask` semantics:
|
| 8 |
+
* 0 = text token → mlp / input_layernorm / post_attention_layernorm
|
| 9 |
+
* 1 = input vision token → mlp_v / *_v
|
| 10 |
+
* 2 = generated vision token (FM latent) → mlp_g / *_g
|
| 11 |
+
|
| 12 |
+
Attention QKVO is **not** extended to a third path — generation tokens reuse
|
| 13 |
+
the `_v` projection (matches the existing HY-Unified design where
|
| 14 |
+
`q_proj_v / k_proj_v / v_proj_v / o_proj_v` cover both input and generated
|
| 15 |
+
vision tokens). We just need to convert the int modality mask to a bool
|
| 16 |
+
"vision" mask before calling `self.self_attn`.
|
| 17 |
+
|
| 18 |
+
mlp_g may optionally have a wider intermediate (Net2Wider scale-up). When
|
| 19 |
+
`config.mlp_g_intermediate_size` is set and larger than `intermediate_size`,
|
| 20 |
+
we build mlp_g with a cloned config that overrides `intermediate_size`.
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import copy
|
| 25 |
+
from typing import Optional
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from transformers.cache_utils import Cache
|
| 30 |
+
from transformers.processing_utils import Unpack
|
| 31 |
+
from transformers.utils import TransformersKwargs
|
| 32 |
+
from transformers.utils.deprecation import deprecate_kwarg
|
| 33 |
+
|
| 34 |
+
from transformers.models.hunyuan_vl_mot.modeling_hunyuan_vl_mot import (
|
| 35 |
+
HunYuanVLMoTDecoderLayer,
|
| 36 |
+
HunYuanVLMoTMLP,
|
| 37 |
+
HunYuanVLMoTRMSNorm,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Side-effect import: replaces upstream's _flash_attention_forward_mot
|
| 41 |
+
from . import attention_mot_packed # noqa: F401 # pylint: disable=unused-import
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
# Three-way mask_apply
|
| 46 |
+
# ---------------------------------------------------------------------------
|
| 47 |
+
|
| 48 |
+
def mask_apply_3way(
|
| 49 |
+
hidden_states: torch.Tensor,
|
| 50 |
+
modality_mask: Optional[torch.Tensor],
|
| 51 |
+
text_funcs,
|
| 52 |
+
vision_funcs,
|
| 53 |
+
gen_funcs,
|
| 54 |
+
out_dims=None,
|
| 55 |
+
padding_mask: Optional[torch.Tensor] = None,
|
| 56 |
+
):
|
| 57 |
+
"""Routes tokens to text/vision/generation function lists by modality_mask.
|
| 58 |
+
|
| 59 |
+
hidden_states: (B, S, D)
|
| 60 |
+
modality_mask: (B, S) int tensor with values in {0, 1, 2}, or None
|
| 61 |
+
0 = text, 1 = vision input, 2 = vision generation
|
| 62 |
+
padding_mask: (B, S) int tensor, 1 = valid token, 0 = padding
|
| 63 |
+
|
| 64 |
+
Returns a list of stacked (B, S, out_d) tensors — one per `text_funcs[i]`.
|
| 65 |
+
"""
|
| 66 |
+
if modality_mask is None:
|
| 67 |
+
# All-text: skip routing entirely
|
| 68 |
+
return [text_funcs[i](hidden_states) for i in range(len(text_funcs))]
|
| 69 |
+
|
| 70 |
+
bsz, seq_len, hidden_dim = hidden_states.size()
|
| 71 |
+
flat = hidden_states.reshape(bsz * seq_len, hidden_dim)
|
| 72 |
+
mask_flat = modality_mask.reshape(bsz * seq_len)
|
| 73 |
+
if padding_mask is not None:
|
| 74 |
+
valid_flat = padding_mask.reshape(bsz * seq_len).bool()
|
| 75 |
+
else:
|
| 76 |
+
valid_flat = None
|
| 77 |
+
|
| 78 |
+
placeholder = hidden_states[0:1, 0:1, :] # (1, 1, D)
|
| 79 |
+
zero_feature = 0
|
| 80 |
+
|
| 81 |
+
num_outputs = len(text_funcs)
|
| 82 |
+
if out_dims is None:
|
| 83 |
+
out_dims_resolved = [hidden_dim] * num_outputs
|
| 84 |
+
else:
|
| 85 |
+
out_dims_resolved = list(out_dims)
|
| 86 |
+
|
| 87 |
+
# Pre-allocate output buffers (empty, not zeros — we overwrite all valid
|
| 88 |
+
# positions and the rest are masked out by the caller / padding).
|
| 89 |
+
out_flat = [
|
| 90 |
+
torch.empty(bsz * seq_len, od, device=flat.device, dtype=flat.dtype)
|
| 91 |
+
for od in out_dims_resolved
|
| 92 |
+
]
|
| 93 |
+
# Padding positions need to be zeroed — we won't touch them in the
|
| 94 |
+
# gather/scatter below. Cheaper than zeroing the whole tensor: only
|
| 95 |
+
# zero the rows that won't be hit by any of the three modalities.
|
| 96 |
+
if valid_flat is not None:
|
| 97 |
+
invalid_flat = ~valid_flat
|
| 98 |
+
if invalid_flat.any():
|
| 99 |
+
for buf in out_flat:
|
| 100 |
+
buf[invalid_flat] = 0
|
| 101 |
+
# else: all rows will be hit by exactly one of {text, vision, gen}.
|
| 102 |
+
|
| 103 |
+
def _dispatch(idx_mask, funcs):
|
| 104 |
+
"""Run `funcs` on rows selected by `idx_mask`, scatter back. If no
|
| 105 |
+
rows are selected, multiply through a placeholder so the params still
|
| 106 |
+
receive grad (avoids "unused parameter" DDP errors)."""
|
| 107 |
+
nonlocal zero_feature
|
| 108 |
+
if idx_mask.any():
|
| 109 |
+
hs_sel = flat[idx_mask]
|
| 110 |
+
for i, fn in enumerate(funcs):
|
| 111 |
+
out_flat[i][idx_mask] = fn(hs_sel)
|
| 112 |
+
else:
|
| 113 |
+
for fn in funcs:
|
| 114 |
+
zero_feature = zero_feature + fn(placeholder).mean() * 0
|
| 115 |
+
|
| 116 |
+
# Text: mask == 0
|
| 117 |
+
text_idx = (mask_flat == 0)
|
| 118 |
+
if valid_flat is not None:
|
| 119 |
+
text_idx = text_idx & valid_flat
|
| 120 |
+
_dispatch(text_idx, text_funcs)
|
| 121 |
+
|
| 122 |
+
# Vision input: mask == 1
|
| 123 |
+
vis_idx = (mask_flat == 1)
|
| 124 |
+
if valid_flat is not None:
|
| 125 |
+
vis_idx = vis_idx & valid_flat
|
| 126 |
+
_dispatch(vis_idx, vision_funcs)
|
| 127 |
+
|
| 128 |
+
# Generation: mask == 2
|
| 129 |
+
gen_idx = (mask_flat == 2)
|
| 130 |
+
if valid_flat is not None:
|
| 131 |
+
gen_idx = gen_idx & valid_flat
|
| 132 |
+
_dispatch(gen_idx, gen_funcs)
|
| 133 |
+
|
| 134 |
+
result = [out.view(bsz, seq_len, -1) for out in out_flat]
|
| 135 |
+
result[0] = result[0] + zero_feature
|
| 136 |
+
return result
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# ---------------------------------------------------------------------------
|
| 140 |
+
# Decoder layer subclass
|
| 141 |
+
# ---------------------------------------------------------------------------
|
| 142 |
+
|
| 143 |
+
def _make_g_config(config, mlp_g_intermediate_size: int):
|
| 144 |
+
"""Clone config with a wider intermediate_size for mlp_g (Net2Wider).
|
| 145 |
+
|
| 146 |
+
MUST be `copy.deepcopy`, NOT `copy.copy`. Reason:
|
| 147 |
+
HunYuanVLMoTConfig.__setattr__ is a proxy that re-routes any attribute
|
| 148 |
+
writes (when the key is in text_config.__dict__) into self.text_config.
|
| 149 |
+
Shallow-copying the outer config keeps `g_cfg.text_config is
|
| 150 |
+
config.text_config` — same object — so the subsequent
|
| 151 |
+
`g_cfg.intermediate_size = ...` setattr ends up mutating the SHARED
|
| 152 |
+
text_config used by every other decoder layer's mlp/mlp_v construction.
|
| 153 |
+
Layer 0 escapes (its mlp/mlp_v are built before _make_g_config runs),
|
| 154 |
+
but layer 1+ then sees text_config.intermediate_size = 12288 and builds
|
| 155 |
+
mlp/mlp_v at the wider size — at that point ckpt weights (which are 6144
|
| 156 |
+
for mlp/mlp_v) no longer match and `from_pretrained` fails with size
|
| 157 |
+
mismatch. Deepcopy the whole config tree to break this aliasing.
|
| 158 |
+
"""
|
| 159 |
+
g_cfg = copy.deepcopy(config)
|
| 160 |
+
g_cfg.intermediate_size = int(mlp_g_intermediate_size)
|
| 161 |
+
return g_cfg
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
class MoTDecoderLayer(HunYuanVLMoTDecoderLayer):
|
| 165 |
+
"""Three-path decoder layer: text / vision-input / vision-gen.
|
| 166 |
+
|
| 167 |
+
Adds `mlp_g`, `input_layernorm_g`, `post_attention_layernorm_g` on top of
|
| 168 |
+
upstream's two-path layer. `modality_mask` is interpreted as int{0,1,2}
|
| 169 |
+
instead of bool.
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
def __init__(self, config, layer_idx: int):
|
| 173 |
+
super().__init__(config, layer_idx)
|
| 174 |
+
|
| 175 |
+
# Text-config-aware lookup (HunYuanVLMoTConfig proxies these to text_config)
|
| 176 |
+
intermediate_size = getattr(config, "intermediate_size", None)
|
| 177 |
+
mlp_g_inter = getattr(config, "mlp_g_intermediate_size", None)
|
| 178 |
+
|
| 179 |
+
if mlp_g_inter is None or intermediate_size is None or mlp_g_inter == intermediate_size:
|
| 180 |
+
g_cfg = config
|
| 181 |
+
else:
|
| 182 |
+
assert mlp_g_inter >= intermediate_size, (
|
| 183 |
+
f"mlp_g_intermediate_size ({mlp_g_inter}) must be >= "
|
| 184 |
+
f"intermediate_size ({intermediate_size}); shrinking is not supported."
|
| 185 |
+
)
|
| 186 |
+
g_cfg = _make_g_config(config, mlp_g_inter)
|
| 187 |
+
|
| 188 |
+
self.mlp_g = HunYuanVLMoTMLP(g_cfg)
|
| 189 |
+
self.input_layernorm_g = HunYuanVLMoTRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 190 |
+
self.post_attention_layernorm_g = HunYuanVLMoTRMSNorm(
|
| 191 |
+
config.hidden_size, eps=config.rms_norm_eps
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
@deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
|
| 195 |
+
def forward(
|
| 196 |
+
self,
|
| 197 |
+
hidden_states: torch.Tensor,
|
| 198 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 199 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 200 |
+
past_key_values: Optional[Cache] = None,
|
| 201 |
+
use_cache: Optional[bool] = False,
|
| 202 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 203 |
+
position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
|
| 204 |
+
modality_mask: Optional[torch.Tensor] = None,
|
| 205 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 206 |
+
) -> torch.Tensor:
|
| 207 |
+
# Pull padding_mask from attention_mask dict for GEMM exclusion
|
| 208 |
+
padding_mask = None
|
| 209 |
+
if isinstance(attention_mask, dict):
|
| 210 |
+
pm = attention_mask.get("padding_mask", None)
|
| 211 |
+
if pm is not None and hidden_states.shape[1] == pm.shape[1]:
|
| 212 |
+
padding_mask = pm
|
| 213 |
+
|
| 214 |
+
# Convert int modality_mask to bool for attention QKVO routing
|
| 215 |
+
# (attention has only 2 paths: text vs vision; gen tokens go through _v)
|
| 216 |
+
attn_modality_mask = None
|
| 217 |
+
if modality_mask is not None:
|
| 218 |
+
attn_modality_mask = (modality_mask > 0)
|
| 219 |
+
|
| 220 |
+
residual = hidden_states
|
| 221 |
+
|
| 222 |
+
# Pre-attention LayerNorm — three paths
|
| 223 |
+
hidden_states = mask_apply_3way(
|
| 224 |
+
hidden_states, modality_mask,
|
| 225 |
+
[self.input_layernorm],
|
| 226 |
+
[self.input_layernorm_v],
|
| 227 |
+
[self.input_layernorm_g],
|
| 228 |
+
padding_mask=padding_mask,
|
| 229 |
+
)[0]
|
| 230 |
+
|
| 231 |
+
# Self-attention (upstream's two-path attention with bool mask)
|
| 232 |
+
hidden_states, _ = self.self_attn(
|
| 233 |
+
hidden_states=hidden_states,
|
| 234 |
+
attention_mask=attention_mask,
|
| 235 |
+
position_ids=position_ids,
|
| 236 |
+
past_key_values=past_key_values,
|
| 237 |
+
use_cache=use_cache,
|
| 238 |
+
cache_position=cache_position,
|
| 239 |
+
position_embeddings=position_embeddings,
|
| 240 |
+
modality_mask=attn_modality_mask,
|
| 241 |
+
**kwargs,
|
| 242 |
+
)
|
| 243 |
+
hidden_states = residual + hidden_states
|
| 244 |
+
|
| 245 |
+
# MLP — three paths (LayerNorm + MLP fused per path to match upstream layout)
|
| 246 |
+
residual = hidden_states
|
| 247 |
+
hidden_states = mask_apply_3way(
|
| 248 |
+
hidden_states, modality_mask,
|
| 249 |
+
[lambda x: self.mlp(self.post_attention_layernorm(x))],
|
| 250 |
+
[lambda x: self.mlp_v(self.post_attention_layernorm_v(x))],
|
| 251 |
+
[lambda x: self.mlp_g(self.post_attention_layernorm_g(x))],
|
| 252 |
+
padding_mask=padding_mask,
|
| 253 |
+
)[0]
|
| 254 |
+
hidden_states = residual + hidden_states
|
| 255 |
+
|
| 256 |
+
# Zero out padding positions to keep residuals clean (matches upstream behavior)
|
| 257 |
+
if padding_mask is not None and hidden_states.shape[1] == padding_mask.shape[1]:
|
| 258 |
+
hidden_states = hidden_states * padding_mask.unsqueeze(-1)
|
| 259 |
+
|
| 260 |
+
return hidden_states
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
__all__ = ["MoTDecoderLayer", "mask_apply_3way"]
|
model/modeling_text_model_mot.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inner text-decoder subclass that threads packed kwargs into the causal_mask dict.
|
| 2 |
+
|
| 3 |
+
The upstream `_HunYuanVLMoTTextModel.forward` builds:
|
| 4 |
+
|
| 5 |
+
causal_mask = {"v_seqlens": visual_segs, "padding_mask": attention_mask}
|
| 6 |
+
|
| 7 |
+
and passes it to each decoder layer. For P-Pack training we additionally need
|
| 8 |
+
`cu_seqlens / sample_ids / g_seqlens / input_image_mask` in that dict so our
|
| 9 |
+
patched flash-attention function can dispatch to the packed code path.
|
| 10 |
+
|
| 11 |
+
Rather than monkey-patching upstream's forward, we subclass `_HunYuanVLMoTTextModel`
|
| 12 |
+
and `_HunYuanVLMoTTextForCausalLM` cleanly and replace the decoder layers with
|
| 13 |
+
`MoTDecoderLayer` (three-path mlp_t / mlp_v / mlp_g) inside __init__.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from typing import Optional, Union
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
from transformers.cache_utils import Cache, DynamicCache
|
| 22 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
|
| 23 |
+
from transformers.processing_utils import Unpack
|
| 24 |
+
from transformers.utils import TransformersKwargs
|
| 25 |
+
|
| 26 |
+
from transformers.models.hunyuan_vl_mot.modeling_hunyuan_vl_mot import (
|
| 27 |
+
_HunYuanVLMoTTextModel,
|
| 28 |
+
_HunYuanVLMoTTextForCausalLM,
|
| 29 |
+
_modality_mask_to_segments,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
from .modeling_decoder_mot import MoTDecoderLayer
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class MoTTextModel(_HunYuanVLMoTTextModel):
|
| 36 |
+
"""Pure text decoder using MoTDecoderLayer (3-path) and packed kwargs.
|
| 37 |
+
|
| 38 |
+
Drop-in replacement for `_HunYuanVLMoTTextModel`:
|
| 39 |
+
* Replaces all decoder layers with `MoTDecoderLayer`
|
| 40 |
+
* Forward accepts `cu_seqlens / sample_ids / g_seqlens / input_image_mask`
|
| 41 |
+
and threads them into the `attention_mask` dict that decoder layers see
|
| 42 |
+
* `modality_mask` may be int{0,1,2} (text/vision-input/vision-gen);
|
| 43 |
+
v_seqlens are derived from `input_image_mask` if provided, else from
|
| 44 |
+
`modality_mask > 0`
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, config):
|
| 48 |
+
super().__init__(config)
|
| 49 |
+
# Replace decoder layers with MoT (3-path). Init weights match upstream
|
| 50 |
+
# for the shared keys; mlp_g/_g get default init (overwritten later by
|
| 51 |
+
# Net2Wider in mot_init_utils.maybe_init_generation_path).
|
| 52 |
+
self.layers = nn.ModuleList(
|
| 53 |
+
[MoTDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
def forward(
|
| 57 |
+
self,
|
| 58 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 59 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 60 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 61 |
+
past_key_values: Optional[Cache] = None,
|
| 62 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 63 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 64 |
+
use_cache: Optional[bool] = None,
|
| 65 |
+
modality_mask: Optional[torch.Tensor] = None,
|
| 66 |
+
# Packed-mode signals
|
| 67 |
+
cu_seqlens: Optional[torch.Tensor] = None,
|
| 68 |
+
sample_ids: Optional[torch.Tensor] = None,
|
| 69 |
+
g_seqlens: Optional[torch.Tensor] = None,
|
| 70 |
+
input_image_mask: Optional[torch.Tensor] = None,
|
| 71 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 72 |
+
) -> BaseModelOutputWithPast:
|
| 73 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 74 |
+
raise ValueError("Specify exactly one of input_ids or inputs_embeds")
|
| 75 |
+
|
| 76 |
+
if inputs_embeds is None:
|
| 77 |
+
inputs_embeds = self.embed_tokens(input_ids)
|
| 78 |
+
|
| 79 |
+
if use_cache and past_key_values is None:
|
| 80 |
+
past_key_values = DynamicCache(config=self.config)
|
| 81 |
+
|
| 82 |
+
if cache_position is None:
|
| 83 |
+
past_seen = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 84 |
+
cache_position = torch.arange(
|
| 85 |
+
past_seen, past_seen + inputs_embeds.shape[1], device=inputs_embeds.device
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Position IDs:
|
| 89 |
+
# * Packed mode (cu_seqlens given): caller must pass position_ids (per-sample arange)
|
| 90 |
+
# * Padded B>1: derive from attention_mask cumsum
|
| 91 |
+
# * B==1 / decode: cache_position
|
| 92 |
+
if position_ids is None:
|
| 93 |
+
if cu_seqlens is not None:
|
| 94 |
+
raise ValueError("Packed mode requires position_ids from the collator.")
|
| 95 |
+
if attention_mask is not None and attention_mask.shape[0] > 1:
|
| 96 |
+
position_ids = attention_mask.long().cumsum(dim=-1) - 1
|
| 97 |
+
position_ids = position_ids.clamp(min=0)
|
| 98 |
+
seq_len = inputs_embeds.shape[1]
|
| 99 |
+
if position_ids.shape[1] > seq_len:
|
| 100 |
+
position_ids = position_ids[:, -seq_len:]
|
| 101 |
+
else:
|
| 102 |
+
position_ids = cache_position.unsqueeze(0)
|
| 103 |
+
text_position_ids = position_ids
|
| 104 |
+
|
| 105 |
+
if modality_mask is None:
|
| 106 |
+
modality_mask = torch.zeros(
|
| 107 |
+
inputs_embeds.shape[:-1], dtype=torch.long, device=inputs_embeds.device
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# v_seqlens source: input_image_mask (input images only) when training
|
| 111 |
+
# flow generation; otherwise modality_mask > 0.
|
| 112 |
+
if input_image_mask is not None:
|
| 113 |
+
vis_mask = input_image_mask.bool()
|
| 114 |
+
else:
|
| 115 |
+
vis_mask = modality_mask > 0
|
| 116 |
+
visual_segs = _modality_mask_to_segments(vis_mask)
|
| 117 |
+
|
| 118 |
+
# Truncate modality_mask if shape mismatch (decode KV cache)
|
| 119 |
+
seq_len = inputs_embeds.shape[1]
|
| 120 |
+
if modality_mask.shape[1] > seq_len:
|
| 121 |
+
modality_mask = modality_mask[:, -seq_len:]
|
| 122 |
+
|
| 123 |
+
# Build extended causal_mask dict with packed signals.
|
| 124 |
+
# When cu_seqlens is set, padding_mask MUST be None (we treat as packed).
|
| 125 |
+
# PERF: pre-compute max_seqlen ONCE here so the per-layer attention
|
| 126 |
+
# forward doesn't redo `.max().item()` (a D2H sync) 32 times.
|
| 127 |
+
max_seqlen_packed = None
|
| 128 |
+
if cu_seqlens is not None:
|
| 129 |
+
with torch.no_grad():
|
| 130 |
+
max_seqlen_packed = int((cu_seqlens[1:] - cu_seqlens[:-1]).max().item())
|
| 131 |
+
|
| 132 |
+
causal_mask = {
|
| 133 |
+
"v_seqlens": visual_segs,
|
| 134 |
+
"g_seqlens": g_seqlens,
|
| 135 |
+
"cu_seqlens": cu_seqlens,
|
| 136 |
+
"sample_ids": sample_ids,
|
| 137 |
+
"padding_mask": attention_mask if cu_seqlens is None else None,
|
| 138 |
+
"max_seqlen_packed": max_seqlen_packed,
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
hidden_states = inputs_embeds
|
| 142 |
+
position_embeddings = self.rotary_emb(hidden_states, text_position_ids)
|
| 143 |
+
|
| 144 |
+
for decoder_layer in self.layers:
|
| 145 |
+
hidden_states = decoder_layer(
|
| 146 |
+
hidden_states,
|
| 147 |
+
attention_mask=causal_mask,
|
| 148 |
+
position_ids=text_position_ids,
|
| 149 |
+
past_key_values=past_key_values,
|
| 150 |
+
cache_position=cache_position,
|
| 151 |
+
position_embeddings=position_embeddings,
|
| 152 |
+
modality_mask=modality_mask,
|
| 153 |
+
**kwargs,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
hidden_states = self.norm(hidden_states)
|
| 157 |
+
return BaseModelOutputWithPast(
|
| 158 |
+
last_hidden_state=hidden_states,
|
| 159 |
+
past_key_values=past_key_values,
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class MoTTextForCausalLM(_HunYuanVLMoTTextForCausalLM):
|
| 164 |
+
"""Inner text + lm_head wrapper using MoTTextModel.
|
| 165 |
+
|
| 166 |
+
Threads packed kwargs and `shift_labels` through. When `shift_labels` is
|
| 167 |
+
provided, computes split text/image loss aggregates so the outer wrapper
|
| 168 |
+
can report them separately.
|
| 169 |
+
"""
|
| 170 |
+
|
| 171 |
+
def __init__(self, config):
|
| 172 |
+
super().__init__(config)
|
| 173 |
+
# Replace inner text model with MoT (3-path) version
|
| 174 |
+
self.model = MoTTextModel(config)
|
| 175 |
+
|
| 176 |
+
def forward(
|
| 177 |
+
self,
|
| 178 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 179 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 180 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 181 |
+
past_key_values: Optional[Cache] = None,
|
| 182 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 183 |
+
labels: Optional[torch.LongTensor] = None,
|
| 184 |
+
use_cache: Optional[bool] = None,
|
| 185 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 186 |
+
logits_to_keep: Union[int, torch.Tensor] = 0,
|
| 187 |
+
modality_mask: Optional[torch.Tensor] = None,
|
| 188 |
+
# Packed kwargs threaded to inner MoTTextModel
|
| 189 |
+
cu_seqlens: Optional[torch.Tensor] = None,
|
| 190 |
+
sample_ids: Optional[torch.Tensor] = None,
|
| 191 |
+
g_seqlens: Optional[torch.Tensor] = None,
|
| 192 |
+
input_image_mask: Optional[torch.Tensor] = None,
|
| 193 |
+
# Optional pre-shifted labels (used when image regions need -100 masking)
|
| 194 |
+
shift_labels: Optional[torch.Tensor] = None,
|
| 195 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 196 |
+
) -> CausalLMOutputWithPast:
|
| 197 |
+
outputs: BaseModelOutputWithPast = self.model(
|
| 198 |
+
input_ids=input_ids,
|
| 199 |
+
attention_mask=attention_mask,
|
| 200 |
+
position_ids=position_ids,
|
| 201 |
+
past_key_values=past_key_values,
|
| 202 |
+
inputs_embeds=inputs_embeds,
|
| 203 |
+
use_cache=use_cache,
|
| 204 |
+
cache_position=cache_position,
|
| 205 |
+
modality_mask=modality_mask,
|
| 206 |
+
cu_seqlens=cu_seqlens,
|
| 207 |
+
sample_ids=sample_ids,
|
| 208 |
+
g_seqlens=g_seqlens,
|
| 209 |
+
input_image_mask=input_image_mask,
|
| 210 |
+
**kwargs,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
hidden_states = outputs.last_hidden_state
|
| 214 |
+
|
| 215 |
+
# Choose label source: explicit shift_labels (collator-prepared, with
|
| 216 |
+
# image-region -100 masking) takes precedence over `labels`.
|
| 217 |
+
train_labels = shift_labels if shift_labels is not None else labels
|
| 218 |
+
|
| 219 |
+
if train_labels is not None:
|
| 220 |
+
flat_hs = hidden_states.reshape(-1, hidden_states.size(-1))
|
| 221 |
+
flat_labels = train_labels.reshape(-1)
|
| 222 |
+
valid = flat_labels >= 0
|
| 223 |
+
if valid.sum() == 0:
|
| 224 |
+
flat_hs_v = flat_hs[:1]
|
| 225 |
+
flat_labels_v = flat_labels[:1]
|
| 226 |
+
else:
|
| 227 |
+
flat_hs_v = flat_hs[valid]
|
| 228 |
+
flat_labels_v = flat_labels[valid]
|
| 229 |
+
logits = self.lm_head(flat_hs_v)
|
| 230 |
+
loss = self.loss_function(
|
| 231 |
+
logits=logits, labels=flat_labels_v, vocab_size=self.config.vocab_size, **kwargs
|
| 232 |
+
)
|
| 233 |
+
else:
|
| 234 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 235 |
+
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
| 236 |
+
loss = None
|
| 237 |
+
|
| 238 |
+
return CausalLMOutputWithPast(
|
| 239 |
+
loss=loss,
|
| 240 |
+
logits=logits,
|
| 241 |
+
past_key_values=outputs.past_key_values,
|
| 242 |
+
hidden_states=outputs.last_hidden_state,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
__all__ = ["MoTTextModel", "MoTTextForCausalLM"]
|
model/modeling_unified_mot.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""UnifiedMoT main model: HunYuanVLMoT base + Flow Matching for image generation.
|
| 2 |
+
|
| 3 |
+
Architecture:
|
| 4 |
+
* Inherits `transformers.models.hunyuan_vl_mot.HunYuanVLMoTModel` and uses
|
| 5 |
+
its HYViT2 vision tower via `self.visual`. The inner language model is
|
| 6 |
+
swapped to `MoTTextForCausalLM` so all decoder layers are 3-path
|
| 7 |
+
(mlp_t / mlp_v / mlp_g).
|
| 8 |
+
* Adds Flow Matching modules on the `ForConditionalGeneration` class:
|
| 9 |
+
`time_embedder`, `vae2llm`, `llm2vae`, `latent_pos_embed`.
|
| 10 |
+
* Forward supports both packed (P-Pack training) and padded modes.
|
| 11 |
+
* Computes split text-CE + flow-MSE + optional patch-boundary L1 loss
|
| 12 |
+
and exposes loss aggregates on the output (image_loss_sum / text_loss_sum /
|
| 13 |
+
image_loss_count / text_loss_count) for trainer-side weighting.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from dataclasses import dataclass
|
| 18 |
+
from typing import List, Optional, Tuple, Union
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn as nn
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
|
| 24 |
+
from transformers.cache_utils import Cache
|
| 25 |
+
from transformers.modeling_outputs import ModelOutput
|
| 26 |
+
|
| 27 |
+
from transformers.models.hunyuan_vl_mot.modeling_hunyuan_vl_mot import (
|
| 28 |
+
HY_VL_MOT_IMAGE_TOKEN_ID,
|
| 29 |
+
HY_VL_MOT_VIDEO_TOKEN_ID,
|
| 30 |
+
HunYuanVLMoTForConditionalGeneration,
|
| 31 |
+
HunYuanVLMoTModel,
|
| 32 |
+
HunYuanVLMoTPreTrainedModel,
|
| 33 |
+
HunYuanVLMoTModelOutputWithPast,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Side-effect imports: replace upstream attention + extend decoder/text-model
|
| 37 |
+
from . import attention_mot_packed # noqa: F401 # pylint: disable=unused-import
|
| 38 |
+
|
| 39 |
+
from .config_unified_mot import UnifiedMoTConfig
|
| 40 |
+
from .modeling_text_model_mot import MoTTextForCausalLM
|
| 41 |
+
from .flow_matching_modules import (
|
| 42 |
+
TimestepEmbedder,
|
| 43 |
+
PositionEmbedding,
|
| 44 |
+
pack_latents,
|
| 45 |
+
sample_timesteps,
|
| 46 |
+
build_noisy_latent,
|
| 47 |
+
patch_boundary_loss,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ---------------------------------------------------------------------------
|
| 52 |
+
# Output dataclass
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class UnifiedMoTOutput(ModelOutput):
|
| 57 |
+
"""Output of UnifiedMoTForConditionalGeneration.
|
| 58 |
+
|
| 59 |
+
Includes the standard CausalLMOutputWithPast fields plus split text/image
|
| 60 |
+
loss aggregates so the trainer can apply per-modality weights.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
loss: Optional[torch.FloatTensor] = None
|
| 64 |
+
logits: Optional[torch.FloatTensor] = None
|
| 65 |
+
past_key_values: Optional[Cache] = None
|
| 66 |
+
hidden_states: Optional[Tuple[torch.FloatTensor]] = None
|
| 67 |
+
# Split aggregates (sum-reduced; count of contributing tokens)
|
| 68 |
+
image_loss_sum: Optional[torch.FloatTensor] = None
|
| 69 |
+
image_loss_count: Optional[torch.LongTensor] = None
|
| 70 |
+
text_loss_sum: Optional[torch.FloatTensor] = None
|
| 71 |
+
text_loss_count: Optional[torch.LongTensor] = None
|
| 72 |
+
boundary_loss: Optional[torch.FloatTensor] = None
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
# UnifiedMoTModel — wraps HunYuanVLMoTModel, swaps language_model to MoT version
|
| 77 |
+
# ---------------------------------------------------------------------------
|
| 78 |
+
|
| 79 |
+
class UnifiedMoTModel(HunYuanVLMoTModel):
|
| 80 |
+
"""HunYuanVLMoT wrapper with 3-path MoT decoder layers.
|
| 81 |
+
|
| 82 |
+
`__init__` first calls upstream's super-__init__ to set up `self.visual`,
|
| 83 |
+
`self.language_model`, and the misc plumbing, then **rebuilds**
|
| 84 |
+
`self.language_model` as `MoTTextForCausalLM` so its decoder layers are
|
| 85 |
+
`MoTDecoderLayer` (with `mlp_g`).
|
| 86 |
+
|
| 87 |
+
Forward injects optional `flow_embeds` at `flow_positions` (absolute packed
|
| 88 |
+
coords) before delegating to language_model. All packed kwargs pass
|
| 89 |
+
straight through to the inner text model.
|
| 90 |
+
"""
|
| 91 |
+
config_class = UnifiedMoTConfig
|
| 92 |
+
|
| 93 |
+
def __init__(self, config: UnifiedMoTConfig):
|
| 94 |
+
super().__init__(config)
|
| 95 |
+
# Replace upstream's language_model with our MoT (3-path) version.
|
| 96 |
+
# Done via `_from_config` to honor PretrainedConfig conventions.
|
| 97 |
+
self.language_model = MoTTextForCausalLM._from_config(config)
|
| 98 |
+
|
| 99 |
+
# -----------------------------------------------------------------
|
| 100 |
+
# Forward
|
| 101 |
+
# -----------------------------------------------------------------
|
| 102 |
+
|
| 103 |
+
def forward(
|
| 104 |
+
self,
|
| 105 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 106 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 107 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 108 |
+
past_key_values: Optional[Cache] = None,
|
| 109 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 110 |
+
pixel_values: Optional[torch.Tensor] = None,
|
| 111 |
+
pixel_values_videos: Optional[torch.FloatTensor] = None,
|
| 112 |
+
image_grid_thw: Optional[torch.LongTensor] = None,
|
| 113 |
+
video_grid_thw: Optional[torch.LongTensor] = None,
|
| 114 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 115 |
+
logits_to_keep: Union[int, torch.Tensor] = 0,
|
| 116 |
+
labels: Optional[torch.LongTensor] = None,
|
| 117 |
+
# Packed-mode signals
|
| 118 |
+
cu_seqlens: Optional[torch.Tensor] = None,
|
| 119 |
+
sample_ids: Optional[torch.Tensor] = None,
|
| 120 |
+
modality_mask: Optional[torch.Tensor] = None,
|
| 121 |
+
input_image_mask: Optional[torch.Tensor] = None,
|
| 122 |
+
# Flow Matching
|
| 123 |
+
flow_embeds: Optional[torch.FloatTensor] = None, # (total_flow_tokens, D)
|
| 124 |
+
flow_positions: Optional[torch.Tensor] = None, # (M, 2) absolute packed coords
|
| 125 |
+
g_seqlens: Optional[torch.Tensor] = None, # (M, 2) absolute, used by attention
|
| 126 |
+
**kwargs,
|
| 127 |
+
) -> HunYuanVLMoTModelOutputWithPast:
|
| 128 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 129 |
+
raise ValueError("Specify exactly one of input_ids or inputs_embeds")
|
| 130 |
+
|
| 131 |
+
if inputs_embeds is None:
|
| 132 |
+
inputs_embeds = self.get_input_embeddings()(input_ids)
|
| 133 |
+
|
| 134 |
+
# ----- Vision: encode input images/videos and scatter into embeds -----
|
| 135 |
+
union_mask = None
|
| 136 |
+
has_input_media = (pixel_values is not None) or (pixel_values_videos is not None)
|
| 137 |
+
if self.training or has_input_media:
|
| 138 |
+
if has_input_media:
|
| 139 |
+
image_embeds, video_embeds, zero_feature = self.get_image_video_features(
|
| 140 |
+
pixel_values, pixel_values_videos,
|
| 141 |
+
image_grid_thw, video_grid_thw,
|
| 142 |
+
inputs_embeds.device, inputs_embeds.dtype,
|
| 143 |
+
)
|
| 144 |
+
if len(image_embeds) > 0:
|
| 145 |
+
image_embeds_cat = torch.cat(image_embeds, dim=0).to(
|
| 146 |
+
inputs_embeds.device, inputs_embeds.dtype
|
| 147 |
+
)
|
| 148 |
+
image_mask, _, union_mask = self.get_placeholder_mask(
|
| 149 |
+
input_ids, inputs_embeds, image_features=image_embeds_cat
|
| 150 |
+
)
|
| 151 |
+
inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds_cat)
|
| 152 |
+
if len(video_embeds) > 0:
|
| 153 |
+
video_embeds_cat = torch.cat(video_embeds, dim=0).to(
|
| 154 |
+
inputs_embeds.device, inputs_embeds.dtype
|
| 155 |
+
)
|
| 156 |
+
_, video_mask, union_mask = self.get_placeholder_mask(
|
| 157 |
+
input_ids, inputs_embeds, video_features=video_embeds_cat
|
| 158 |
+
)
|
| 159 |
+
inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds_cat)
|
| 160 |
+
inputs_embeds = inputs_embeds + zero_feature
|
| 161 |
+
|
| 162 |
+
# ----- Flow Matching: inject pre-built flow embeds at flow_positions ---
|
| 163 |
+
if flow_embeds is not None and flow_positions is not None and flow_positions.numel() > 0:
|
| 164 |
+
seq_len = inputs_embeds.shape[1]
|
| 165 |
+
replacement = inputs_embeds[0].clone()
|
| 166 |
+
position_mask = torch.zeros(seq_len, device=inputs_embeds.device, dtype=torch.bool)
|
| 167 |
+
# PERF: bulk D2H of flow_positions (small tensor) avoids 2 .item()
|
| 168 |
+
# calls per range × ~25 ranges per forward.
|
| 169 |
+
flow_positions_cpu = flow_positions.tolist()
|
| 170 |
+
idx = 0
|
| 171 |
+
for row in flow_positions_cpu:
|
| 172 |
+
s = int(row[0])
|
| 173 |
+
e = int(row[1])
|
| 174 |
+
n = e - s
|
| 175 |
+
replacement[s:e] = flow_embeds[idx : idx + n].to(inputs_embeds.dtype)
|
| 176 |
+
position_mask[s:e] = True
|
| 177 |
+
idx += n
|
| 178 |
+
mask_f = position_mask.to(inputs_embeds.dtype).unsqueeze(-1)
|
| 179 |
+
inputs_embeds = (
|
| 180 |
+
inputs_embeds[0] * (1.0 - mask_f) + replacement * mask_f
|
| 181 |
+
).unsqueeze(0)
|
| 182 |
+
|
| 183 |
+
# ----- modality_mask sanity ------------------------------------------
|
| 184 |
+
if modality_mask is None:
|
| 185 |
+
if union_mask is not None:
|
| 186 |
+
# union_mask is bool (input image/video tokens). Promote to int.
|
| 187 |
+
modality_mask = union_mask.long()
|
| 188 |
+
else:
|
| 189 |
+
modality_mask = torch.zeros(
|
| 190 |
+
inputs_embeds.shape[:-1], dtype=torch.long, device=inputs_embeds.device
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# ----- Forward through inner language model -------------------------
|
| 194 |
+
outputs = self.language_model(
|
| 195 |
+
input_ids=None,
|
| 196 |
+
position_ids=position_ids,
|
| 197 |
+
attention_mask=attention_mask,
|
| 198 |
+
past_key_values=past_key_values,
|
| 199 |
+
inputs_embeds=inputs_embeds,
|
| 200 |
+
cache_position=cache_position,
|
| 201 |
+
labels=labels,
|
| 202 |
+
modality_mask=modality_mask,
|
| 203 |
+
cu_seqlens=cu_seqlens,
|
| 204 |
+
sample_ids=sample_ids,
|
| 205 |
+
g_seqlens=g_seqlens,
|
| 206 |
+
input_image_mask=input_image_mask,
|
| 207 |
+
**kwargs,
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
return HunYuanVLMoTModelOutputWithPast(
|
| 211 |
+
loss=outputs.loss,
|
| 212 |
+
logits=outputs.logits,
|
| 213 |
+
past_key_values=outputs.past_key_values,
|
| 214 |
+
hidden_states=outputs.hidden_states,
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
# ---------------------------------------------------------------------------
|
| 219 |
+
# UnifiedMoTForConditionalGeneration — main entry point
|
| 220 |
+
# ---------------------------------------------------------------------------
|
| 221 |
+
|
| 222 |
+
class UnifiedMoTForConditionalGeneration(HunYuanVLMoTForConditionalGeneration):
|
| 223 |
+
"""Full unified model: MoT VL backbone + Flow Matching head."""
|
| 224 |
+
config_class = UnifiedMoTConfig
|
| 225 |
+
|
| 226 |
+
# mlp_g and FM modules don't exist in upstream HunYuanVLMoT base checkpoints;
|
| 227 |
+
# let `from_pretrained` skip them.
|
| 228 |
+
_keys_to_ignore_on_load_missing = [
|
| 229 |
+
r"time_embedder\..*",
|
| 230 |
+
r"vae2llm\..*",
|
| 231 |
+
r"llm2vae\..*",
|
| 232 |
+
r"latent_pos_embed\..*",
|
| 233 |
+
r".*\.mlp_g\..*",
|
| 234 |
+
r".*\.input_layernorm_g\..*",
|
| 235 |
+
r".*\.post_attention_layernorm_g\..*",
|
| 236 |
+
]
|
| 237 |
+
|
| 238 |
+
def __init__(self, config: UnifiedMoTConfig):
|
| 239 |
+
# Bypass upstream's __init__ (which would build a regular HunYuanVLMoTModel
|
| 240 |
+
# and miss our 3-path layers). Init the PreTrainedModel base directly.
|
| 241 |
+
HunYuanVLMoTPreTrainedModel.__init__(self, config)
|
| 242 |
+
self.model = UnifiedMoTModel(config)
|
| 243 |
+
self.config = config
|
| 244 |
+
|
| 245 |
+
hidden_size = config.text_config.hidden_size
|
| 246 |
+
|
| 247 |
+
# Flow Matching geometry (cached for hot path)
|
| 248 |
+
self.latent_patch_size = config.latent_patch_size
|
| 249 |
+
self.timestep_shift = config.timestep_shift
|
| 250 |
+
self.max_latent_size = config.max_latent_size
|
| 251 |
+
self.latent_channel = config.vae_z_channels
|
| 252 |
+
# Effective downsample from pixel to packed-patch latent token grid
|
| 253 |
+
self.latent_downsample = config.vae_image_downsample # already includes patch_size
|
| 254 |
+
self.patch_latent_dim = self.latent_patch_size ** 2 * self.latent_channel
|
| 255 |
+
|
| 256 |
+
# FM modules — initialized from scratch (zero-loaded by from_pretrained)
|
| 257 |
+
self.time_embedder = TimestepEmbedder(hidden_size)
|
| 258 |
+
self.vae2llm = nn.Linear(self.patch_latent_dim, hidden_size)
|
| 259 |
+
self.llm2vae = nn.Linear(hidden_size, self.patch_latent_dim)
|
| 260 |
+
self.latent_pos_embed = PositionEmbedding(self.max_latent_size, hidden_size)
|
| 261 |
+
|
| 262 |
+
# Loss weights
|
| 263 |
+
self.boundary_loss_weight = config.boundary_loss_weight
|
| 264 |
+
|
| 265 |
+
self.post_init()
|
| 266 |
+
|
| 267 |
+
# -----------------------------------------------------------------
|
| 268 |
+
# Helpers — delegate vision encoding/placeholder to upstream methods
|
| 269 |
+
# -----------------------------------------------------------------
|
| 270 |
+
|
| 271 |
+
def get_image_features(self, *args, **kwargs):
|
| 272 |
+
return self.model.get_image_features(*args, **kwargs)
|
| 273 |
+
|
| 274 |
+
def get_video_features(self, *args, **kwargs):
|
| 275 |
+
return self.model.get_video_features(*args, **kwargs)
|
| 276 |
+
|
| 277 |
+
def get_image_video_features(self, *args, **kwargs):
|
| 278 |
+
return self.model.get_image_video_features(*args, **kwargs)
|
| 279 |
+
|
| 280 |
+
def get_placeholder_mask(self, *args, **kwargs):
|
| 281 |
+
return self.model.get_placeholder_mask(*args, **kwargs)
|
| 282 |
+
|
| 283 |
+
def get_input_embeddings(self):
|
| 284 |
+
return self.model.language_model.get_input_embeddings()
|
| 285 |
+
|
| 286 |
+
def set_input_embeddings(self, value):
|
| 287 |
+
self.model.language_model.set_input_embeddings(value)
|
| 288 |
+
|
| 289 |
+
def get_output_embeddings(self):
|
| 290 |
+
return self.model.language_model.get_output_embeddings()
|
| 291 |
+
|
| 292 |
+
def set_output_embeddings(self, value):
|
| 293 |
+
self.model.language_model.set_output_embeddings(value)
|
| 294 |
+
|
| 295 |
+
# -----------------------------------------------------------------
|
| 296 |
+
# FM input builder — turn padded VAE latents into flow_embeds
|
| 297 |
+
# -----------------------------------------------------------------
|
| 298 |
+
|
| 299 |
+
def _build_flow_embeds(
|
| 300 |
+
self,
|
| 301 |
+
padded_latents: List[torch.Tensor],
|
| 302 |
+
latent_shapes: List[Tuple[int, int]],
|
| 303 |
+
latent_positions: Optional[torch.Tensor],
|
| 304 |
+
device,
|
| 305 |
+
dtype,
|
| 306 |
+
):
|
| 307 |
+
"""Pack VAE latents → noisy x_t → vae2llm + time_embed + pos_embed.
|
| 308 |
+
|
| 309 |
+
Returns (flow_embeds, vae_latent_clean, noise, timesteps).
|
| 310 |
+
"""
|
| 311 |
+
clean = pack_latents(padded_latents, latent_shapes, self.latent_patch_size, self.latent_channel)
|
| 312 |
+
clean = clean.to(device=device, dtype=dtype)
|
| 313 |
+
noise = torch.randn_like(clean)
|
| 314 |
+
timesteps = sample_timesteps(latent_shapes, self.timestep_shift, device, dtype)
|
| 315 |
+
noisy = build_noisy_latent(clean, noise, timesteps)
|
| 316 |
+
|
| 317 |
+
flow_embeds = self.vae2llm(noisy.to(self.vae2llm.weight.dtype)) + self.time_embedder(timesteps)
|
| 318 |
+
if latent_positions is not None:
|
| 319 |
+
flow_embeds = flow_embeds + self.latent_pos_embed(latent_positions)
|
| 320 |
+
return flow_embeds, clean, noise, timesteps
|
| 321 |
+
|
| 322 |
+
def _build_input_latent_embeds(
|
| 323 |
+
self,
|
| 324 |
+
padded_latents: List[torch.Tensor],
|
| 325 |
+
latent_shapes: List[Tuple[int, int]],
|
| 326 |
+
latent_positions: Optional[torch.Tensor],
|
| 327 |
+
device,
|
| 328 |
+
dtype,
|
| 329 |
+
):
|
| 330 |
+
"""For TI2I: clean (t=0) input image VAE latents projected into LLM space."""
|
| 331 |
+
clean = pack_latents(padded_latents, latent_shapes, self.latent_patch_size, self.latent_channel)
|
| 332 |
+
clean = clean.to(device=device, dtype=dtype)
|
| 333 |
+
t_zero = torch.zeros(clean.shape[0], device=device, dtype=dtype)
|
| 334 |
+
embeds = self.vae2llm(clean.to(self.vae2llm.weight.dtype)) + self.time_embedder(t_zero)
|
| 335 |
+
if latent_positions is not None:
|
| 336 |
+
embeds = embeds + self.latent_pos_embed(latent_positions)
|
| 337 |
+
return embeds
|
| 338 |
+
|
| 339 |
+
# -----------------------------------------------------------------
|
| 340 |
+
# Forward
|
| 341 |
+
# -----------------------------------------------------------------
|
| 342 |
+
|
| 343 |
+
def forward(
|
| 344 |
+
self,
|
| 345 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 346 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 347 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 348 |
+
past_key_values: Optional[Cache] = None,
|
| 349 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 350 |
+
pixel_values: Optional[torch.Tensor] = None,
|
| 351 |
+
pixel_values_videos: Optional[torch.FloatTensor] = None,
|
| 352 |
+
image_grid_thw: Optional[torch.LongTensor] = None,
|
| 353 |
+
video_grid_thw: Optional[torch.LongTensor] = None,
|
| 354 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 355 |
+
logits_to_keep: Union[int, torch.Tensor] = 0,
|
| 356 |
+
labels: Optional[torch.LongTensor] = None,
|
| 357 |
+
shift_labels: Optional[torch.LongTensor] = None,
|
| 358 |
+
# Packed signals
|
| 359 |
+
cu_seqlens: Optional[torch.Tensor] = None,
|
| 360 |
+
sample_ids: Optional[torch.Tensor] = None,
|
| 361 |
+
modality_mask: Optional[torch.Tensor] = None,
|
| 362 |
+
input_image_mask: Optional[torch.Tensor] = None,
|
| 363 |
+
# FM training inputs
|
| 364 |
+
use_flow_matching: bool = False,
|
| 365 |
+
padded_latent: Optional[List[torch.Tensor]] = None,
|
| 366 |
+
patchified_vae_latent_shapes: Optional[List[Tuple[int, int]]] = None,
|
| 367 |
+
vae_latent_indexes: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
| 368 |
+
vae_latent_positions: Optional[torch.Tensor] = None,
|
| 369 |
+
flow_positions: Optional[torch.Tensor] = None,
|
| 370 |
+
g_seqlens: Optional[torch.Tensor] = None,
|
| 371 |
+
# TI2I (input image VAE latent conditioning)
|
| 372 |
+
input_padded_latent: Optional[List[torch.Tensor]] = None,
|
| 373 |
+
input_vae_latent_shapes: Optional[List[Tuple[int, int]]] = None,
|
| 374 |
+
input_vae_latent_indexes: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
| 375 |
+
input_vae_latent_positions: Optional[torch.Tensor] = None,
|
| 376 |
+
**kwargs,
|
| 377 |
+
) -> UnifiedMoTOutput:
|
| 378 |
+
device = (input_ids if input_ids is not None else inputs_embeds).device
|
| 379 |
+
flow_embeds = None
|
| 380 |
+
vae_latent_clean = None
|
| 381 |
+
noise = None
|
| 382 |
+
timesteps = None
|
| 383 |
+
|
| 384 |
+
# ---- TI2I clean input latents (t=0, gets injected at input_*_indexes) -
|
| 385 |
+
# When provided, prepend them into inputs_embeds via flow_embeds path
|
| 386 |
+
# using the same flow_positions mechanism. Many call sites only use one
|
| 387 |
+
# of (input, generation) latents, so we handle them additively.
|
| 388 |
+
input_flow_embeds = None
|
| 389 |
+
if input_padded_latent is not None and input_vae_latent_shapes is not None:
|
| 390 |
+
if inputs_embeds is None and input_ids is not None:
|
| 391 |
+
inputs_embeds = self.get_input_embeddings()(input_ids)
|
| 392 |
+
input_ids = None # already materialized
|
| 393 |
+
input_flow_embeds = self._build_input_latent_embeds(
|
| 394 |
+
input_padded_latent, input_vae_latent_shapes,
|
| 395 |
+
input_vae_latent_positions, device, inputs_embeds.dtype,
|
| 396 |
+
)
|
| 397 |
+
# Splice into inputs_embeds at the input_vae_latent_indexes positions
|
| 398 |
+
if input_vae_latent_indexes is not None:
|
| 399 |
+
b_idx, s_idx = input_vae_latent_indexes
|
| 400 |
+
inputs_embeds = inputs_embeds.clone()
|
| 401 |
+
inputs_embeds[b_idx, s_idx] = input_flow_embeds.to(inputs_embeds.dtype)
|
| 402 |
+
|
| 403 |
+
# ---- Generation flow embeds (noisy x_t, used during FM training) ----
|
| 404 |
+
if use_flow_matching and padded_latent is not None and patchified_vae_latent_shapes:
|
| 405 |
+
if inputs_embeds is None and input_ids is not None:
|
| 406 |
+
inputs_embeds = self.get_input_embeddings()(input_ids)
|
| 407 |
+
input_ids = None
|
| 408 |
+
flow_embeds, vae_latent_clean, noise, timesteps = self._build_flow_embeds(
|
| 409 |
+
padded_latent, patchified_vae_latent_shapes,
|
| 410 |
+
vae_latent_positions, device, inputs_embeds.dtype,
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# ---- Forward through model -----------------------------------------
|
| 414 |
+
# Prefer pre-built inputs_embeds when we touched them above.
|
| 415 |
+
outputs = self.model(
|
| 416 |
+
input_ids=input_ids,
|
| 417 |
+
inputs_embeds=inputs_embeds,
|
| 418 |
+
attention_mask=attention_mask,
|
| 419 |
+
position_ids=position_ids,
|
| 420 |
+
past_key_values=past_key_values,
|
| 421 |
+
pixel_values=pixel_values,
|
| 422 |
+
pixel_values_videos=pixel_values_videos,
|
| 423 |
+
image_grid_thw=image_grid_thw,
|
| 424 |
+
video_grid_thw=video_grid_thw,
|
| 425 |
+
cache_position=cache_position,
|
| 426 |
+
logits_to_keep=logits_to_keep,
|
| 427 |
+
labels=None, # we compute loss ourselves
|
| 428 |
+
cu_seqlens=cu_seqlens,
|
| 429 |
+
sample_ids=sample_ids,
|
| 430 |
+
modality_mask=modality_mask,
|
| 431 |
+
input_image_mask=input_image_mask,
|
| 432 |
+
flow_embeds=flow_embeds,
|
| 433 |
+
flow_positions=flow_positions,
|
| 434 |
+
g_seqlens=g_seqlens,
|
| 435 |
+
**kwargs,
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
last_hidden_state = outputs.hidden_states # actually carries last_hidden_state per upstream output type
|
| 439 |
+
logits = outputs.logits
|
| 440 |
+
|
| 441 |
+
# ---- Compute losses ------------------------------------------------
|
| 442 |
+
text_loss_sum = None
|
| 443 |
+
text_loss_count = None
|
| 444 |
+
image_loss_sum = None
|
| 445 |
+
image_loss_count = None
|
| 446 |
+
boundary_loss = None
|
| 447 |
+
loss = None
|
| 448 |
+
|
| 449 |
+
# ---- Text CE loss (on shift_labels mask)
|
| 450 |
+
train_labels = shift_labels if shift_labels is not None else labels
|
| 451 |
+
if train_labels is not None and last_hidden_state is not None:
|
| 452 |
+
flat_hs = last_hidden_state.reshape(-1, last_hidden_state.size(-1))
|
| 453 |
+
flat_labels = train_labels.reshape(-1)
|
| 454 |
+
valid = flat_labels >= 0
|
| 455 |
+
n_valid = int(valid.sum().item())
|
| 456 |
+
if n_valid > 0:
|
| 457 |
+
hs_v = flat_hs[valid]
|
| 458 |
+
lbl_v = flat_labels[valid]
|
| 459 |
+
text_logits = self.get_output_embeddings()(hs_v).float()
|
| 460 |
+
ce_per_tok = F.cross_entropy(text_logits, lbl_v, reduction="none")
|
| 461 |
+
text_loss_sum = ce_per_tok.sum()
|
| 462 |
+
text_loss_count = torch.tensor(n_valid, device=device, dtype=torch.long)
|
| 463 |
+
|
| 464 |
+
# ---- Flow MSE loss + optional boundary loss
|
| 465 |
+
if vae_latent_clean is not None and vae_latent_indexes is not None and last_hidden_state is not None:
|
| 466 |
+
b_idx, s_idx = vae_latent_indexes
|
| 467 |
+
mse_hidden = last_hidden_state[b_idx, s_idx]
|
| 468 |
+
mse_preds = self.llm2vae(mse_hidden)
|
| 469 |
+
target = noise - vae_latent_clean # velocity target
|
| 470 |
+
has_mse = timesteps > 0
|
| 471 |
+
if has_mse.any():
|
| 472 |
+
mse_pred_f32 = mse_preds[has_mse].float()
|
| 473 |
+
target_f32 = target[has_mse].float()
|
| 474 |
+
# Per-token MSE summed for split aggregation; trainer divides by count
|
| 475 |
+
mse_per_tok = ((mse_pred_f32 - target_f32) ** 2).mean(dim=-1)
|
| 476 |
+
image_loss_sum = mse_per_tok.sum()
|
| 477 |
+
# PERF: avoid D2H .item() round-trip — has_mse.sum() is already
|
| 478 |
+
# a GPU tensor, just cast it to long.
|
| 479 |
+
image_loss_count = has_mse.sum().long()
|
| 480 |
+
|
| 481 |
+
if self.boundary_loss_weight > 0.0:
|
| 482 |
+
# Recover predicted x_0 = x_t - t * v_pred
|
| 483 |
+
noisy = build_noisy_latent(vae_latent_clean.detach(), noise.detach(), timesteps.detach())
|
| 484 |
+
x0_pred = noisy - timesteps[:, None].detach() * mse_preds
|
| 485 |
+
boundary_loss = patch_boundary_loss(
|
| 486 |
+
x0_pred, patchified_vae_latent_shapes,
|
| 487 |
+
self.latent_patch_size, self.latent_channel,
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
# ---- Aggregate scalar loss -----------------------------------------
|
| 491 |
+
# By default: mean text-CE + mean flow-MSE + boundary_weight * boundary
|
| 492 |
+
parts = []
|
| 493 |
+
if text_loss_sum is not None and text_loss_count is not None and text_loss_count.item() > 0:
|
| 494 |
+
parts.append(self.config.text_loss_weight * (text_loss_sum / text_loss_count.float()))
|
| 495 |
+
if image_loss_sum is not None and image_loss_count is not None and image_loss_count.item() > 0:
|
| 496 |
+
parts.append(self.config.flow_loss_weight * (image_loss_sum / image_loss_count.float()))
|
| 497 |
+
if boundary_loss is not None:
|
| 498 |
+
parts.append(self.boundary_loss_weight * boundary_loss)
|
| 499 |
+
if parts:
|
| 500 |
+
loss = parts[0]
|
| 501 |
+
for extra in parts[1:]:
|
| 502 |
+
loss = loss + extra
|
| 503 |
+
|
| 504 |
+
return UnifiedMoTOutput(
|
| 505 |
+
loss=loss,
|
| 506 |
+
logits=logits,
|
| 507 |
+
past_key_values=outputs.past_key_values,
|
| 508 |
+
hidden_states=last_hidden_state,
|
| 509 |
+
image_loss_sum=image_loss_sum,
|
| 510 |
+
image_loss_count=image_loss_count,
|
| 511 |
+
text_loss_sum=text_loss_sum,
|
| 512 |
+
text_loss_count=text_loss_count,
|
| 513 |
+
boundary_loss=boundary_loss,
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
__all__ = [
|
| 518 |
+
"UnifiedMoTModel",
|
| 519 |
+
"UnifiedMoTForConditionalGeneration",
|
| 520 |
+
"UnifiedMoTOutput",
|
| 521 |
+
"HY_VL_MOT_IMAGE_TOKEN_ID",
|
| 522 |
+
"HY_VL_MOT_VIDEO_TOKEN_ID",
|
| 523 |
+
]
|
model/mot_init_utils.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Initialization utilities for the MoT generation path (mlp_g, layernorm_g).
|
| 2 |
+
|
| 3 |
+
Two regimes are handled by a single entry point `maybe_init_generation_path`:
|
| 4 |
+
|
| 5 |
+
(a) same-size copy : mlp_g.intermediate_size == mlp_v.intermediate_size
|
| 6 |
+
-> plain state_dict copy (legacy behavior).
|
| 7 |
+
(b) Net2Wider expansion: mlp_g.intermediate_size > mlp_v.intermediate_size
|
| 8 |
+
-> function-preserving neuron duplication (Chen et al.,
|
| 9 |
+
"Net2Net") with small Gaussian noise to break symmetry.
|
| 10 |
+
|
| 11 |
+
Only integer-multiple widening is exact; non-integer multiples use round-robin
|
| 12 |
+
replicate-and-trim with reciprocal-count scaling (still function-preserving, but
|
| 13 |
+
logged as a warning — integer multiples are strongly preferred).
|
| 14 |
+
|
| 15 |
+
All layernorm_g state is always plain-copied from layernorm_v because layernorms
|
| 16 |
+
are on hidden_size and unaffected by MLP widening.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import logging
|
| 21 |
+
from typing import Optional
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn as nn
|
| 25 |
+
|
| 26 |
+
logger = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@torch.no_grad()
|
| 30 |
+
def _net2wider_expand_mlp(
|
| 31 |
+
mlp_g: nn.Module,
|
| 32 |
+
mlp_v: nn.Module,
|
| 33 |
+
noise_std: float = 1e-4,
|
| 34 |
+
generator: Optional[torch.Generator] = None,
|
| 35 |
+
) -> None:
|
| 36 |
+
"""In-place initialize `mlp_g` to be function-equivalent to `mlp_v` via neuron
|
| 37 |
+
duplication. After this call, for any input x, `mlp_g(x) ~= mlp_v(x)` within
|
| 38 |
+
O(noise_std * ||x||).
|
| 39 |
+
|
| 40 |
+
Expects a SwiGLU-style MLP with linear modules `.gate_proj`, `.up_proj`,
|
| 41 |
+
`.down_proj`, all bias-free, with shapes:
|
| 42 |
+
gate_proj / up_proj : (D, H)
|
| 43 |
+
down_proj : (H, D)
|
| 44 |
+
"""
|
| 45 |
+
d_v = mlp_v.gate_proj.out_features
|
| 46 |
+
d_g = mlp_g.gate_proj.out_features
|
| 47 |
+
assert mlp_v.gate_proj.in_features == mlp_g.gate_proj.in_features, (
|
| 48 |
+
f"hidden_size mismatch: mlp_v={mlp_v.gate_proj.in_features}, "
|
| 49 |
+
f"mlp_g={mlp_g.gate_proj.in_features}"
|
| 50 |
+
)
|
| 51 |
+
assert mlp_v.down_proj.out_features == mlp_g.down_proj.out_features, (
|
| 52 |
+
f"hidden_size mismatch on down_proj: mlp_v={mlp_v.down_proj.out_features}, "
|
| 53 |
+
f"mlp_g={mlp_g.down_proj.out_features}"
|
| 54 |
+
)
|
| 55 |
+
assert d_g >= d_v, (
|
| 56 |
+
f"mlp_g width {d_g} < mlp_v width {d_v} (shrinking unsupported)"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Same-size fast path: plain copy, ignore noise.
|
| 60 |
+
if d_g == d_v:
|
| 61 |
+
mlp_g.load_state_dict(mlp_v.state_dict())
|
| 62 |
+
return
|
| 63 |
+
|
| 64 |
+
if d_g % d_v != 0:
|
| 65 |
+
logger.warning(
|
| 66 |
+
f"Net2Wider: d_g={d_g} not an integer multiple of d_v={d_v}; "
|
| 67 |
+
f"using replicate-and-trim. Integer multiples (e.g. 2x) are strongly "
|
| 68 |
+
f"preferred for cleanest function preservation."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
device = mlp_v.gate_proj.weight.device
|
| 72 |
+
# indices[j] = source neuron index for destination neuron j
|
| 73 |
+
# Round-robin: for D_g = k*D_v this produces [0,1,...,D_v-1, 0,1,...,D_v-1, ...]
|
| 74 |
+
# which makes twins "adjacent in source" rather than "adjacent in destination".
|
| 75 |
+
# Either layout works mathematically; this choice keeps each source's twins
|
| 76 |
+
# spread out along the destination axis, which is marginally friendlier to
|
| 77 |
+
# downstream gather/scatter operations if any later code happens to assume
|
| 78 |
+
# contiguity within a source group.
|
| 79 |
+
indices = torch.arange(d_g, device=device) % d_v
|
| 80 |
+
counts = torch.bincount(indices, minlength=d_v) # (D_v,)
|
| 81 |
+
|
| 82 |
+
# --- gate_proj and up_proj: row-wise replication ---------------------------
|
| 83 |
+
for name in ("gate_proj", "up_proj"):
|
| 84 |
+
src = getattr(mlp_v, name).weight.data # (D_v, H)
|
| 85 |
+
dst_param = getattr(mlp_g, name).weight
|
| 86 |
+
dst = src[indices].clone().to(dtype=dst_param.dtype) # (D_g, H)
|
| 87 |
+
if noise_std > 0:
|
| 88 |
+
noise = torch.empty(
|
| 89 |
+
dst.shape, dtype=dst.dtype, device=dst.device,
|
| 90 |
+
).normal_(mean=0.0, std=noise_std, generator=generator)
|
| 91 |
+
dst.add_(noise)
|
| 92 |
+
dst_param.data.copy_(dst)
|
| 93 |
+
|
| 94 |
+
# --- down_proj: column-wise replication with reciprocal-count scaling -----
|
| 95 |
+
# down_v: (H, D_v), down_g: (H, D_g).
|
| 96 |
+
# For each destination column j with source s = indices[j]:
|
| 97 |
+
# down_g[:, j] = down_v[:, s] / counts[s]
|
| 98 |
+
# so that Sum_{j: indices[j]=s} down_g[:, j] * h_s
|
| 99 |
+
# = down_v[:, s] * h_s (function preserved exactly).
|
| 100 |
+
down_v = mlp_v.down_proj.weight.data # (H, D_v)
|
| 101 |
+
dst_param = mlp_g.down_proj.weight
|
| 102 |
+
scale = (1.0 / counts.to(down_v.dtype)) # (D_v,)
|
| 103 |
+
dst = (down_v[:, indices] * scale[indices]).clone().to(dtype=dst_param.dtype) # (H, D_g)
|
| 104 |
+
if noise_std > 0:
|
| 105 |
+
noise = torch.empty(
|
| 106 |
+
dst.shape, dtype=dst.dtype, device=dst.device,
|
| 107 |
+
).normal_(mean=0.0, std=noise_std, generator=generator)
|
| 108 |
+
dst.add_(noise)
|
| 109 |
+
dst_param.data.copy_(dst)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@torch.no_grad()
|
| 113 |
+
def init_mlp_g_from_mlp_v_net2wider(
|
| 114 |
+
layer,
|
| 115 |
+
noise_std: float = 1e-4,
|
| 116 |
+
generator: Optional[torch.Generator] = None,
|
| 117 |
+
) -> dict:
|
| 118 |
+
"""Initialize a single decoder layer's generation path (mlp_g + layernorm_g)
|
| 119 |
+
from its vision path (mlp_v + layernorm_v).
|
| 120 |
+
|
| 121 |
+
Auto-detects same-size vs expansion case by comparing MLP widths.
|
| 122 |
+
Layernorms are always plain-copied (shape is hidden_size, unchanged).
|
| 123 |
+
|
| 124 |
+
Defensive against meta-device parameters: when transformers'
|
| 125 |
+
`from_pretrained(dtype=...)` leaves `_g` params on the meta device (because
|
| 126 |
+
they aren't in the upstream state_dict), a plain `load_state_dict` on
|
| 127 |
+
those params doesn't materialize them — they stay meta and the `.to(...)`
|
| 128 |
+
that follows produces garbage GPU memory. To avoid that we rebuild the
|
| 129 |
+
`_g` parameters as fresh tensors on the same device/dtype as their `_v`
|
| 130 |
+
counterparts.
|
| 131 |
+
"""
|
| 132 |
+
d_v = layer.mlp_v.gate_proj.out_features
|
| 133 |
+
d_g = layer.mlp_g.gate_proj.out_features
|
| 134 |
+
|
| 135 |
+
# Materialize meta-device params before in-place copy: rebuild _g linear
|
| 136 |
+
# weights on the same device/dtype as _v.
|
| 137 |
+
def _materialize_linear(dst_lin: nn.Linear, src_lin: nn.Linear):
|
| 138 |
+
if dst_lin.weight.is_meta or dst_lin.weight.device.type == "meta":
|
| 139 |
+
new_w = torch.empty_like(src_lin.weight)
|
| 140 |
+
dst_lin.weight = nn.Parameter(new_w, requires_grad=dst_lin.weight.requires_grad)
|
| 141 |
+
if dst_lin.bias is not None:
|
| 142 |
+
new_b = torch.empty_like(src_lin.bias) if src_lin.bias is not None else torch.zeros(
|
| 143 |
+
dst_lin.out_features, dtype=src_lin.weight.dtype, device=src_lin.weight.device,
|
| 144 |
+
)
|
| 145 |
+
dst_lin.bias = nn.Parameter(new_b, requires_grad=dst_lin.bias.requires_grad)
|
| 146 |
+
|
| 147 |
+
def _materialize_norm(dst_norm, src_norm):
|
| 148 |
+
if dst_norm.weight.is_meta or dst_norm.weight.device.type == "meta":
|
| 149 |
+
new_w = torch.empty_like(src_norm.weight)
|
| 150 |
+
dst_norm.weight = nn.Parameter(new_w, requires_grad=dst_norm.weight.requires_grad)
|
| 151 |
+
|
| 152 |
+
for name in ("gate_proj", "up_proj", "down_proj"):
|
| 153 |
+
if d_g == d_v:
|
| 154 |
+
_materialize_linear(getattr(layer.mlp_g, name), getattr(layer.mlp_v, name))
|
| 155 |
+
# else: widening path will rebuild mlp_g entirely in maybe_init_generation_path
|
| 156 |
+
_materialize_norm(layer.input_layernorm_g, layer.input_layernorm_v)
|
| 157 |
+
_materialize_norm(layer.post_attention_layernorm_g, layer.post_attention_layernorm_v)
|
| 158 |
+
|
| 159 |
+
_net2wider_expand_mlp(
|
| 160 |
+
layer.mlp_g, layer.mlp_v,
|
| 161 |
+
noise_std=noise_std if d_g > d_v else 0.0,
|
| 162 |
+
generator=generator,
|
| 163 |
+
)
|
| 164 |
+
layer.input_layernorm_g.load_state_dict(layer.input_layernorm_v.state_dict())
|
| 165 |
+
layer.post_attention_layernorm_g.load_state_dict(
|
| 166 |
+
layer.post_attention_layernorm_v.state_dict()
|
| 167 |
+
)
|
| 168 |
+
return {"D_v": d_v, "D_g": d_g, "expanded": d_g > d_v, "noise_std": noise_std}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@torch.no_grad()
|
| 172 |
+
def verify_mlp_g_equals_mlp_v(
|
| 173 |
+
layer,
|
| 174 |
+
num_samples: int = 4,
|
| 175 |
+
seq_len: int = 8,
|
| 176 |
+
) -> float:
|
| 177 |
+
"""Diagnostic: forward random activations through both paths (under their
|
| 178 |
+
respective post-attention layernorms) and return max absolute difference.
|
| 179 |
+
|
| 180 |
+
Called from unit tests and logged right after init on layer 0 as a sanity
|
| 181 |
+
check. Uses fp32 computation when possible (casts output back if needed).
|
| 182 |
+
"""
|
| 183 |
+
hidden = layer.hidden_size
|
| 184 |
+
dev = layer.mlp_v.gate_proj.weight.device
|
| 185 |
+
dt = layer.mlp_v.gate_proj.weight.dtype
|
| 186 |
+
x = torch.randn(num_samples, seq_len, hidden, device=dev, dtype=dt)
|
| 187 |
+
y_v = layer.mlp_v(layer.post_attention_layernorm_v(x))
|
| 188 |
+
y_g = layer.mlp_g(layer.post_attention_layernorm_g(x))
|
| 189 |
+
return (y_v.float() - y_g.float()).abs().max().item()
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def checkpoint_has_g_keys(model_load_path: Optional[str]) -> bool:
|
| 193 |
+
"""True iff the checkpoint on disk already contains `_g.` weights.
|
| 194 |
+
|
| 195 |
+
This is the robust way to tell "generation path was previously saved; load
|
| 196 |
+
it as-is" from "generation path is absent; we need to initialize it from
|
| 197 |
+
mlp_v".
|
| 198 |
+
|
| 199 |
+
Returns False if the path does not exist or no safetensors file is found
|
| 200 |
+
(caller may fall back to a tensor-level check if needed).
|
| 201 |
+
"""
|
| 202 |
+
if model_load_path is None:
|
| 203 |
+
return False
|
| 204 |
+
import os
|
| 205 |
+
import json as _json
|
| 206 |
+
index_path = os.path.join(model_load_path, "model.safetensors.index.json")
|
| 207 |
+
single_path = os.path.join(model_load_path, "model.safetensors")
|
| 208 |
+
if os.path.exists(index_path):
|
| 209 |
+
with open(index_path) as f:
|
| 210 |
+
keys = set(_json.load(f)["weight_map"].keys())
|
| 211 |
+
return any("_g." in k for k in keys)
|
| 212 |
+
if os.path.exists(single_path):
|
| 213 |
+
try:
|
| 214 |
+
from safetensors import safe_open
|
| 215 |
+
except ImportError:
|
| 216 |
+
return False
|
| 217 |
+
with safe_open(single_path, framework="pt") as f:
|
| 218 |
+
return any("_g." in k for k in f.keys())
|
| 219 |
+
return False
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def _resolve_decoder_layers(model):
|
| 223 |
+
"""Walk common wrapper paths to find the list of transformer decoder layers.
|
| 224 |
+
|
| 225 |
+
Supports:
|
| 226 |
+
UnifiedMoTForConditionalGeneration -> .model.language_model.model.layers
|
| 227 |
+
bare HunYuan MoT model -> .language_model.model.layers
|
| 228 |
+
even-barer HF model -> .model.layers
|
| 229 |
+
"""
|
| 230 |
+
for path in (
|
| 231 |
+
"model.language_model.model.layers",
|
| 232 |
+
"language_model.model.layers",
|
| 233 |
+
"model.layers",
|
| 234 |
+
):
|
| 235 |
+
obj = model
|
| 236 |
+
ok = True
|
| 237 |
+
for attr in path.split("."):
|
| 238 |
+
if not hasattr(obj, attr):
|
| 239 |
+
ok = False
|
| 240 |
+
break
|
| 241 |
+
obj = getattr(obj, attr)
|
| 242 |
+
if ok:
|
| 243 |
+
return obj
|
| 244 |
+
raise AttributeError(
|
| 245 |
+
"Could not locate decoder layers on model; tried "
|
| 246 |
+
"model.language_model.model.layers / language_model.model.layers / model.layers"
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def maybe_init_generation_path(
|
| 251 |
+
model,
|
| 252 |
+
model_load_path: Optional[str] = None,
|
| 253 |
+
noise_std: float = 1e-4,
|
| 254 |
+
logger_: Optional[logging.Logger] = None,
|
| 255 |
+
seed: int = 0xA17C0D1F,
|
| 256 |
+
target_mlp_g_intermediate_size: Optional[int] = None,
|
| 257 |
+
) -> bool:
|
| 258 |
+
"""Top-level entry point used by train / inference / eval scripts.
|
| 259 |
+
|
| 260 |
+
Behavior:
|
| 261 |
+
* If `model_load_path` is provided and the checkpoint contains any `_g.`
|
| 262 |
+
key, do nothing and return False (generation path was already loaded
|
| 263 |
+
from the checkpoint — this is the T2I-resume / TI2I-continuation path).
|
| 264 |
+
* Otherwise iterate every decoder layer and call
|
| 265 |
+
`init_mlp_g_from_mlp_v_net2wider`:
|
| 266 |
+
- Same-size case -> plain state_dict copy (noise ignored).
|
| 267 |
+
- Expansion case -> Net2Wider neuron duplication + Gaussian noise
|
| 268 |
+
(std = `noise_std`).
|
| 269 |
+
|
| 270 |
+
`target_mlp_g_intermediate_size`: if provided and larger than the current
|
| 271 |
+
`mlp_g.intermediate_size`, REBUILD each layer's `mlp_g` to the wider size
|
| 272 |
+
before running Net2Wider init. Use this when the base checkpoint was
|
| 273 |
+
loaded with mlp_g at mlp_v's width (so from_pretrained accepted the
|
| 274 |
+
shapes) and you want to widen post-load.
|
| 275 |
+
|
| 276 |
+
Returns True if initialization was performed.
|
| 277 |
+
"""
|
| 278 |
+
log = logger_ or logger
|
| 279 |
+
|
| 280 |
+
# ---- Re-materialize the latent_pos_embed (sin-cos table) ---------------
|
| 281 |
+
# PositionEmbedding's parameter is computed at __init__ time but
|
| 282 |
+
# `from_pretrained(dtype=...)` uses meta-init that bypasses the
|
| 283 |
+
# constructor's data assignment, leaving the param uninitialized.
|
| 284 |
+
pos_embed_module = None
|
| 285 |
+
for path in ("latent_pos_embed", "model.latent_pos_embed"):
|
| 286 |
+
obj = model
|
| 287 |
+
ok = True
|
| 288 |
+
for attr in path.split("."):
|
| 289 |
+
if not hasattr(obj, attr):
|
| 290 |
+
ok = False
|
| 291 |
+
break
|
| 292 |
+
obj = getattr(obj, attr)
|
| 293 |
+
if ok:
|
| 294 |
+
pos_embed_module = obj
|
| 295 |
+
break
|
| 296 |
+
if pos_embed_module is not None and hasattr(pos_embed_module, "_reset_parameters"):
|
| 297 |
+
pos_embed_module._reset_parameters()
|
| 298 |
+
log.info(
|
| 299 |
+
f"latent_pos_embed re-initialized from sin-cos table "
|
| 300 |
+
f"(shape={tuple(pos_embed_module.pos_embed.shape)})"
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
if checkpoint_has_g_keys(model_load_path):
|
| 304 |
+
log.info(
|
| 305 |
+
"Generation path present in checkpoint (found `_g.` keys); "
|
| 306 |
+
"skipping mlp_g initialization."
|
| 307 |
+
)
|
| 308 |
+
return False
|
| 309 |
+
|
| 310 |
+
layers = _resolve_decoder_layers(model)
|
| 311 |
+
d_v = layers[0].mlp_v.gate_proj.out_features
|
| 312 |
+
d_g_current = layers[0].mlp_g.gate_proj.out_features
|
| 313 |
+
|
| 314 |
+
# ---- Optional: rebuild mlp_g at a wider size before init ----
|
| 315 |
+
if target_mlp_g_intermediate_size is not None and target_mlp_g_intermediate_size > d_g_current:
|
| 316 |
+
from transformers.models.hunyuan_vl_mot.modeling_hunyuan_vl_mot import HunYuanVLMoTMLP
|
| 317 |
+
for layer in layers:
|
| 318 |
+
base_cfg = layer.mlp_g.gate_proj.weight # to inherit device/dtype
|
| 319 |
+
device = base_cfg.device
|
| 320 |
+
dtype = base_cfg.dtype
|
| 321 |
+
# Use a duck-typed mini config matching upstream MLP's expected fields
|
| 322 |
+
class _MiniCfg:
|
| 323 |
+
pass
|
| 324 |
+
mini = _MiniCfg()
|
| 325 |
+
mini.hidden_size = layer.mlp_v.gate_proj.in_features
|
| 326 |
+
mini.intermediate_size = target_mlp_g_intermediate_size
|
| 327 |
+
mini.hidden_act = "silu"
|
| 328 |
+
mini.mlp_bias = (layer.mlp_v.gate_proj.bias is not None)
|
| 329 |
+
new_mlp_g = HunYuanVLMoTMLP(mini).to(device=device, dtype=dtype)
|
| 330 |
+
layer.mlp_g = new_mlp_g
|
| 331 |
+
d_g = target_mlp_g_intermediate_size
|
| 332 |
+
log.info(f"Rebuilt mlp_g for all layers at intermediate_size={d_g}.")
|
| 333 |
+
else:
|
| 334 |
+
d_g = d_g_current
|
| 335 |
+
|
| 336 |
+
expansion = d_g > d_v
|
| 337 |
+
mode = "copy" if not expansion else f"net2wider (x{d_g / d_v:.3f})"
|
| 338 |
+
log.info(
|
| 339 |
+
f"Initializing mlp_g/layernorm_g from mlp_v/layernorm_v: "
|
| 340 |
+
f"mode={mode} (d_v={d_v}, d_g={d_g}, "
|
| 341 |
+
f"noise_std={noise_std if expansion else 0.0:g})"
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
# Single deterministic generator for reproducibility across layers.
|
| 345 |
+
gen = torch.Generator(device=layers[0].mlp_v.gate_proj.weight.device)
|
| 346 |
+
gen.manual_seed(seed)
|
| 347 |
+
|
| 348 |
+
for layer in layers:
|
| 349 |
+
init_mlp_g_from_mlp_v_net2wider(
|
| 350 |
+
layer,
|
| 351 |
+
noise_std=noise_std if expansion else 0.0,
|
| 352 |
+
generator=gen,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
# Spot-check layer 0: forward a random input through both paths. The diff
|
| 356 |
+
# should be ~O(noise_std * sqrt(D_v) * ||x||); much larger indicates a bug.
|
| 357 |
+
try:
|
| 358 |
+
diff = verify_mlp_g_equals_mlp_v(layers[0])
|
| 359 |
+
log.info(
|
| 360 |
+
f" post-init forward diff (layer 0, random input): "
|
| 361 |
+
f"max |mlp_v - mlp_g| = {diff:.3e}"
|
| 362 |
+
)
|
| 363 |
+
except Exception as e: # pylint: disable=broad-except # pragma: no cover - diagnostic only
|
| 364 |
+
log.warning(f"verify_mlp_g_equals_mlp_v diagnostic failed: {e}")
|
| 365 |
+
|
| 366 |
+
return True
|
multiframe_inference.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Joint fixed-N multi-frame generation inference (world-model / x_to_N).
|
| 2 |
+
|
| 3 |
+
Unlike `interleave_inference.py` (SigLIP-handoff autoregressive rollout — a prior
|
| 4 |
+
frame re-enters the next step as a *clean SigLIP image*), multi-frame generation
|
| 5 |
+
lays out **all N `<Image>` blocks in ONE sequence** and integrates them **jointly**
|
| 6 |
+
with a single Euler ODE. A prior frame stays in context as its **VAE latent**
|
| 7 |
+
(at the shared ODE timestep), exactly matching training, where the assistant turn
|
| 8 |
+
held N latent blocks denoised together (per-frame independent timesteps + hybrid
|
| 9 |
+
block attention: causal across frames, bidirectional within). So inference is:
|
| 10 |
+
|
| 11 |
+
seq = prompt(+ViT obs) + "open text" + N×(<Image>[LAT]*n</Image>) + EOS
|
| 12 |
+
x_k ~ N(0,1) for k in 1..N
|
| 13 |
+
for t in linspace(1,0,steps): # ONE shared schedule
|
| 14 |
+
flow_embed_k = vae2llm(x_k) + time(t) + pos # k = 1..N, concatenated
|
| 15 |
+
hidden = model(... flow_embeds @ N flow_positions ...) # ONE forward
|
| 16 |
+
x_k -= dt * llm2vae(hidden @ block_k) # k = 1..N
|
| 17 |
+
frame_k = VAE.decode(x_k)
|
| 18 |
+
|
| 19 |
+
The N frames cohere because frame k attends to frames 1..k (cross-frame causal),
|
| 20 |
+
all in the single forward pass — no per-frame autoregressive loop.
|
| 21 |
+
|
| 22 |
+
Alignment with training (this is the load-bearing correspondence)
|
| 23 |
+
-----------------------------------------------------------------
|
| 24 |
+
TRAINING (modeling_unified_mot._build_flow_embeds + flow_matching_modules.sample_timesteps):
|
| 25 |
+
the N target frames are packed into one sequence; `sample_timesteps` draws ONE
|
| 26 |
+
INDEPENDENT timestep per frame (`randn(len(shapes))`), each broadcast over that
|
| 27 |
+
frame's tokens; `x_t=(1-t)·clean+t·noise` per frame; a SINGLE forward predicts
|
| 28 |
+
the velocity for ALL N frames and the FM-MSE is summed over all N. So: every
|
| 29 |
+
frame independently noised, one forward → N frames.
|
| 30 |
+
|
| 31 |
+
INFERENCE (here): one forward PER Euler step injects all N frames' current x_t
|
| 32 |
+
(each with its own `time_embedder(t_k)`) at the N flow_positions and reads the
|
| 33 |
+
velocity for ALL N frames — structurally identical to the training forward
|
| 34 |
+
(one forward, N frames, per-frame time embedding). We therefore do NOT loop
|
| 35 |
+
per frame and do NOT re-encode prior frames via SigLIP.
|
| 36 |
+
|
| 37 |
+
The only inference-time choice is the per-frame timestep SCHEDULE `t_k(step)`:
|
| 38 |
+
- lockstep (default, validated ~28 dB): all frames share the same t each step,
|
| 39 |
+
swept 1→0 together. This is the equal-t diagonal of the joint t-space that the
|
| 40 |
+
independent-per-frame training already covers, so it is in-distribution.
|
| 41 |
+
- Because training randomized t per frame (incl. cases where earlier frames are
|
| 42 |
+
much cleaner than later ones), a staggered "diffusion-forcing" schedule
|
| 43 |
+
(frame k offset so it lags frame k-1) is also in-distribution and tends to
|
| 44 |
+
help long autoregressive rollouts — left as a future `--schedule` option.
|
| 45 |
+
Either way each step is ONE forward over all N frames; we never collapse the
|
| 46 |
+
model's per-frame timestep capability into N separate passes.
|
| 47 |
+
|
| 48 |
+
Run:
|
| 49 |
+
python multiframe_inference.py --ckpt <ckpt> --vae <ae.safetensors> \
|
| 50 |
+
--frames obs0.jpg obs1.jpg --task "imagine the next frames" \
|
| 51 |
+
--num_frames 4 --num_steps 50 --out_dir multiframe_out
|
| 52 |
+
"""
|
| 53 |
+
from __future__ import annotations
|
| 54 |
+
|
| 55 |
+
import argparse
|
| 56 |
+
import os
|
| 57 |
+
from typing import List, Optional
|
| 58 |
+
|
| 59 |
+
import torch
|
| 60 |
+
from PIL import Image
|
| 61 |
+
from transformers.models.hunyuan_vl_mot import HunYuanVLMoTProcessor
|
| 62 |
+
|
| 63 |
+
# Reuse the (obs frames + task) prompt constructor, ViT placeholder ids, and the
|
| 64 |
+
# montage helper from the interleave inference module.
|
| 65 |
+
from interleave_inference import (
|
| 66 |
+
build_conditioned_sequence,
|
| 67 |
+
INPUT_IMAGE_PLACEHOLDER_IDS,
|
| 68 |
+
_row,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@torch.no_grad()
|
| 73 |
+
def _ar_decode_opening_text(
|
| 74 |
+
inner, processor, prompt_ids, pixel_values, image_grid_thw,
|
| 75 |
+
image_start_id, eos_id, device, max_text_tokens,
|
| 76 |
+
):
|
| 77 |
+
"""Greedily decode the assistant opening text until the model emits <Image>.
|
| 78 |
+
|
| 79 |
+
Mirrors the text loop in interleave_inference.generate_step_joint. Returns the
|
| 80 |
+
decoded token ids (without the terminating <Image>/EOS)."""
|
| 81 |
+
seq = list(prompt_ids)
|
| 82 |
+
text_ids: List[int] = []
|
| 83 |
+
|
| 84 |
+
def logits_last(ids):
|
| 85 |
+
seq_len = len(ids)
|
| 86 |
+
inp = torch.tensor(ids, dtype=torch.long, device=device).unsqueeze(0)
|
| 87 |
+
mod = torch.zeros(1, seq_len, dtype=torch.long, device=device)
|
| 88 |
+
iim = torch.zeros(1, seq_len, dtype=torch.bool, device=device)
|
| 89 |
+
for pid in INPUT_IMAGE_PLACEHOLDER_IDS:
|
| 90 |
+
m = inp[0] == pid
|
| 91 |
+
mod[0, m] = 1
|
| 92 |
+
iim[0, m] = True
|
| 93 |
+
out = inner(
|
| 94 |
+
input_ids=inp, inputs_embeds=None, attention_mask=None,
|
| 95 |
+
position_ids=torch.arange(seq_len, device=device).unsqueeze(0),
|
| 96 |
+
pixel_values=pixel_values, image_grid_thw=image_grid_thw,
|
| 97 |
+
cu_seqlens=torch.tensor([0, seq_len], dtype=torch.int32, device=device),
|
| 98 |
+
sample_ids=torch.zeros(1, seq_len, dtype=torch.int32, device=device),
|
| 99 |
+
modality_mask=mod, input_image_mask=iim,
|
| 100 |
+
flow_embeds=None, flow_positions=None,
|
| 101 |
+
g_seqlens=torch.zeros((0, 2), dtype=torch.int32, device=device),
|
| 102 |
+
)
|
| 103 |
+
return out.logits[0, -1]
|
| 104 |
+
|
| 105 |
+
for _ in range(max_text_tokens):
|
| 106 |
+
nxt = int(logits_last(seq).argmax().item())
|
| 107 |
+
if nxt == image_start_id or nxt == eos_id:
|
| 108 |
+
break
|
| 109 |
+
text_ids.append(nxt)
|
| 110 |
+
seq.append(nxt)
|
| 111 |
+
return text_ids
|
| 112 |
+
|
| 113 |
+
@torch.no_grad()
|
| 114 |
+
def generate_multiframe_joint(
|
| 115 |
+
model, vae, processor,
|
| 116 |
+
obs_frames: List[str], task_text: str, num_frames: int,
|
| 117 |
+
height: int, width: int, num_steps: int, device, dtype,
|
| 118 |
+
max_text_tokens: int = 96, opening_text: Optional[str] = None,
|
| 119 |
+
):
|
| 120 |
+
"""Jointly denoise N frames in one sequence. Returns (opening_text, [imgs])."""
|
| 121 |
+
from model.flow_matching_modules import unpatchify_latent
|
| 122 |
+
from text2image_inference import get_2d_position_ids
|
| 123 |
+
|
| 124 |
+
cfg = model.config
|
| 125 |
+
eos = cfg.eos_token_id
|
| 126 |
+
# Multi-frame uses a single <Video> ... </Video> wrapper instead of N×<Image>.
|
| 127 |
+
video_start = cfg.video_start_token_id # 120122
|
| 128 |
+
video_end = cfg.video_end_token_id # 120123
|
| 129 |
+
latent_ph = cfg.flow_latent_placeholder_id
|
| 130 |
+
inner = model.model
|
| 131 |
+
|
| 132 |
+
p = model.latent_patch_size
|
| 133 |
+
ds = cfg.vae_image_downsample
|
| 134 |
+
h_lat, w_lat = height // ds, width // ds
|
| 135 |
+
n_latent = h_lat * w_lat
|
| 136 |
+
patch_dim = p * p * cfg.vae_z_channels
|
| 137 |
+
|
| 138 |
+
# Append the multi-frame task instruction to the user turn (train==infer parity).
|
| 139 |
+
# build_conditioned_sequence folds task_text into the prompt.
|
| 140 |
+
from inference_utils import TASK_INSTRUCTION_MULTI_FRAME
|
| 141 |
+
instr_text = (task_text + "\n" + TASK_INSTRUCTION_MULTI_FRAME) if task_text else TASK_INSTRUCTION_MULTI_FRAME
|
| 142 |
+
prompt_ids, proc = build_conditioned_sequence(processor, obs_frames, instr_text)
|
| 143 |
+
pixel_values = proc.get("pixel_values")
|
| 144 |
+
image_grid_thw = proc.get("image_grid_thw")
|
| 145 |
+
if pixel_values is not None:
|
| 146 |
+
pixel_values = pixel_values.to(device=device, dtype=dtype)
|
| 147 |
+
if image_grid_thw is not None:
|
| 148 |
+
image_grid_thw = image_grid_thw.to(device=device)
|
| 149 |
+
|
| 150 |
+
# ---- opening text: teacher-force from GT or AR-decode until <Video> ----
|
| 151 |
+
if opening_text is not None:
|
| 152 |
+
text_ids = processor.tokenizer.encode(opening_text, add_special_tokens=False)
|
| 153 |
+
text_str = opening_text
|
| 154 |
+
else:
|
| 155 |
+
text_ids = _ar_decode_opening_text(
|
| 156 |
+
inner, processor, prompt_ids, pixel_values, image_grid_thw,
|
| 157 |
+
video_start, eos, device, max_text_tokens,
|
| 158 |
+
)
|
| 159 |
+
text_str = processor.tokenizer.decode(text_ids, skip_special_tokens=True)
|
| 160 |
+
|
| 161 |
+
# ---- lay out the full sequence: <Video> + N×[LAT] + </Video> (bare latents,
|
| 162 |
+
# matches the dataset's multi-frame layout; one flow_positions row per frame) ----
|
| 163 |
+
seq = list(prompt_ids) + list(text_ids)
|
| 164 |
+
seq.append(video_start)
|
| 165 |
+
frame_spans = [] # (latent_start, latent_end) per frame
|
| 166 |
+
for _ in range(num_frames):
|
| 167 |
+
ls = len(seq)
|
| 168 |
+
seq.extend([latent_ph] * n_latent)
|
| 169 |
+
le = len(seq)
|
| 170 |
+
frame_spans.append((ls, le))
|
| 171 |
+
seq.append(video_end)
|
| 172 |
+
seq.append(eos)
|
| 173 |
+
seq_len = len(seq)
|
| 174 |
+
input_ids = torch.tensor(seq, dtype=torch.long, device=device).unsqueeze(0)
|
| 175 |
+
|
| 176 |
+
mod = torch.zeros(1, seq_len, dtype=torch.long, device=device)
|
| 177 |
+
iim = torch.zeros(1, seq_len, dtype=torch.bool, device=device)
|
| 178 |
+
for pid in INPUT_IMAGE_PLACEHOLDER_IDS:
|
| 179 |
+
m = input_ids[0] == pid
|
| 180 |
+
mod[0, m] = 1
|
| 181 |
+
iim[0, m] = True
|
| 182 |
+
for ls, le in frame_spans:
|
| 183 |
+
mod[0, ls:le] = 2 # generation-latent route
|
| 184 |
+
|
| 185 |
+
# flow_positions / g_seqlens in frame order — must match flow_embeds concat order
|
| 186 |
+
# (model injects flow_embeds slices into flow_positions rows in order; see
|
| 187 |
+
# modeling_unified_mot.py:177-183).
|
| 188 |
+
flow_positions = torch.tensor(
|
| 189 |
+
[[ls, le] for ls, le in frame_spans], dtype=torch.int32, device=device
|
| 190 |
+
)
|
| 191 |
+
g_seqlens = flow_positions.clone()
|
| 192 |
+
latent_pos_ids = get_2d_position_ids(h_lat, w_lat, cfg.max_latent_size).to(device)
|
| 193 |
+
cu = torch.tensor([0, seq_len], dtype=torch.int32, device=device)
|
| 194 |
+
sid = torch.zeros(1, seq_len, dtype=torch.int32, device=device)
|
| 195 |
+
pos = torch.arange(seq_len, device=device).unsqueeze(0)
|
| 196 |
+
|
| 197 |
+
# ---- N latent buffers, joint Euler ODE (ONE forward/step over all N frames) ----
|
| 198 |
+
# Per-frame timestep schedule t_k(step): training noised each frame at its OWN t,
|
| 199 |
+
# so we keep a per-frame t here too. Default = lockstep (all frames share the
|
| 200 |
+
# step's t) — the validated in-distribution schedule. `frame_t_offset[k]` is the
|
| 201 |
+
# hook for a staggered "diffusion-forcing" schedule (kept 0 = lockstep).
|
| 202 |
+
xs = [torch.randn(n_latent, patch_dim, device=device, dtype=dtype) for _ in range(num_frames)]
|
| 203 |
+
ts = torch.linspace(1.0, 0.0, num_steps + 1, device=device, dtype=dtype)
|
| 204 |
+
frame_t_offset = [0.0] * num_frames # all 0 -> lockstep; set per-frame for diffusion-forcing
|
| 205 |
+
pos_emb = model.latent_pos_embed(latent_pos_ids).to(dtype) # same per-frame (ids restart at 0)
|
| 206 |
+
for i in range(num_steps):
|
| 207 |
+
dt = ts[i] - ts[i + 1]
|
| 208 |
+
fe_parts = []
|
| 209 |
+
for k in range(num_frames):
|
| 210 |
+
t_k = (ts[i] + frame_t_offset[k]).clamp(0.0, 1.0) # this frame's timestep
|
| 211 |
+
time_emb_k = model.time_embedder(t_k.expand(n_latent)).to(dtype)
|
| 212 |
+
x_proj = model.vae2llm(xs[k].to(model.vae2llm.weight.dtype)).to(dtype)
|
| 213 |
+
fe_parts.append(x_proj + time_emb_k + pos_emb)
|
| 214 |
+
flow_embeds = torch.cat(fe_parts, dim=0) # (N*n_latent, D), frame order == flow_positions
|
| 215 |
+
out = inner(
|
| 216 |
+
input_ids=input_ids, inputs_embeds=None, attention_mask=None,
|
| 217 |
+
position_ids=pos, pixel_values=pixel_values, image_grid_thw=image_grid_thw,
|
| 218 |
+
cu_seqlens=cu, sample_ids=sid, modality_mask=mod, input_image_mask=iim,
|
| 219 |
+
flow_embeds=flow_embeds, flow_positions=flow_positions, g_seqlens=g_seqlens,
|
| 220 |
+
)
|
| 221 |
+
hidden = out.hidden_states
|
| 222 |
+
for k, (ls, le) in enumerate(frame_spans):
|
| 223 |
+
v = model.llm2vae(hidden[0, ls:le]).to(dtype)
|
| 224 |
+
xs[k] = xs[k] - dt * v
|
| 225 |
+
|
| 226 |
+
# ---- decode each frame ----
|
| 227 |
+
imgs = []
|
| 228 |
+
vae_dtype = next(vae.parameters()).dtype
|
| 229 |
+
for k in range(num_frames):
|
| 230 |
+
x_lat = unpatchify_latent(xs[k].float(), h_lat, w_lat, p, cfg.vae_z_channels)
|
| 231 |
+
x_lat = x_lat.unsqueeze(0).to(device=device, dtype=vae_dtype)
|
| 232 |
+
img = vae.decode(x_lat)
|
| 233 |
+
if hasattr(img, "sample"):
|
| 234 |
+
img = img.sample
|
| 235 |
+
imgs.append(img.squeeze(0).float().clamp(-1, 1))
|
| 236 |
+
return text_str, imgs
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def vae_target_hw(img_path: str, max_size: int = 256, min_size: int = 96, stride: int = 16):
|
| 240 |
+
"""(h_px, w_px) that training's VAEImageTransform produces for this image — so the
|
| 241 |
+
generated frame's latent grid matches what the model was trained to output. Runs the
|
| 242 |
+
REAL VAEImageTransform for exact parity; returns None on failure.
|
| 243 |
+
|
| 244 |
+
Why this matters: training resized each target frame aspect-preserving to <= max_size,
|
| 245 |
+
stride-divisible — jaka/umi 848x480 -> 256x144 (144 latent tok), xtrainer 640x480 ->
|
| 246 |
+
256x192 (192 latent tok). Generating every robot at a fixed 256x144 is wrong for
|
| 247 |
+
xtrainer (4:3): wrong token count + wrong aspect, which corrupts both its output and
|
| 248 |
+
its PSNR-vs-GT comparison (GT got squished 4:3 -> 16:9).
|
| 249 |
+
"""
|
| 250 |
+
try:
|
| 251 |
+
from inference_utils import VAEImageTransform
|
| 252 |
+
t = VAEImageTransform(max_size=max_size, min_size=min_size, stride=stride)
|
| 253 |
+
tensor = t(Image.open(img_path).convert("RGB")) # (3, H, W)
|
| 254 |
+
return int(tensor.shape[1]), int(tensor.shape[2])
|
| 255 |
+
except Exception: # pylint: disable=broad-except # any failure → caller falls back
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def main():
|
| 260 |
+
ap = argparse.ArgumentParser(description="Joint fixed-N multi-frame generation.")
|
| 261 |
+
ap.add_argument("--ckpt", required=True)
|
| 262 |
+
ap.add_argument("--vae", required=True)
|
| 263 |
+
ap.add_argument("--frames", nargs="+", required=True, help="observation frame path(s)")
|
| 264 |
+
ap.add_argument("--task", required=True, help="overall task text")
|
| 265 |
+
ap.add_argument("--num_frames", type=int, default=4,
|
| 266 |
+
help="number of future frames to generate")
|
| 267 |
+
ap.add_argument("--out_dir", default="multiframe_out")
|
| 268 |
+
ap.add_argument("--height", type=int, default=None,
|
| 269 |
+
help="override auto-derived generation height (px). Default: derive "
|
| 270 |
+
"per-record from the GT/obs frame via the training VAEImageTransform "
|
| 271 |
+
"(jaka/umi 848x480 -> 144, xtrainer 640x480 -> 192).")
|
| 272 |
+
ap.add_argument("--width", type=int, default=None,
|
| 273 |
+
help="override auto-derived generation width (px). Default: derived (256).")
|
| 274 |
+
ap.add_argument("--max_image_size", type=int, default=256,
|
| 275 |
+
help="VAEImageTransform max side — MUST match training (256).")
|
| 276 |
+
ap.add_argument("--min_image_size", type=int, default=96,
|
| 277 |
+
help="VAEImageTransform min side — MUST match training (96).")
|
| 278 |
+
ap.add_argument("--image_stride", type=int, default=16,
|
| 279 |
+
help="VAEImageTransform stride — MUST match training (16).")
|
| 280 |
+
ap.add_argument("--num_steps", type=int, default=50)
|
| 281 |
+
ap.add_argument("--max_text_tokens", type=int, default=96)
|
| 282 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 283 |
+
ap.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16", "float32"])
|
| 284 |
+
ap.add_argument("--understanding_max_pixels", type=int, default=524288,
|
| 285 |
+
help="Cap obs-frame ViT input pixels to MATCH training. "
|
| 286 |
+
"Default 524288 (~494 tok/frame); the model default 4194304 (~4050 tok/frame) "
|
| 287 |
+
"is an 8x train/infer resolution mismatch that degrades eval. Set 0 to disable.")
|
| 288 |
+
args = ap.parse_args()
|
| 289 |
+
|
| 290 |
+
from model import UnifiedMoTForConditionalGeneration, maybe_init_generation_path
|
| 291 |
+
from vae_model.autoencoder import load_ae
|
| 292 |
+
|
| 293 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 294 |
+
dtype = {"bfloat16": torch.bfloat16, "float16": torch.float16, "float32": torch.float32}[args.dtype]
|
| 295 |
+
torch.manual_seed(args.seed)
|
| 296 |
+
|
| 297 |
+
# ---- resolve obs frames + task ----
|
| 298 |
+
obs, task = args.frames, args.task
|
| 299 |
+
n_frames = args.num_frames or 1
|
| 300 |
+
if not n_frames:
|
| 301 |
+
raise ValueError("num_frames resolved to 0 — pass --num_frames >= 1")
|
| 302 |
+
|
| 303 |
+
processor = HunYuanVLMoTProcessor.from_pretrained(args.ckpt, trust_remote_code=True)
|
| 304 |
+
# train==infer parity: cap obs-frame ViT pixels to the SAME value training used.
|
| 305 |
+
# Without this, inference obs frames are ~8x higher-res than what the model saw at train.
|
| 306 |
+
if args.understanding_max_pixels and args.understanding_max_pixels > 0:
|
| 307 |
+
ip = processor.image_processor
|
| 308 |
+
ip.max_pixels = args.understanding_max_pixels
|
| 309 |
+
if isinstance(getattr(ip, "size", None), dict) and "longest_edge" in ip.size:
|
| 310 |
+
ip.size["longest_edge"] = args.understanding_max_pixels
|
| 311 |
+
model = UnifiedMoTForConditionalGeneration.from_pretrained(args.ckpt, dtype=dtype)
|
| 312 |
+
maybe_init_generation_path(model, model_load_path=args.ckpt)
|
| 313 |
+
model.to(device).eval()
|
| 314 |
+
vae, _ = load_ae(args.vae)
|
| 315 |
+
vae.requires_grad_(False)
|
| 316 |
+
vae.eval()
|
| 317 |
+
vae.to(device, dtype=dtype)
|
| 318 |
+
|
| 319 |
+
os.makedirs(args.out_dir, exist_ok=True)
|
| 320 |
+
print(f"JOINT multi-frame | obs={len(obs)} frame(s) | num_frames={n_frames}")
|
| 321 |
+
print(f"TASK: {task}\n")
|
| 322 |
+
|
| 323 |
+
# ---- generation resolution: match training's VAEImageTransform per record ----
|
| 324 |
+
# Training resized each TARGET frame aspect-preserving to <=max_image_size,
|
| 325 |
+
# stride-divisible (jaka/umi -> 256x144, xtrainer -> 256x192). Generating every
|
| 326 |
+
# robot at a fixed 256x144 was wrong for xtrainer (4:3). Derive from the data;
|
| 327 |
+
# explicit --height/--width override.
|
| 328 |
+
if args.height and args.width:
|
| 329 |
+
height, width = args.height, args.width
|
| 330 |
+
else:
|
| 331 |
+
ref = obs[0] if obs else None
|
| 332 |
+
hw = (vae_target_hw(ref, args.max_image_size, args.min_image_size, args.image_stride)
|
| 333 |
+
if ref else None)
|
| 334 |
+
if hw is None:
|
| 335 |
+
height, width = 144, 256
|
| 336 |
+
print("[warn] could not derive resolution from data; falling back to 144x256")
|
| 337 |
+
else:
|
| 338 |
+
height, width = hw
|
| 339 |
+
ds = model.config.vae_image_downsample
|
| 340 |
+
print(f" gen resolution: {width}x{height} px (latent {width // ds}x{height // ds} = {(width // ds) * (height // ds)} tok/frame)")
|
| 341 |
+
|
| 342 |
+
text_str, imgs = generate_multiframe_joint(
|
| 343 |
+
model, vae, processor, obs, task, n_frames,
|
| 344 |
+
height, width, args.num_steps, device, dtype,
|
| 345 |
+
max_text_tokens=args.max_text_tokens, opening_text=None,
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
# ---- save frames + montage + result.txt ----
|
| 349 |
+
gen_paths = []
|
| 350 |
+
for k, img in enumerate(imgs):
|
| 351 |
+
arr = ((img.cpu().permute(1, 2, 0).numpy() + 1.0) * 127.5).clip(0, 255).astype("uint8")
|
| 352 |
+
path = os.path.join(args.out_dir, f"frame{k + 1}.png")
|
| 353 |
+
Image.fromarray(arr).save(path)
|
| 354 |
+
gen_paths.append(path)
|
| 355 |
+
|
| 356 |
+
lines = [f"TASK: {task}", "",
|
| 357 |
+
f"OPEN text (decoded): {text_str}"]
|
| 358 |
+
lines.append("")
|
| 359 |
+
for k in range(len(imgs)):
|
| 360 |
+
lines.append(f"frame {k + 1}")
|
| 361 |
+
txt_path = os.path.join(args.out_dir, "result.txt")
|
| 362 |
+
open(txt_path, "w").write("\n".join(lines))
|
| 363 |
+
print("\n".join(lines))
|
| 364 |
+
|
| 365 |
+
from PIL import ImageDraw
|
| 366 |
+
# montage: INPUT (obs frames) / GEN (ours),
|
| 367 |
+
# each a horizontal strip of W×H cells (W,H = the derived generation resolution),
|
| 368 |
+
# with a left label column.
|
| 369 |
+
label_w = 64
|
| 370 |
+
gap = 6
|
| 371 |
+
rows = [("INPUT", list(obs)),
|
| 372 |
+
("GEN", gen_paths)]
|
| 373 |
+
rows = [(lbl, paths) for lbl, paths in rows if paths] # drop empty
|
| 374 |
+
strips = [(lbl, _row(paths, width, height)) for lbl, paths in rows]
|
| 375 |
+
montage_w = label_w + max(s.width for _, s in strips)
|
| 376 |
+
montage_h = sum(s.height for _, s in strips) + gap * (len(strips) - 1)
|
| 377 |
+
montage = Image.new("RGB", (montage_w, montage_h), (0, 0, 0))
|
| 378 |
+
draw = ImageDraw.Draw(montage)
|
| 379 |
+
y = 0
|
| 380 |
+
for lbl, strip in strips:
|
| 381 |
+
montage.paste(strip, (label_w, y))
|
| 382 |
+
draw.text((6, y + strip.height // 2 - 4), lbl, fill=(255, 255, 255))
|
| 383 |
+
y += strip.height + gap
|
| 384 |
+
out_png = os.path.join(args.out_dir, "multiframe_input_GEN.png")
|
| 385 |
+
montage.save(out_png)
|
| 386 |
+
print(f"\nGenerated {len(imgs)} frames -> {args.out_dir}")
|
| 387 |
+
print(f" montage (INPUT/GEN): {out_png}")
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
if __name__ == "__main__":
|
| 391 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cu130
|
| 2 |
+
torch==2.10.0
|
| 3 |
+
torchvision==0.25.0
|
| 4 |
+
safetensors
|
| 5 |
+
einops
|
| 6 |
+
numpy
|
| 7 |
+
pillow
|
| 8 |
+
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt210-cu130-cp312/flash_attn-2.8.3-cp312-cp312-linux_x86_64.whl
|
| 9 |
+
git+https://github.com/huggingface/transformers@9293856c419762ebf98fbe2bd9440f9ce7069f1a
|
text2image_inference.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal text-to-image inference for the packed UnifiedMoT model.
|
| 2 |
+
|
| 3 |
+
Performs Euler-ODE sampling in the LLM hidden space:
|
| 4 |
+
x_T ~ N(0, 1)
|
| 5 |
+
for t in linspace(1, 0, num_steps + 1)[:-1]:
|
| 6 |
+
flow_embed = vae2llm(x_t) + time_embedder(t) + latent_pos_embed
|
| 7 |
+
hidden = model(... flow_embed injected at latent positions ...)
|
| 8 |
+
v_pred = llm2vae(hidden_at_latent_positions)
|
| 9 |
+
x_{t-dt} = x_t - dt * v_pred
|
| 10 |
+
|
| 11 |
+
Then VAE-decodes the final x_0 to pixels.
|
| 12 |
+
|
| 13 |
+
This script is single-sample / single-image. Classifier-free guidance (CFG)
|
| 14 |
+
is optional via ``--cfg_scale`` (>1 enables it): each ODE step runs a second
|
| 15 |
+
forward with an empty prompt, matching the 10% ``text_cond_dropout`` used in
|
| 16 |
+
training. ``--cfg_scale 1`` (default) disables CFG for the fastest path.
|
| 17 |
+
|
| 18 |
+
Run:
|
| 19 |
+
# no CFG (fastest)
|
| 20 |
+
python text2image_inference.py --ckpt /path/to/checkpoint \\
|
| 21 |
+
--prompt "a watercolor cat" \\
|
| 22 |
+
--vae /path/to/vae.safetensors \\
|
| 23 |
+
--out out.png \\
|
| 24 |
+
--height 256 --width 256 --num_steps 25
|
| 25 |
+
|
| 26 |
+
# with CFG
|
| 27 |
+
python text2image_inference.py --ckpt /path/to/checkpoint \\
|
| 28 |
+
--prompt "a watercolor cat" \\
|
| 29 |
+
--vae /path/to/vae.safetensors \\
|
| 30 |
+
--cfg_scale 5.0 --num_steps 50
|
| 31 |
+
"""
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import argparse
|
| 35 |
+
|
| 36 |
+
import torch
|
| 37 |
+
from PIL import Image
|
| 38 |
+
from transformers.models.hunyuan_vl_mot import HunYuanVLMoTProcessor
|
| 39 |
+
|
| 40 |
+
from model import (
|
| 41 |
+
UnifiedMoTConfig,
|
| 42 |
+
UnifiedMoTForConditionalGeneration,
|
| 43 |
+
maybe_init_generation_path,
|
| 44 |
+
)
|
| 45 |
+
from model.flow_matching_modules import (
|
| 46 |
+
unpatchify_latent,
|
| 47 |
+
)
|
| 48 |
+
from vae_model.autoencoder import load_ae
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def parse_args():
|
| 52 |
+
p = argparse.ArgumentParser()
|
| 53 |
+
p.add_argument("--ckpt", required=True, help="Model checkpoint directory")
|
| 54 |
+
p.add_argument("--vae", required=True, help="VAE safetensors path")
|
| 55 |
+
p.add_argument("--prompt", required=True)
|
| 56 |
+
p.add_argument("--out", default="out.png")
|
| 57 |
+
p.add_argument("--height", type=int, default=256)
|
| 58 |
+
p.add_argument("--width", type=int, default=256)
|
| 59 |
+
p.add_argument("--num_steps", type=int, default=25)
|
| 60 |
+
p.add_argument("--cfg_scale", type=float, default=1.0,
|
| 61 |
+
help="Classifier-free guidance scale. >1 enables CFG "
|
| 62 |
+
"(runs an extra empty-prompt forward per step; 2-5 typical). "
|
| 63 |
+
"1.0 disables CFG.")
|
| 64 |
+
p.add_argument("--seed", type=int, default=0)
|
| 65 |
+
p.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16", "float32"])
|
| 66 |
+
return p.parse_args()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def get_2d_position_ids(h: int, w: int, max_per_side: int) -> torch.Tensor:
|
| 70 |
+
"""2D-flattened position ids matching model.PositionEmbedding lookup table."""
|
| 71 |
+
rows = torch.arange(h)[:, None] * max_per_side
|
| 72 |
+
cols = torch.arange(w)[None, :]
|
| 73 |
+
return (rows + cols).reshape(-1).long()
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@torch.no_grad()
|
| 77 |
+
def _build_seq_meta(processor, prompt, cfg, n_latent_tokens, device):
|
| 78 |
+
"""Build the packed (1, T) input sequence + routing tensors for one prompt.
|
| 79 |
+
|
| 80 |
+
Sequence:
|
| 81 |
+
chat_template(user/assistant) + <Image> + LATENT*N + </Image> + EOS
|
| 82 |
+
Returns a dict of everything the per-step forward needs (constant across
|
| 83 |
+
ODE steps except the latent region, which the caller patches each step).
|
| 84 |
+
"""
|
| 85 |
+
eos = cfg.eos_token_id
|
| 86 |
+
prompt_messages = [
|
| 87 |
+
{"role": "user", "content": [{"type": "text", "text": prompt}]},
|
| 88 |
+
{"role": "assistant", "content": ""},
|
| 89 |
+
]
|
| 90 |
+
prompt_inputs = processor.apply_chat_template(
|
| 91 |
+
prompt_messages, return_dict=True, tokenize=True, add_generation_prompt=False,
|
| 92 |
+
)
|
| 93 |
+
prompt_ids = list(prompt_inputs["input_ids"][0])
|
| 94 |
+
# Strip trailing EOS so the assistant turn flows into <Image>
|
| 95 |
+
while prompt_ids and prompt_ids[-1] == eos:
|
| 96 |
+
prompt_ids.pop()
|
| 97 |
+
|
| 98 |
+
latent_ph = cfg.flow_latent_placeholder_id # upstream latent_token_id
|
| 99 |
+
image_start = cfg.image_start_token_id
|
| 100 |
+
image_end = cfg.image_end_token_id
|
| 101 |
+
|
| 102 |
+
seq = (
|
| 103 |
+
prompt_ids
|
| 104 |
+
+ [image_start]
|
| 105 |
+
+ [latent_ph] * n_latent_tokens
|
| 106 |
+
+ [image_end]
|
| 107 |
+
+ [eos]
|
| 108 |
+
)
|
| 109 |
+
input_ids = torch.tensor(seq, dtype=torch.long, device=device).unsqueeze(0) # (1, T)
|
| 110 |
+
seq_len = input_ids.shape[1]
|
| 111 |
+
latent_start = len(prompt_ids) + 1 # right after <Image>
|
| 112 |
+
latent_end = latent_start + n_latent_tokens
|
| 113 |
+
|
| 114 |
+
modality_mask = torch.zeros(1, seq_len, dtype=torch.long, device=device)
|
| 115 |
+
modality_mask[0, latent_start:latent_end] = 2 # gen-latent route
|
| 116 |
+
flow_positions = torch.tensor([[latent_start, latent_end]], dtype=torch.int32, device=device)
|
| 117 |
+
g_seqlens = flow_positions.clone()
|
| 118 |
+
# Single sample → packed degenerate: still feed cu_seqlens/sample_ids so the
|
| 119 |
+
# same attention path runs as during training.
|
| 120 |
+
cu_seqlens = torch.tensor([0, seq_len], dtype=torch.int32, device=device)
|
| 121 |
+
sample_ids = torch.zeros(1, seq_len, dtype=torch.int32, device=device)
|
| 122 |
+
position_ids = torch.arange(seq_len, dtype=torch.long, device=device).unsqueeze(0)
|
| 123 |
+
return {
|
| 124 |
+
"input_ids": input_ids, "T": seq_len,
|
| 125 |
+
"latent_start": latent_start, "latent_end": latent_end,
|
| 126 |
+
"modality_mask": modality_mask, "g_seqlens": g_seqlens,
|
| 127 |
+
"cu_seqlens": cu_seqlens, "sample_ids": sample_ids,
|
| 128 |
+
"position_ids": position_ids,
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@torch.no_grad()
|
| 133 |
+
def _forward_v(model, inner, base_embeds, flow_embed, meta, dtype):
|
| 134 |
+
"""Run one forward with `flow_embed` injected at the latent span; return velocity."""
|
| 135 |
+
inputs_embeds = base_embeds.clone()
|
| 136 |
+
inputs_embeds[0, meta["latent_start"]:meta["latent_end"]] = flow_embed
|
| 137 |
+
out = inner(
|
| 138 |
+
input_ids=None,
|
| 139 |
+
inputs_embeds=inputs_embeds,
|
| 140 |
+
attention_mask=None,
|
| 141 |
+
position_ids=meta["position_ids"],
|
| 142 |
+
cu_seqlens=meta["cu_seqlens"],
|
| 143 |
+
sample_ids=meta["sample_ids"],
|
| 144 |
+
modality_mask=meta["modality_mask"],
|
| 145 |
+
input_image_mask=torch.zeros(1, meta["T"], dtype=torch.bool, device=inputs_embeds.device),
|
| 146 |
+
flow_embeds=None, # we already pre-built inputs_embeds
|
| 147 |
+
flow_positions=None,
|
| 148 |
+
g_seqlens=meta["g_seqlens"],
|
| 149 |
+
)
|
| 150 |
+
# FM velocity target during training is v = noise - x_0
|
| 151 |
+
return model.llm2vae(out.hidden_states[0, meta["latent_start"]:meta["latent_end"]]).to(dtype)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@torch.no_grad()
|
| 155 |
+
def generate_image(
|
| 156 |
+
model: UnifiedMoTForConditionalGeneration,
|
| 157 |
+
vae,
|
| 158 |
+
processor: HunYuanVLMoTProcessor,
|
| 159 |
+
prompt: str,
|
| 160 |
+
height: int,
|
| 161 |
+
width: int,
|
| 162 |
+
num_steps: int,
|
| 163 |
+
device,
|
| 164 |
+
dtype,
|
| 165 |
+
cfg_scale: float = 1.0,
|
| 166 |
+
):
|
| 167 |
+
"""T2I single-sample sampling (optional CFG). Returns a (3, H, W) tensor in [-1, 1].
|
| 168 |
+
|
| 169 |
+
With ``cfg_scale > 1`` each ODE step runs cond + uncond forwards and combines
|
| 170 |
+
v = v_uncond + cfg_scale * (v_cond - v_uncond)
|
| 171 |
+
The uncond branch uses an empty prompt, matching text_cond_dropout=0.1 in
|
| 172 |
+
training where 10% of samples have the caption replaced with "".
|
| 173 |
+
"""
|
| 174 |
+
cfg: UnifiedMoTConfig = model.config
|
| 175 |
+
p = model.latent_patch_size
|
| 176 |
+
downsample = cfg.vae_image_downsample # pixel → patch-token (e.g. 16 = VAE(8) * patch(2))
|
| 177 |
+
h_lat = height // downsample
|
| 178 |
+
w_lat = width // downsample
|
| 179 |
+
n_latent_tokens = h_lat * w_lat
|
| 180 |
+
|
| 181 |
+
do_cfg = (cfg_scale != 1.0)
|
| 182 |
+
embed_layer = model.get_input_embeddings()
|
| 183 |
+
|
| 184 |
+
cond_meta = _build_seq_meta(processor, prompt, cfg, n_latent_tokens, device)
|
| 185 |
+
cond_base = embed_layer(cond_meta["input_ids"])
|
| 186 |
+
if do_cfg:
|
| 187 |
+
uncond_meta = _build_seq_meta(processor, "", cfg, n_latent_tokens, device)
|
| 188 |
+
uncond_base = embed_layer(uncond_meta["input_ids"])
|
| 189 |
+
|
| 190 |
+
# 2D position ids for latent_pos_embed lookup
|
| 191 |
+
latent_pos_ids = get_2d_position_ids(h_lat, w_lat, cfg.max_latent_size).to(device)
|
| 192 |
+
|
| 193 |
+
# Initial noise x_T
|
| 194 |
+
patch_latent_dim = p * p * cfg.vae_z_channels
|
| 195 |
+
x = torch.randn(n_latent_tokens, patch_latent_dim, device=device, dtype=dtype)
|
| 196 |
+
|
| 197 |
+
# Euler ODE: t from 1.0 → 0.0 in `num_steps`
|
| 198 |
+
ts = torch.linspace(1.0, 0.0, num_steps + 1, device=device, dtype=dtype)
|
| 199 |
+
inner = model.model # UnifiedMoTModel
|
| 200 |
+
|
| 201 |
+
for i in range(num_steps):
|
| 202 |
+
t = ts[i]
|
| 203 |
+
dt = ts[i] - ts[i + 1] # positive
|
| 204 |
+
|
| 205 |
+
# Build flow_embed for the current x_t (shared by cond & uncond branches)
|
| 206 |
+
time_emb = model.time_embedder(t.expand(n_latent_tokens)).to(dtype)
|
| 207 |
+
x_proj = model.vae2llm(x.to(model.vae2llm.weight.dtype)).to(dtype)
|
| 208 |
+
pos_emb = model.latent_pos_embed(latent_pos_ids).to(dtype)
|
| 209 |
+
flow_embed = x_proj + time_emb + pos_emb # (n_latent_tokens, D)
|
| 210 |
+
|
| 211 |
+
v_cond = _forward_v(model, inner, cond_base, flow_embed, cond_meta, dtype)
|
| 212 |
+
if do_cfg:
|
| 213 |
+
v_uncond = _forward_v(model, inner, uncond_base, flow_embed, uncond_meta, dtype)
|
| 214 |
+
v = v_uncond + cfg_scale * (v_cond - v_uncond)
|
| 215 |
+
else:
|
| 216 |
+
v = v_cond
|
| 217 |
+
# Euler step: x_{t-dt} = x_t - dt * v
|
| 218 |
+
x = x - dt * v
|
| 219 |
+
|
| 220 |
+
# Unpatchify + VAE decode
|
| 221 |
+
x_lat = unpatchify_latent(x.float(), h_lat, w_lat, p, cfg.vae_z_channels) # (C, H_lat, W_lat)
|
| 222 |
+
vae_dtype = next(vae.parameters()).dtype
|
| 223 |
+
x_lat = x_lat.unsqueeze(0).to(device=device, dtype=vae_dtype)
|
| 224 |
+
img = vae.decode(x_lat)
|
| 225 |
+
if hasattr(img, "sample"):
|
| 226 |
+
img = img.sample
|
| 227 |
+
return img.squeeze(0).float().clamp(-1, 1)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def main():
|
| 231 |
+
args = parse_args()
|
| 232 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 233 |
+
dtype = {"bfloat16": torch.bfloat16, "float16": torch.float16, "float32": torch.float32}[args.dtype]
|
| 234 |
+
torch.manual_seed(args.seed)
|
| 235 |
+
|
| 236 |
+
print(f"Loading processor from {args.ckpt}...")
|
| 237 |
+
processor = HunYuanVLMoTProcessor.from_pretrained(args.ckpt, trust_remote_code=True)
|
| 238 |
+
|
| 239 |
+
print(f"Loading model from {args.ckpt}...")
|
| 240 |
+
model = UnifiedMoTForConditionalGeneration.from_pretrained(args.ckpt, dtype=dtype)
|
| 241 |
+
|
| 242 |
+
maybe_init_generation_path(model, model_load_path=args.ckpt)
|
| 243 |
+
model.to(device)
|
| 244 |
+
model.eval()
|
| 245 |
+
|
| 246 |
+
print(f"Loading VAE from {args.vae}...")
|
| 247 |
+
vae, _ = load_ae(args.vae)
|
| 248 |
+
vae.requires_grad_(False)
|
| 249 |
+
vae.eval()
|
| 250 |
+
vae.to(device, dtype=dtype)
|
| 251 |
+
|
| 252 |
+
cfg_note = f", CFG {args.cfg_scale}" if args.cfg_scale != 1.0 else " (no CFG)"
|
| 253 |
+
print(f"Generating: '{args.prompt}' @ {args.width}x{args.height}, {args.num_steps} ODE steps{cfg_note}")
|
| 254 |
+
img = generate_image(
|
| 255 |
+
model, vae, processor, args.prompt,
|
| 256 |
+
height=args.height, width=args.width, num_steps=args.num_steps,
|
| 257 |
+
device=device, dtype=dtype, cfg_scale=args.cfg_scale,
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
# Save: (C, H, W) in [-1, 1] → uint8 PNG
|
| 261 |
+
arr = ((img.cpu().permute(1, 2, 0).numpy() + 1.0) * 127.5).clip(0, 255).astype("uint8")
|
| 262 |
+
Image.fromarray(arr).save(args.out)
|
| 263 |
+
print(f"Saved → {args.out}")
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
if __name__ == "__main__":
|
| 267 |
+
main()
|
vae_model/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .autoencoder import AutoEncoder
|
vae_model/autoencoder.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2024 Black Forest Labs.
|
| 2 |
+
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates.
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
#
|
| 5 |
+
# This file has been modified by ByteDance Ltd. and/or its affiliates. on 2025-05-20.
|
| 6 |
+
#
|
| 7 |
+
# Original file was released under Apache-2.0, with the full license text
|
| 8 |
+
# available at https://github.com/black-forest-labs/flux/blob/main/LICENSE.
|
| 9 |
+
#
|
| 10 |
+
# This modified file is released under the same license.
|
| 11 |
+
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
from einops import rearrange
|
| 16 |
+
from torch import Tensor, nn
|
| 17 |
+
from safetensors.torch import load_file as load_sft
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class AutoEncoderParams:
|
| 22 |
+
resolution: int
|
| 23 |
+
in_channels: int
|
| 24 |
+
downsample: int
|
| 25 |
+
ch: int
|
| 26 |
+
out_ch: int
|
| 27 |
+
ch_mult: list[int]
|
| 28 |
+
num_res_blocks: int
|
| 29 |
+
z_channels: int
|
| 30 |
+
scale_factor: float
|
| 31 |
+
shift_factor: float
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def swish(x: Tensor) -> Tensor:
|
| 35 |
+
return x * torch.sigmoid(x)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class AttnBlock(nn.Module):
|
| 39 |
+
def __init__(self, in_channels: int):
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.in_channels = in_channels
|
| 42 |
+
|
| 43 |
+
self.norm = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
|
| 44 |
+
|
| 45 |
+
self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 46 |
+
self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 47 |
+
self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 48 |
+
self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 49 |
+
|
| 50 |
+
def attention(self, h_: Tensor) -> Tensor:
|
| 51 |
+
h_ = self.norm(h_)
|
| 52 |
+
q = self.q(h_)
|
| 53 |
+
k = self.k(h_)
|
| 54 |
+
v = self.v(h_)
|
| 55 |
+
|
| 56 |
+
b, c, h, w = q.shape
|
| 57 |
+
q = rearrange(q, "b c h w -> b 1 (h w) c").contiguous()
|
| 58 |
+
k = rearrange(k, "b c h w -> b 1 (h w) c").contiguous()
|
| 59 |
+
v = rearrange(v, "b c h w -> b 1 (h w) c").contiguous()
|
| 60 |
+
h_ = nn.functional.scaled_dot_product_attention(q, k, v)
|
| 61 |
+
|
| 62 |
+
return rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
|
| 63 |
+
|
| 64 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 65 |
+
return x + self.proj_out(self.attention(x))
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class ResnetBlock(nn.Module):
|
| 69 |
+
def __init__(self, in_channels: int, out_channels: int):
|
| 70 |
+
super().__init__()
|
| 71 |
+
self.in_channels = in_channels
|
| 72 |
+
out_channels = in_channels if out_channels is None else out_channels
|
| 73 |
+
self.out_channels = out_channels
|
| 74 |
+
|
| 75 |
+
self.norm1 = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
|
| 76 |
+
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
|
| 77 |
+
self.norm2 = nn.GroupNorm(num_groups=32, num_channels=out_channels, eps=1e-6, affine=True)
|
| 78 |
+
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
|
| 79 |
+
if self.in_channels != self.out_channels:
|
| 80 |
+
self.nin_shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
|
| 81 |
+
|
| 82 |
+
def forward(self, x):
|
| 83 |
+
h = x
|
| 84 |
+
h = self.norm1(h)
|
| 85 |
+
h = swish(h)
|
| 86 |
+
h = self.conv1(h)
|
| 87 |
+
|
| 88 |
+
h = self.norm2(h)
|
| 89 |
+
h = swish(h)
|
| 90 |
+
h = self.conv2(h)
|
| 91 |
+
|
| 92 |
+
if self.in_channels != self.out_channels:
|
| 93 |
+
x = self.nin_shortcut(x)
|
| 94 |
+
|
| 95 |
+
return x + h
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class Downsample(nn.Module):
|
| 99 |
+
def __init__(self, in_channels: int):
|
| 100 |
+
super().__init__()
|
| 101 |
+
# no asymmetric padding in torch conv, must do it ourselves
|
| 102 |
+
self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0)
|
| 103 |
+
|
| 104 |
+
def forward(self, x: Tensor):
|
| 105 |
+
pad = (0, 1, 0, 1)
|
| 106 |
+
x = nn.functional.pad(x, pad, mode="constant", value=0)
|
| 107 |
+
x = self.conv(x)
|
| 108 |
+
return x
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class Upsample(nn.Module):
|
| 112 |
+
def __init__(self, in_channels: int):
|
| 113 |
+
super().__init__()
|
| 114 |
+
self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1)
|
| 115 |
+
|
| 116 |
+
def forward(self, x: Tensor):
|
| 117 |
+
x = nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
|
| 118 |
+
x = self.conv(x)
|
| 119 |
+
return x
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class Encoder(nn.Module):
|
| 123 |
+
def __init__(
|
| 124 |
+
self,
|
| 125 |
+
resolution: int,
|
| 126 |
+
in_channels: int,
|
| 127 |
+
ch: int,
|
| 128 |
+
ch_mult: list[int],
|
| 129 |
+
num_res_blocks: int,
|
| 130 |
+
z_channels: int,
|
| 131 |
+
):
|
| 132 |
+
super().__init__()
|
| 133 |
+
self.ch = ch
|
| 134 |
+
self.num_resolutions = len(ch_mult)
|
| 135 |
+
self.num_res_blocks = num_res_blocks
|
| 136 |
+
self.resolution = resolution
|
| 137 |
+
self.in_channels = in_channels
|
| 138 |
+
# downsampling
|
| 139 |
+
self.conv_in = nn.Conv2d(in_channels, self.ch, kernel_size=3, stride=1, padding=1)
|
| 140 |
+
|
| 141 |
+
curr_res = resolution
|
| 142 |
+
in_ch_mult = (1,) + tuple(ch_mult)
|
| 143 |
+
self.in_ch_mult = in_ch_mult
|
| 144 |
+
self.down = nn.ModuleList()
|
| 145 |
+
block_in = self.ch
|
| 146 |
+
for i_level in range(self.num_resolutions):
|
| 147 |
+
block = nn.ModuleList()
|
| 148 |
+
attn = nn.ModuleList()
|
| 149 |
+
block_in = ch * in_ch_mult[i_level]
|
| 150 |
+
block_out = ch * ch_mult[i_level]
|
| 151 |
+
for _ in range(self.num_res_blocks):
|
| 152 |
+
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
|
| 153 |
+
block_in = block_out
|
| 154 |
+
down = nn.Module()
|
| 155 |
+
down.block = block
|
| 156 |
+
down.attn = attn
|
| 157 |
+
if i_level != self.num_resolutions - 1:
|
| 158 |
+
down.downsample = Downsample(block_in)
|
| 159 |
+
curr_res = curr_res // 2
|
| 160 |
+
self.down.append(down)
|
| 161 |
+
|
| 162 |
+
# middle
|
| 163 |
+
self.mid = nn.Module()
|
| 164 |
+
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 165 |
+
self.mid.attn_1 = AttnBlock(block_in)
|
| 166 |
+
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 167 |
+
|
| 168 |
+
# end
|
| 169 |
+
self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
|
| 170 |
+
self.conv_out = nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1)
|
| 171 |
+
|
| 172 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 173 |
+
# downsampling
|
| 174 |
+
hs = [self.conv_in(x)]
|
| 175 |
+
for i_level in range(self.num_resolutions):
|
| 176 |
+
for i_block in range(self.num_res_blocks):
|
| 177 |
+
h = self.down[i_level].block[i_block](hs[-1])
|
| 178 |
+
if len(self.down[i_level].attn) > 0:
|
| 179 |
+
h = self.down[i_level].attn[i_block](h)
|
| 180 |
+
hs.append(h)
|
| 181 |
+
if i_level != self.num_resolutions - 1:
|
| 182 |
+
hs.append(self.down[i_level].downsample(hs[-1]))
|
| 183 |
+
|
| 184 |
+
# middle
|
| 185 |
+
h = hs[-1]
|
| 186 |
+
h = self.mid.block_1(h)
|
| 187 |
+
h = self.mid.attn_1(h)
|
| 188 |
+
h = self.mid.block_2(h)
|
| 189 |
+
# end
|
| 190 |
+
h = self.norm_out(h)
|
| 191 |
+
h = swish(h)
|
| 192 |
+
h = self.conv_out(h)
|
| 193 |
+
return h
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
class Decoder(nn.Module):
|
| 197 |
+
def __init__(
|
| 198 |
+
self,
|
| 199 |
+
ch: int,
|
| 200 |
+
out_ch: int,
|
| 201 |
+
ch_mult: list[int],
|
| 202 |
+
num_res_blocks: int,
|
| 203 |
+
in_channels: int,
|
| 204 |
+
resolution: int,
|
| 205 |
+
z_channels: int,
|
| 206 |
+
):
|
| 207 |
+
super().__init__()
|
| 208 |
+
self.ch = ch
|
| 209 |
+
self.num_resolutions = len(ch_mult)
|
| 210 |
+
self.num_res_blocks = num_res_blocks
|
| 211 |
+
self.resolution = resolution
|
| 212 |
+
self.in_channels = in_channels
|
| 213 |
+
self.ffactor = 2 ** (self.num_resolutions - 1)
|
| 214 |
+
|
| 215 |
+
# compute in_ch_mult, block_in and curr_res at lowest res
|
| 216 |
+
block_in = ch * ch_mult[self.num_resolutions - 1]
|
| 217 |
+
curr_res = resolution // 2 ** (self.num_resolutions - 1)
|
| 218 |
+
self.z_shape = (1, z_channels, curr_res, curr_res)
|
| 219 |
+
|
| 220 |
+
# z to block_in
|
| 221 |
+
self.conv_in = nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1)
|
| 222 |
+
|
| 223 |
+
# middle
|
| 224 |
+
self.mid = nn.Module()
|
| 225 |
+
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 226 |
+
self.mid.attn_1 = AttnBlock(block_in)
|
| 227 |
+
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 228 |
+
|
| 229 |
+
# upsampling
|
| 230 |
+
self.up = nn.ModuleList()
|
| 231 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 232 |
+
block = nn.ModuleList()
|
| 233 |
+
attn = nn.ModuleList()
|
| 234 |
+
block_out = ch * ch_mult[i_level]
|
| 235 |
+
for _ in range(self.num_res_blocks + 1):
|
| 236 |
+
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
|
| 237 |
+
block_in = block_out
|
| 238 |
+
up = nn.Module()
|
| 239 |
+
up.block = block
|
| 240 |
+
up.attn = attn
|
| 241 |
+
if i_level != 0:
|
| 242 |
+
up.upsample = Upsample(block_in)
|
| 243 |
+
curr_res = curr_res * 2
|
| 244 |
+
self.up.insert(0, up) # prepend to get consistent order
|
| 245 |
+
|
| 246 |
+
# end
|
| 247 |
+
self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
|
| 248 |
+
self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
|
| 249 |
+
|
| 250 |
+
def forward(self, z: Tensor) -> Tensor:
|
| 251 |
+
# z to block_in
|
| 252 |
+
h = self.conv_in(z)
|
| 253 |
+
|
| 254 |
+
# middle
|
| 255 |
+
h = self.mid.block_1(h)
|
| 256 |
+
h = self.mid.attn_1(h)
|
| 257 |
+
h = self.mid.block_2(h)
|
| 258 |
+
|
| 259 |
+
# upsampling
|
| 260 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 261 |
+
for i_block in range(self.num_res_blocks + 1):
|
| 262 |
+
h = self.up[i_level].block[i_block](h)
|
| 263 |
+
if len(self.up[i_level].attn) > 0:
|
| 264 |
+
h = self.up[i_level].attn[i_block](h)
|
| 265 |
+
if i_level != 0:
|
| 266 |
+
h = self.up[i_level].upsample(h)
|
| 267 |
+
|
| 268 |
+
# end
|
| 269 |
+
h = self.norm_out(h)
|
| 270 |
+
h = swish(h)
|
| 271 |
+
h = self.conv_out(h)
|
| 272 |
+
return h
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
class DiagonalGaussian(nn.Module):
|
| 276 |
+
def __init__(self, sample: bool = True, chunk_dim: int = 1):
|
| 277 |
+
super().__init__()
|
| 278 |
+
self.sample = sample
|
| 279 |
+
self.chunk_dim = chunk_dim
|
| 280 |
+
|
| 281 |
+
def forward(self, z: Tensor) -> Tensor:
|
| 282 |
+
mean, logvar = torch.chunk(z, 2, dim=self.chunk_dim)
|
| 283 |
+
if self.sample:
|
| 284 |
+
std = torch.exp(0.5 * logvar)
|
| 285 |
+
return mean + std * torch.randn_like(mean)
|
| 286 |
+
else:
|
| 287 |
+
return mean
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
class AutoEncoder(nn.Module):
|
| 291 |
+
def __init__(self, params: AutoEncoderParams):
|
| 292 |
+
super().__init__()
|
| 293 |
+
self.encoder = Encoder(
|
| 294 |
+
resolution=params.resolution,
|
| 295 |
+
in_channels=params.in_channels,
|
| 296 |
+
ch=params.ch,
|
| 297 |
+
ch_mult=params.ch_mult,
|
| 298 |
+
num_res_blocks=params.num_res_blocks,
|
| 299 |
+
z_channels=params.z_channels,
|
| 300 |
+
)
|
| 301 |
+
self.decoder = Decoder(
|
| 302 |
+
resolution=params.resolution,
|
| 303 |
+
in_channels=params.in_channels,
|
| 304 |
+
ch=params.ch,
|
| 305 |
+
out_ch=params.out_ch,
|
| 306 |
+
ch_mult=params.ch_mult,
|
| 307 |
+
num_res_blocks=params.num_res_blocks,
|
| 308 |
+
z_channels=params.z_channels,
|
| 309 |
+
)
|
| 310 |
+
self.reg = DiagonalGaussian()
|
| 311 |
+
|
| 312 |
+
self.scale_factor = params.scale_factor
|
| 313 |
+
self.shift_factor = params.shift_factor
|
| 314 |
+
|
| 315 |
+
def encode(self, x: Tensor) -> Tensor:
|
| 316 |
+
z = self.reg(self.encoder(x))
|
| 317 |
+
z = self.scale_factor * (z - self.shift_factor)
|
| 318 |
+
return z
|
| 319 |
+
|
| 320 |
+
def decode(self, z: Tensor) -> Tensor:
|
| 321 |
+
z = z / self.scale_factor + self.shift_factor
|
| 322 |
+
return self.decoder(z)
|
| 323 |
+
|
| 324 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 325 |
+
return self.decode(self.encode(x))
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def print_load_warning(missing: list[str], unexpected: list[str]) -> None:
|
| 329 |
+
if len(missing) > 0 and len(unexpected) > 0:
|
| 330 |
+
print(f"Got {len(missing)} missing keys:\n\t" + "\n\t".join(missing))
|
| 331 |
+
print("\n" + "-" * 79 + "\n")
|
| 332 |
+
print(f"Got {len(unexpected)} unexpected keys:\n\t" + "\n\t".join(unexpected))
|
| 333 |
+
elif len(missing) > 0:
|
| 334 |
+
print(f"Got {len(missing)} missing keys:\n\t" + "\n\t".join(missing))
|
| 335 |
+
elif len(unexpected) > 0:
|
| 336 |
+
print(f"Got {len(unexpected)} unexpected keys:\n\t" + "\n\t".join(unexpected))
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def load_ae(local_path: str) -> AutoEncoder:
|
| 340 |
+
ae_params = AutoEncoderParams(
|
| 341 |
+
resolution=256,
|
| 342 |
+
in_channels=3,
|
| 343 |
+
downsample=8,
|
| 344 |
+
ch=128,
|
| 345 |
+
out_ch=3,
|
| 346 |
+
ch_mult=[1, 2, 4, 4],
|
| 347 |
+
num_res_blocks=2,
|
| 348 |
+
z_channels=16,
|
| 349 |
+
scale_factor=0.3611,
|
| 350 |
+
shift_factor=0.1159,
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
# Loading the autoencoder
|
| 354 |
+
ae = AutoEncoder(ae_params)
|
| 355 |
+
|
| 356 |
+
if local_path is not None:
|
| 357 |
+
sd = load_sft(local_path)
|
| 358 |
+
missing, unexpected = ae.load_state_dict(sd, strict=False, assign=True)
|
| 359 |
+
print_load_warning(missing, unexpected)
|
| 360 |
+
return ae, ae_params
|
vqa_inference.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal VQA / text-understanding inference for RxBrain (UnifiedMoT).
|
| 2 |
+
|
| 3 |
+
Pure autoregressive text decoding: (image(s) + question) -> answer text.
|
| 4 |
+
No VAE / flow-matching needed. Reuses the SAME prompt construction
|
| 5 |
+
(`build_conditioned_sequence`) and KV-cached decode plumbing the interleaved
|
| 6 |
+
planner uses for its text step, but decodes all the way to EOS instead of
|
| 7 |
+
stopping at the <Image> modality-transition token.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
from typing import List
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
from transformers.models.hunyuan_vl_mot import HunYuanVLMoTProcessor
|
| 16 |
+
from transformers.cache_utils import DynamicCache
|
| 17 |
+
|
| 18 |
+
from model import UnifiedMoTForConditionalGeneration, maybe_init_generation_path
|
| 19 |
+
from interleave_inference import build_conditioned_sequence, INPUT_IMAGE_PLACEHOLDER_IDS
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@torch.no_grad()
|
| 23 |
+
def answer(model, processor, image_paths: List[str], question: str,
|
| 24 |
+
device, dtype, max_new_tokens: int = 256) -> str:
|
| 25 |
+
cfg = model.config
|
| 26 |
+
eos = cfg.eos_token_id
|
| 27 |
+
inner = model.model
|
| 28 |
+
|
| 29 |
+
# No generation task-instruction: this is understanding, not image gen.
|
| 30 |
+
prompt_ids, proc = build_conditioned_sequence(processor, image_paths, question)
|
| 31 |
+
|
| 32 |
+
pixel_values = proc.get("pixel_values")
|
| 33 |
+
image_grid_thw = proc.get("image_grid_thw")
|
| 34 |
+
if pixel_values is not None:
|
| 35 |
+
pixel_values = pixel_values.to(device=device, dtype=dtype)
|
| 36 |
+
if image_grid_thw is not None:
|
| 37 |
+
image_grid_thw = image_grid_thw.to(device=device)
|
| 38 |
+
|
| 39 |
+
def _dmask(ids):
|
| 40 |
+
n = len(ids)
|
| 41 |
+
inp = torch.tensor(ids, dtype=torch.long, device=device).unsqueeze(0)
|
| 42 |
+
mod = torch.zeros(1, n, dtype=torch.long, device=device)
|
| 43 |
+
iim = torch.zeros(1, n, dtype=torch.bool, device=device)
|
| 44 |
+
for pid in INPUT_IMAGE_PLACEHOLDER_IDS:
|
| 45 |
+
m = inp[0] == pid
|
| 46 |
+
mod[0, m] = 1
|
| 47 |
+
iim[0, m] = True
|
| 48 |
+
return inp, mod, iim
|
| 49 |
+
|
| 50 |
+
_empty_g = torch.zeros((0, 2), dtype=torch.int32, device=device)
|
| 51 |
+
pkv = DynamicCache()
|
| 52 |
+
prompt_len = len(prompt_ids)
|
| 53 |
+
inp, mod, iim = _dmask(list(prompt_ids))
|
| 54 |
+
out = inner(input_ids=inp, position_ids=torch.arange(prompt_len, device=device).unsqueeze(0),
|
| 55 |
+
pixel_values=pixel_values, image_grid_thw=image_grid_thw,
|
| 56 |
+
cu_seqlens=torch.tensor([0, prompt_len], dtype=torch.int32, device=device),
|
| 57 |
+
sample_ids=torch.zeros(1, prompt_len, dtype=torch.int32, device=device),
|
| 58 |
+
modality_mask=mod, input_image_mask=iim,
|
| 59 |
+
flow_embeds=None, flow_positions=None, g_seqlens=_empty_g,
|
| 60 |
+
use_cache=True, past_key_values=pkv,
|
| 61 |
+
cache_position=torch.arange(prompt_len, device=device))
|
| 62 |
+
pkv = out.past_key_values
|
| 63 |
+
nxt = int(out.logits[0, -1].argmax().item())
|
| 64 |
+
cur = prompt_len
|
| 65 |
+
text_ids = []
|
| 66 |
+
for _ in range(max_new_tokens):
|
| 67 |
+
if nxt == eos:
|
| 68 |
+
break
|
| 69 |
+
text_ids.append(nxt)
|
| 70 |
+
out = inner(input_ids=torch.tensor([[nxt]], dtype=torch.long, device=device),
|
| 71 |
+
position_ids=torch.tensor([[cur]], device=device),
|
| 72 |
+
pixel_values=None, image_grid_thw=None,
|
| 73 |
+
cu_seqlens=torch.tensor([0, cur + 1], dtype=torch.int32, device=device),
|
| 74 |
+
sample_ids=torch.zeros(1, 1, dtype=torch.int32, device=device),
|
| 75 |
+
modality_mask=torch.zeros(1, 1, dtype=torch.long, device=device),
|
| 76 |
+
input_image_mask=torch.zeros(1, 1, dtype=torch.bool, device=device),
|
| 77 |
+
flow_embeds=None, flow_positions=None, g_seqlens=_empty_g,
|
| 78 |
+
use_cache=True, past_key_values=pkv,
|
| 79 |
+
cache_position=torch.tensor([cur], device=device))
|
| 80 |
+
pkv = out.past_key_values
|
| 81 |
+
nxt = int(out.logits[0, -1].argmax().item())
|
| 82 |
+
cur += 1
|
| 83 |
+
return processor.tokenizer.decode(text_ids, skip_special_tokens=True)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def main():
|
| 87 |
+
ap = argparse.ArgumentParser()
|
| 88 |
+
ap.add_argument("--ckpt", required=True)
|
| 89 |
+
ap.add_argument("--images", nargs="+", required=True)
|
| 90 |
+
ap.add_argument("--question", required=True)
|
| 91 |
+
ap.add_argument("--max_new_tokens", type=int, default=256)
|
| 92 |
+
ap.add_argument("--dtype", choices=["bfloat16", "float16", "float32"], default="bfloat16")
|
| 93 |
+
args = ap.parse_args()
|
| 94 |
+
|
| 95 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 96 |
+
dtype = getattr(torch, args.dtype)
|
| 97 |
+
|
| 98 |
+
print(f"Loading processor from {args.ckpt}...")
|
| 99 |
+
processor = HunYuanVLMoTProcessor.from_pretrained(args.ckpt, trust_remote_code=True)
|
| 100 |
+
print(f"Loading model from {args.ckpt}...")
|
| 101 |
+
model = UnifiedMoTForConditionalGeneration.from_pretrained(args.ckpt, dtype=dtype)
|
| 102 |
+
maybe_init_generation_path(model, model_load_path=args.ckpt)
|
| 103 |
+
model.to(device)
|
| 104 |
+
model.eval()
|
| 105 |
+
|
| 106 |
+
print(f"\nIMAGES: {args.images}")
|
| 107 |
+
print(f"Q: {args.question}")
|
| 108 |
+
ans = answer(model, processor, args.images, args.question, device, dtype, args.max_new_tokens)
|
| 109 |
+
print(f"A: {ans}")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
main()
|