mrfakename commited on
Commit
1ec0d76
·
verified ·
1 Parent(s): bafcbbc

Initial Space demo

Browse files
.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/cats.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/street.jpg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ *.pyc
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md CHANGED
@@ -1,13 +1,19 @@
1
  ---
2
  title: PixelUMM
3
- emoji: 🌖
4
- colorFrom: gray
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.29.1
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
  title: PixelUMM
3
+ emoji: 🎨
4
+ colorFrom: green
5
+ colorTo: gray
6
  sdk: gradio
7
+ sdk_version: "5.50.0"
 
8
  app_file: app.py
9
  pinned: false
10
+ license: other
11
+ short_description: Encoder-free unified image/video understanding + generation
12
+ models:
13
+ - nvidia/PixelUMM
14
+ - Qwen/Qwen3-8B
15
  ---
16
 
17
+ Gradio ZeroGPU demo for [nvidia/PixelUMM](https://huggingface.co/nvidia/PixelUMM): text-to-image, text-to-video, image understanding and video understanding.
18
+
19
+ Model code vendored from [nv-tlabs/PixelUMM](https://github.com/nv-tlabs/PixelUMM) (Apache-2.0; `modeling/pixelumm/modeling_utils.py` keeps its CC BY-NC 4.0 notice), with FlashAttention varlen replaced by a PyTorch SDPA fallback. Model weights are under the NVIDIA One-Way Noncommercial License.
app.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import tempfile
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import spaces # must be imported before torch on ZeroGPU
8
+ import gradio as gr
9
+ import numpy as np
10
+ import torch
11
+ import torchvision.transforms.functional as transforms_f
12
+ from huggingface_hub import snapshot_download
13
+ from PIL import Image
14
+
15
+ from data.pixelumm_smart_resize import smart_resize_pixels
16
+ from data.pixelumm_video_decoder import decode_pixelumm_video
17
+ from eval.vlm.release_preprocess_contract import IMAGE_VLM_CONTRACT, VIDEO_VLM_CONTRACT
18
+ from train.eval_utils import (
19
+ _generate_t2i_one,
20
+ _generate_t2v_one,
21
+ _generate_v2t_one,
22
+ generate_vlm_interleaved,
23
+ save_video_mp4,
24
+ )
25
+ from train.model_factory import build_pixelumm_model
26
+ from train.release_checkpoint import (
27
+ DEFAULT_RELEASE_CONFIG,
28
+ load_checkpoint_weights,
29
+ parse_release_arguments,
30
+ )
31
+
32
+ # Inference code adapted from https://github.com/nv-tlabs/PixelUMM (inference.py).
33
+
34
+ MODEL_ID = "nvidia/PixelUMM"
35
+ MODEL_REVISION = "b621a91463d8a438f64c5d0116f42fb13cf26ce7"
36
+ CHECKPOINT = "S8-F22-R05" # default checkpoint; supports all four tasks
37
+ QWEN_ID = "Qwen/Qwen3-8B"
38
+ QWEN_REVISION = "b968826d9c46dd6066d109eabc6255188de91218"
39
+ MAX_SEED = 2**31 - 1
40
+ NEGATIVE_PROMPT = (
41
+ Path(__file__).parent / "experiments" / "s8_f22_r07" / "t2v_negative_prompt.txt"
42
+ ).read_text(encoding="utf-8").strip()
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Model loading (module level, as required by ZeroGPU)
46
+ # ---------------------------------------------------------------------------
47
+ model_root = snapshot_download(
48
+ MODEL_ID,
49
+ revision=MODEL_REVISION,
50
+ allow_patterns=[f"{CHECKPOINT}/__SAVE_COMPLETE", f"{CHECKPOINT}/model/*"],
51
+ )
52
+ qwen_dir = snapshot_download(
53
+ QWEN_ID,
54
+ revision=QWEN_REVISION,
55
+ allow_patterns=["config.json", "tokenizer.json", "tokenizer_config.json", "vocab.json", "merges.txt"],
56
+ )
57
+
58
+ model_args, data_args, training_args = parse_release_arguments(
59
+ DEFAULT_RELEASE_CONFIG, extra_args=["--llm_path", qwen_dir]
60
+ )
61
+ _prev_dtype = torch.get_default_dtype()
62
+ torch.set_default_dtype(torch.bfloat16)
63
+ try:
64
+ import logging
65
+
66
+ logger = logging.getLogger("pixelumm")
67
+ logger.addHandler(logging.StreamHandler())
68
+ logger.setLevel(logging.INFO)
69
+ model, tokenizer, new_token_ids = build_pixelumm_model(
70
+ model_args, data_args, training_args, logger
71
+ )
72
+ finally:
73
+ torch.set_default_dtype(_prev_dtype)
74
+ model = load_checkpoint_weights(os.path.join(model_root, CHECKPOINT), model, logger=logger)
75
+ model = model.to(dtype=torch.bfloat16).eval()
76
+ for p in model.parameters():
77
+ p.requires_grad = False
78
+ model.to("cuda")
79
+ PATCH = int(model_args.pixel_token_patch_size)
80
+ print("PixelUMM loaded")
81
+
82
+
83
+ def _seed_everything(seed: int) -> None:
84
+ random.seed(seed)
85
+ np.random.seed(seed)
86
+ torch.manual_seed(seed)
87
+ if torch.cuda.is_available():
88
+ torch.cuda.manual_seed_all(seed)
89
+
90
+
91
+ def _resolve_seed(seed, randomize):
92
+ return random.randint(0, MAX_SEED) if randomize else int(seed)
93
+
94
+
95
+ def _snap(v: int) -> int:
96
+ return max(PATCH, int(round(int(v) / PATCH)) * PATCH)
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Text to image
101
+ # ---------------------------------------------------------------------------
102
+ def _t2i_duration(prompt, height, width, steps, cfg, shift, seed):
103
+ return int(30 + steps * 0.6 * max(1.0, (height * width) / (512 * 512)))
104
+
105
+
106
+ @spaces.GPU(duration=_t2i_duration)
107
+ def _t2i_gpu(prompt, height, width, steps, cfg, shift, seed):
108
+ _seed_everything(seed)
109
+ device = torch.device("cuda")
110
+ with torch.inference_mode():
111
+ return _generate_t2i_one(
112
+ model, tokenizer, new_token_ids, prompt, (height, width), device,
113
+ steps, shift, "dpm-solver", cfg, 1.0, (0.0, 1.0), "none",
114
+ )
115
+
116
+
117
+ def text_to_image(prompt, height, width, steps, cfg, shift, seed, randomize_seed):
118
+ if not prompt or not prompt.strip():
119
+ raise gr.Error("Please enter a prompt.")
120
+ seed = _resolve_seed(seed, randomize_seed)
121
+ t0 = time.time()
122
+ image = _t2i_gpu(prompt.strip(), _snap(height), _snap(width), int(steps), float(cfg), float(shift), seed)
123
+ print(f"t2i {height}x{width} steps={steps} took {time.time() - t0:.1f}s")
124
+ return image, seed
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Text to video
129
+ # ---------------------------------------------------------------------------
130
+ VIDEO_SIZES = {
131
+ "320x176 (default)": (176, 320),
132
+ "176x320 (portrait)": (320, 176),
133
+ "480x272": (272, 480),
134
+ "640x352": (352, 640),
135
+ "832x464 (480p)": (464, 832),
136
+ }
137
+ XLARGE_TOKEN_THRESHOLD = 12_000
138
+
139
+
140
+ def _video_tokens(height, width, frames):
141
+ return (frames // 4) * (height // PATCH) * (width // PATCH)
142
+
143
+
144
+ def _t2v_duration(prompt, negative, height, width, frames, steps, cfg, shift, seed):
145
+ tokens = _video_tokens(height, width, frames)
146
+ passes = steps * (2 if cfg > 1.0 else 1)
147
+ est = passes * (tokens / 5000.0) * 0.35 * (1 + tokens / 40000.0)
148
+ return int(min(300, 40 + est))
149
+
150
+
151
+ def _t2v_impl(prompt, negative, height, width, frames, steps, cfg, shift, seed):
152
+ _seed_everything(seed)
153
+ device = torch.device("cuda")
154
+ with torch.inference_mode():
155
+ return _generate_t2v_one(
156
+ model, tokenizer, new_token_ids, prompt, (frames, height, width), device,
157
+ steps, shift, "unipc", cfg, 1.0, (0.0, 1.0), "none",
158
+ video_fps=24.0, negative_prompt=negative,
159
+ )
160
+
161
+
162
+ @spaces.GPU(duration=_t2v_duration)
163
+ def _t2v_gpu(*args):
164
+ return _t2v_impl(*args)
165
+
166
+
167
+ @spaces.GPU(duration=_t2v_duration, size="xlarge")
168
+ def _t2v_gpu_xlarge(*args):
169
+ return _t2v_impl(*args)
170
+
171
+
172
+ def text_to_video(prompt, negative, size, frames, steps, cfg, shift, seed, randomize_seed):
173
+ if not prompt or not prompt.strip():
174
+ raise gr.Error("Please enter a prompt.")
175
+ height, width = VIDEO_SIZES[size]
176
+ frames = max(4, int(frames) // 4 * 4)
177
+ seed = _resolve_seed(seed, randomize_seed)
178
+ tokens = _video_tokens(height, width, frames)
179
+ fn = _t2v_gpu_xlarge if tokens > XLARGE_TOKEN_THRESHOLD else _t2v_gpu
180
+ t0 = time.time()
181
+ frames_out = fn(
182
+ prompt.strip(), (negative or "").strip(), height, width, frames,
183
+ int(steps), float(cfg), float(shift), seed,
184
+ )
185
+ print(f"t2v {width}x{height}x{frames} tokens={tokens} steps={steps} took {time.time() - t0:.1f}s")
186
+ out_dir = tempfile.mkdtemp()
187
+ path = save_video_mp4(frames_out, out_dir, fps=24.0, filename="pixelumm.mp4")
188
+ if path is None:
189
+ raise gr.Error("Generation produced no frames.")
190
+ return path, seed
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Image understanding
195
+ # ---------------------------------------------------------------------------
196
+ def _resize_image_vlm(image: Image.Image) -> Image.Image:
197
+ image = image.convert("RGB")
198
+ h, w = smart_resize_pixels(
199
+ image.height,
200
+ image.width,
201
+ factor=PATCH,
202
+ min_pixels=IMAGE_VLM_CONTRACT.min_image_pixels,
203
+ max_pixels=IMAGE_VLM_CONTRACT.max_image_tokens * PATCH**2,
204
+ )
205
+ return transforms_f.resize(
206
+ image, [h, w], interpolation=transforms_f.InterpolationMode.BICUBIC, antialias=True
207
+ )
208
+
209
+
210
+ @spaces.GPU(duration=90)
211
+ def _image_vlm_gpu(image, prompt, max_new_tokens, temperature):
212
+ device = torch.device("cuda")
213
+ with torch.inference_mode():
214
+ text = generate_vlm_interleaved(
215
+ model=model,
216
+ tokenizer=tokenizer,
217
+ new_token_ids=new_token_ids,
218
+ images=[image],
219
+ instruction=prompt,
220
+ device=device,
221
+ max_length=max_new_tokens,
222
+ temperature=temperature,
223
+ fixed_length=False,
224
+ reasoning_mode="standard_bare",
225
+ exact_max_new_tokens=True,
226
+ )
227
+ return text[0] if isinstance(text, tuple) else text
228
+
229
+
230
+ def image_understanding(image, prompt, max_new_tokens, temperature):
231
+ if image is None:
232
+ raise gr.Error("Please upload an image.")
233
+ if not prompt or not prompt.strip():
234
+ raise gr.Error("Please enter a question.")
235
+ image = _resize_image_vlm(image)
236
+ return _image_vlm_gpu(image, prompt.strip(), int(max_new_tokens), float(temperature)).strip()
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # Video understanding
241
+ # ---------------------------------------------------------------------------
242
+ def _decode_video_vlm(path: str):
243
+ video_path = Path(path)
244
+ c = VIDEO_VLM_CONTRACT
245
+ decoded = decode_pixelumm_video(
246
+ key=video_path.name,
247
+ data=video_path.read_bytes(),
248
+ min_fps=1.0,
249
+ max_fps=240.0,
250
+ target_fps=c.target_fps,
251
+ dense_target_fps=c.dense_target_fps,
252
+ sparse_target_fps=c.sparse_target_fps,
253
+ max_sampled_frames=c.max_sampled_frames,
254
+ sparse_max_sampled_frames=c.sparse_max_sampled_frames,
255
+ dense_representation_probability=c.dense_representation_probability,
256
+ max_raw_patch_tokens=c.max_raw_patch_tokens,
257
+ patch_size=PATCH,
258
+ resize_multiple=PATCH,
259
+ min_frame_pixels=c.min_frame_pixels,
260
+ max_frame_pixels=c.max_frame_pixels,
261
+ temporal_patch_size=c.temporal_patch_size,
262
+ drop_incomplete_dense_tube=c.drop_incomplete_dense_tube,
263
+ representation_selector_key=str(video_path),
264
+ )
265
+ if decoded is None:
266
+ raise gr.Error("Could not decode this video (unsupported format, frame rate or length).")
267
+ videos = decoded["videos"]
268
+ frames = [
269
+ Image.fromarray(videos[:, i].permute(1, 2, 0).cpu().numpy()).convert("RGB")
270
+ for i in range(int(videos.shape[1]))
271
+ ]
272
+ source_fps = float(decoded["source_fps"])
273
+ timestamps = [float(i) / source_fps for i in decoded["frame_indices"]]
274
+ return frames, timestamps, int(decoded["temporal_patch_size"])
275
+
276
+
277
+ @spaces.GPU(duration=120)
278
+ def _video_vlm_gpu(frames, timestamps, temporal_patch_size, prompt, max_new_tokens, temperature):
279
+ device = torch.device("cuda")
280
+ with torch.inference_mode():
281
+ return _generate_v2t_one(
282
+ model, tokenizer, new_token_ids, frames, timestamps, prompt, device,
283
+ max_new_tokens + 1, temperature,
284
+ temporal_patch_size=temporal_patch_size,
285
+ timestamp_mode=VIDEO_VLM_CONTRACT.timestamp_mode,
286
+ )
287
+
288
+
289
+ def video_understanding(video, prompt, max_new_tokens, temperature):
290
+ if not video:
291
+ raise gr.Error("Please upload a video.")
292
+ if not prompt or not prompt.strip():
293
+ raise gr.Error("Please enter a question.")
294
+ frames, timestamps, tps = _decode_video_vlm(video)
295
+ return _video_vlm_gpu(
296
+ frames, timestamps, tps, prompt.strip(), int(max_new_tokens), float(temperature)
297
+ ).strip()
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # UI
302
+ # ---------------------------------------------------------------------------
303
+ EX_DIR = Path(__file__).parent / "examples"
304
+
305
+ with gr.Blocks(title="PixelUMM") as demo:
306
+ gr.Markdown("# PixelUMM")
307
+ gr.Markdown(
308
+ f"[Model on Hugging Face](https://huggingface.co/{MODEL_ID}) · "
309
+ "[Code](https://github.com/nv-tlabs/PixelUMM) · "
310
+ "[Paper](https://arxiv.org/abs/2609.38597)\n\n"
311
+ "Encoder-free unified multimodal model from NVIDIA: one decoder-only Transformer "
312
+ "(Qwen3-8B backbone, 15.2B params) that reads and writes raw pixels, with no VAE and no vision encoder. "
313
+ f"This demo runs the default `{CHECKPOINT}` checkpoint. "
314
+ "Weights are released under the NVIDIA One-Way Noncommercial License (research/evaluation only)."
315
+ )
316
+
317
+ with gr.Tabs():
318
+ with gr.Tab("Text to Image"):
319
+ with gr.Row():
320
+ with gr.Column():
321
+ t2i_prompt = gr.Textbox(label="Prompt", lines=3, placeholder="A red panda reading beside a window")
322
+ t2i_btn = gr.Button("Generate", variant="primary")
323
+ with gr.Accordion("Advanced settings", open=False):
324
+ with gr.Row():
325
+ t2i_w = gr.Slider(256, 768, value=512, step=16, label="Width")
326
+ t2i_h = gr.Slider(256, 768, value=512, step=16, label="Height")
327
+ t2i_steps = gr.Slider(10, 100, value=50, step=1, label="Steps (DPM-Solver++)")
328
+ t2i_cfg = gr.Slider(1.0, 10.0, value=3.5, step=0.1, label="CFG scale")
329
+ t2i_shift = gr.Slider(1.0, 10.0, value=3.0, step=0.1, label="Timestep shift")
330
+ with gr.Row():
331
+ t2i_seed = gr.Number(value=4396, label="Seed", precision=0)
332
+ t2i_rand = gr.Checkbox(value=True, label="Randomize seed")
333
+ with gr.Column():
334
+ t2i_out = gr.Image(label="Image", type="pil", format="png")
335
+ t2i_seed_out = gr.Number(label="Seed used", interactive=False)
336
+ gr.Examples(
337
+ examples=[
338
+ ["A red panda reading beside a window"],
339
+ ["A cozy cabin in a snowy pine forest at dusk, warm light glowing from the windows"],
340
+ ["A bowl of ramen with a soft-boiled egg, studio food photography"],
341
+ ["An astronaut riding a horse on the surface of Mars, cinematic lighting"],
342
+ ],
343
+ inputs=[t2i_prompt],
344
+ )
345
+ t2i_btn.click(
346
+ text_to_image,
347
+ inputs=[t2i_prompt, t2i_h, t2i_w, t2i_steps, t2i_cfg, t2i_shift, t2i_seed, t2i_rand],
348
+ outputs=[t2i_out, t2i_seed_out],
349
+ )
350
+
351
+ with gr.Tab("Text to Video"):
352
+ with gr.Row():
353
+ with gr.Column():
354
+ t2v_prompt = gr.Textbox(
355
+ label="Prompt",
356
+ lines=3,
357
+ placeholder="A golden retriever runs across a grassy field in bright daylight.",
358
+ )
359
+ t2v_btn = gr.Button("Generate", variant="primary")
360
+ t2v_size = gr.Dropdown(list(VIDEO_SIZES), value="320x176 (default)", label="Resolution (W x H)")
361
+ t2v_frames = gr.Slider(16, 96, value=96, step=4, label="Frames (24 fps; 96 = 4 s)")
362
+ with gr.Accordion("Advanced settings", open=False):
363
+ t2v_neg = gr.Textbox(label="Negative prompt", value=NEGATIVE_PROMPT, lines=3)
364
+ t2v_steps = gr.Slider(10, 50, value=35, step=1, label="Steps (UniPC)")
365
+ t2v_cfg = gr.Slider(1.0, 10.0, value=6.0, step=0.1, label="CFG scale")
366
+ t2v_shift = gr.Slider(1.0, 15.0, value=10.0, step=0.5, label="Timestep shift")
367
+ with gr.Row():
368
+ t2v_seed = gr.Number(value=4396, label="Seed", precision=0)
369
+ t2v_rand = gr.Checkbox(value=True, label="Randomize seed")
370
+ gr.Markdown(
371
+ "Larger resolutions with many frames run on a full-size GPU and use more quota. "
372
+ "The official Cosmos guardrail checks are not run in this demo."
373
+ )
374
+ with gr.Column():
375
+ t2v_out = gr.Video(label="Video", autoplay=True)
376
+ t2v_seed_out = gr.Number(label="Seed used", interactive=False)
377
+ gr.Examples(
378
+ examples=[
379
+ ["A golden retriever runs across a grassy field in bright daylight. The camera tracks smoothly beside the dog."],
380
+ ["Ocean waves crash against black volcanic rocks at sunset, slow motion, spray in the air."],
381
+ ["A steaming cup of coffee on a wooden table, morning sunlight, steam slowly rising."],
382
+ ["Aerial drone shot flying over a winding river through an autumn forest."],
383
+ ],
384
+ inputs=[t2v_prompt],
385
+ )
386
+ t2v_btn.click(
387
+ text_to_video,
388
+ inputs=[t2v_prompt, t2v_neg, t2v_size, t2v_frames, t2v_steps, t2v_cfg, t2v_shift, t2v_seed, t2v_rand],
389
+ outputs=[t2v_out, t2v_seed_out],
390
+ )
391
+
392
+ with gr.Tab("Image Understanding"):
393
+ with gr.Row():
394
+ with gr.Column():
395
+ iu_image = gr.Image(label="Image", type="pil")
396
+ iu_prompt = gr.Textbox(label="Question", value="Describe the image.", lines=2)
397
+ iu_btn = gr.Button("Ask", variant="primary")
398
+ with gr.Accordion("Advanced settings", open=False):
399
+ iu_tokens = gr.Slider(16, 1024, value=256, step=16, label="Max new tokens")
400
+ iu_temp = gr.Slider(0.0, 1.5, value=0.0, step=0.05, label="Temperature (0 = greedy)")
401
+ with gr.Column():
402
+ iu_out = gr.Textbox(label="Answer", lines=12)
403
+ gr.Examples(
404
+ examples=[
405
+ [str(EX_DIR / "cats.jpg"), "Describe the image in detail."],
406
+ [str(EX_DIR / "cats.jpg"), "How many cats are there, and what are they lying on?"],
407
+ [str(EX_DIR / "street.jpg"), "What objects are in this room?"],
408
+ ],
409
+ inputs=[iu_image, iu_prompt],
410
+ )
411
+ iu_btn.click(image_understanding, inputs=[iu_image, iu_prompt, iu_tokens, iu_temp], outputs=iu_out)
412
+
413
+ with gr.Tab("Video Understanding"):
414
+ with gr.Row():
415
+ with gr.Column():
416
+ vu_video = gr.Video(label="Video")
417
+ vu_prompt = gr.Textbox(label="Question", value="Describe the video.", lines=2)
418
+ vu_btn = gr.Button("Ask", variant="primary")
419
+ with gr.Accordion("Advanced settings", open=False):
420
+ vu_tokens = gr.Slider(16, 1024, value=256, step=16, label="Max new tokens")
421
+ vu_temp = gr.Slider(0.0, 1.5, value=0.0, step=0.05, label="Temperature (0 = greedy)")
422
+ gr.Markdown("Frames are sampled at 1 fps (up to 96 frames, at most 448x448 each).")
423
+ with gr.Column():
424
+ vu_out = gr.Textbox(label="Answer", lines=12)
425
+ vu_btn.click(video_understanding, inputs=[vu_video, vu_prompt, vu_tokens, vu_temp], outputs=vu_out)
426
+
427
+ gr.Markdown("[Twitter / X](https://x.com/realmrfakename)")
428
+
429
+
430
+ if __name__ == "__main__":
431
+ demo.queue().launch()
data/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
data/data_utils.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+
6
+ import math
7
+
8
+ import torch
9
+ from torch.nn.attention.flex_attention import or_masks, and_masks
10
+
11
+
12
+ PIXELUMM_VIDEO_MROPE_BASE_FPS = 24.0
13
+
14
+
15
+ def temporal_rope_units_per_second(fps, temporal_compression_factor):
16
+ """Return physical-time RoPE units per second for a temporal token grid."""
17
+ fps = float(fps)
18
+ temporal_compression_factor = float(temporal_compression_factor)
19
+ if not math.isfinite(fps) or fps <= 0:
20
+ raise ValueError(f"fps must be finite and positive, got {fps}")
21
+ if not math.isfinite(temporal_compression_factor) or temporal_compression_factor <= 0:
22
+ raise ValueError(
23
+ "temporal_compression_factor must be finite and positive, got "
24
+ f"{temporal_compression_factor}"
25
+ )
26
+ return fps / temporal_compression_factor
27
+
28
+
29
+ def create_sparse_mask(document_lens, split_lens, attn_modes, device, total_length=None):
30
+ def causal_mask(b, h, q_idx, kv_idx):
31
+ return q_idx >= kv_idx
32
+
33
+ def full_and_noise_mask(b, h, q_idx, kv_idx):
34
+ return (full_and_noise_seq_id[q_idx] == full_and_noise_seq_id[kv_idx]) & (full_and_noise_seq_id[q_idx] >= 0)
35
+
36
+ def remove_noise_mask(b, h, q_idx, kv_idx):
37
+ return (~((noise_seq_id[kv_idx] >= 0) & (noise_seq_id[q_idx] != noise_seq_id[kv_idx])))
38
+
39
+ def sample_mask(b, h, q_idx, kv_idx):
40
+ return (document_id[q_idx] >= 0) & (document_id[q_idx] == document_id[kv_idx])
41
+
42
+ full_and_noise_tmp = []
43
+ noise_tmp = []
44
+
45
+ for i, (length, model) in enumerate(zip(split_lens, attn_modes)):
46
+ value = i if model in ['full', 'noise'] else -1
47
+ full_and_noise_tmp.extend([value] * length)
48
+ value_noise = i if model == 'noise' else -1
49
+ noise_tmp.extend([value_noise] * length)
50
+
51
+ logical_length = sum(document_lens)
52
+ total_length = logical_length if total_length is None else int(total_length)
53
+ if total_length < logical_length:
54
+ raise ValueError((total_length, logical_length))
55
+ trailing_padding = total_length - logical_length
56
+ full_and_noise_tmp.extend([-1] * trailing_padding)
57
+ noise_tmp.extend([-1] * trailing_padding)
58
+ full_and_noise_seq_id = torch.Tensor(full_and_noise_tmp).to(device)
59
+ noise_seq_id = torch.Tensor(noise_tmp).to(device)
60
+
61
+ document_id = torch.cat(
62
+ [torch.full((l,), i) for i, l in enumerate(document_lens, start=1)]
63
+ + ([torch.full((trailing_padding,), -1)] if trailing_padding else [])
64
+ ).to(device)
65
+
66
+ return and_masks(or_masks(causal_mask, full_and_noise_mask), remove_noise_mask, sample_mask)
67
+
68
+
69
+ def tubeify(video, patch_size, temporal_patch_size):
70
+ """Patchify a fixed-length video clip into spatio-temporal tube patches.
71
+
72
+ Args:
73
+ video: (T, C, H, W) tensor.
74
+ patch_size: spatial patch size.
75
+ temporal_patch_size: number of frames per tube token.
76
+
77
+ Returns:
78
+ (num_tubes, temporal_patch_size * patch_size * patch_size * C)
79
+ """
80
+ t, c, h, w = video.shape
81
+ p = patch_size
82
+ assert t % temporal_patch_size == 0
83
+ assert h % p == 0 and w % p == 0
84
+ num_tubes_t = t // temporal_patch_size
85
+ video = video.reshape(num_tubes_t, temporal_patch_size, c, h // p, p, w // p, p)
86
+ # Canonical video token order is THW:
87
+ # temporal group first, then the full spatial grid for that group.
88
+ video = torch.einsum("utchpwq->uhwtpqc", video)
89
+ video = video.reshape(-1, temporal_patch_size * p ** 2 * c)
90
+ return video
91
+
92
+
93
+ def build_pixelumm_vision_mrope_position_ids(
94
+ temporal_offset,
95
+ t_tokens,
96
+ h_tokens,
97
+ w_tokens,
98
+ device=None,
99
+ fps=None,
100
+ base_fps=PIXELUMM_VIDEO_MROPE_BASE_FPS,
101
+ temporal_compression_factor=4,
102
+ ):
103
+ """Build PixelUMM VFM unified_3d_mrope ids for one THW-flattened diffusion/pixel grid."""
104
+ if fps is not None:
105
+ tps = temporal_rope_units_per_second(fps, temporal_compression_factor)
106
+ base_tps = temporal_rope_units_per_second(base_fps, temporal_compression_factor)
107
+ t = torch.arange(t_tokens, dtype=torch.float32, device=device)
108
+ t = t / tps * base_tps + float(temporal_offset)
109
+ t = t.view(-1, 1).expand(-1, h_tokens * w_tokens)
110
+ else:
111
+ t = torch.arange(t_tokens, dtype=torch.long, device=device).view(-1, 1).expand(-1, h_tokens * w_tokens)
112
+ t = t + int(temporal_offset)
113
+ h = torch.arange(h_tokens, dtype=torch.long, device=device).view(1, -1, 1).expand(t_tokens, -1, w_tokens)
114
+ w = torch.arange(w_tokens, dtype=torch.long, device=device).view(1, 1, -1).expand(t_tokens, h_tokens, -1)
115
+ if fps is not None:
116
+ ids = torch.stack([t.flatten(), h.flatten().to(torch.float32), w.flatten().to(torch.float32)], dim=0)
117
+ else:
118
+ ids = torch.stack([t.flatten(), h.flatten(), w.flatten()], dim=0)
119
+ next_temporal_offset = math.ceil(ids.max().item()) + 1 if ids.numel() else int(temporal_offset)
120
+ return ids, next_temporal_offset
121
+
122
+
123
+ def untubeify(video_tubes, image_size, num_frames, patch_size):
124
+ """Reconstruct video frames from THW-ordered spatio-temporal tube tokens."""
125
+ H, W = image_size
126
+ h_patches, w_patches = H // patch_size, W // patch_size
127
+ tube_frames = video_tubes.shape[-1] // (patch_size * patch_size * 3)
128
+ if num_frames % tube_frames != 0:
129
+ raise ValueError(
130
+ f"Expected num_frames to be divisible by tube_frames={tube_frames}, got {num_frames}"
131
+ )
132
+ temporal_groups = num_frames // tube_frames
133
+ frames = video_tubes.float().cpu().reshape(
134
+ temporal_groups, h_patches, w_patches, tube_frames, patch_size, patch_size, 3
135
+ )
136
+ frames = frames.permute(0, 3, 1, 4, 2, 5, 6).contiguous().reshape(num_frames, H, W, 3)
137
+ return frames
138
+
139
+
140
+ def add_special_tokens(tokenizer):
141
+ all_special_tokens = []
142
+ for k, v in tokenizer.special_tokens_map.items():
143
+ if isinstance(v, str):
144
+ all_special_tokens.append(v)
145
+ elif isinstance(v, list):
146
+ all_special_tokens += v
147
+
148
+ new_tokens = []
149
+
150
+ if '<|im_start|>' not in all_special_tokens:
151
+ new_tokens.append('<|im_start|>')
152
+
153
+ if '<|im_end|>' not in all_special_tokens:
154
+ new_tokens.append('<|im_end|>')
155
+
156
+ if '<|vision_start|>' not in all_special_tokens:
157
+ new_tokens.append('<|vision_start|>')
158
+
159
+ if '<|vision_end|>' not in all_special_tokens:
160
+ new_tokens.append('<|vision_end|>')
161
+
162
+ num_new_tokens = tokenizer.add_tokens(new_tokens)
163
+ bos_token_id = tokenizer.convert_tokens_to_ids('<|im_start|>')
164
+ eos_token_id = tokenizer.convert_tokens_to_ids('<|im_end|>')
165
+ start_of_image = tokenizer.convert_tokens_to_ids('<|vision_start|>')
166
+ end_of_image = tokenizer.convert_tokens_to_ids('<|vision_end|>')
167
+
168
+ new_token_ids = dict(
169
+ bos_token_id=bos_token_id,
170
+ eos_token_id=eos_token_id,
171
+ start_of_image=start_of_image,
172
+ end_of_image=end_of_image,
173
+ )
174
+
175
+ return tokenizer, new_token_ids, num_new_tokens
176
+
177
+
178
+ def len2weight(x, loss_reduction='square'):
179
+ if x == 0:
180
+ return x
181
+ if loss_reduction == 'token':
182
+ return 1
183
+ if loss_reduction == 'sample':
184
+ return 1 / x
185
+ if loss_reduction == 'square':
186
+ return 1 / (x ** 0.5)
187
+ raise NotImplementedError(loss_reduction)
data/pixelumm_image_resize.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """PixelUMM-owned image conversion and aspect-preserving resize helpers."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ import torch
11
+
12
+ from data.pixelumm_smart_resize import smart_resize_pixels
13
+
14
+
15
+ def pil_to_chw_uint8(image: Any) -> torch.Tensor:
16
+ """Convert a PIL-compatible image to contiguous RGB ``uint8`` CHW."""
17
+
18
+ if image.mode != "RGB":
19
+ image = image.convert("RGB")
20
+ return (
21
+ torch.frombuffer(bytearray(image.tobytes()), dtype=torch.uint8)
22
+ .view(image.height, image.width, 3)
23
+ .permute(2, 0, 1)
24
+ .contiguous()
25
+ )
26
+
27
+
28
+ def resize_image_to_exact_hw(
29
+ image: torch.Tensor,
30
+ target_hw: tuple[int, int],
31
+ ) -> torch.Tensor:
32
+ """Resize one CHW tensor with the released bicubic-antialias contract."""
33
+
34
+ target_h, target_w = (int(target_hw[0]), int(target_hw[1]))
35
+ if target_h <= 0 or target_w <= 0:
36
+ raise ValueError(f"target H/W must be positive, got {target_hw!r}")
37
+ if (int(image.shape[-2]), int(image.shape[-1])) == (target_h, target_w):
38
+ return image.contiguous()
39
+ resized = torch.nn.functional.interpolate(
40
+ image.float().unsqueeze(0),
41
+ size=(target_h, target_w),
42
+ mode="bicubic",
43
+ align_corners=False,
44
+ antialias=True,
45
+ ).squeeze(0)
46
+ if image.dtype == torch.uint8:
47
+ return resized.round().clamp_(0, 255).to(torch.uint8).contiguous()
48
+ return resized.to(dtype=image.dtype).contiguous()
49
+
50
+
51
+ def resize_image_native_smart_resize(
52
+ image: torch.Tensor,
53
+ *,
54
+ patch_size: int,
55
+ max_tokens: int | None,
56
+ min_pixels: int,
57
+ enabled: bool,
58
+ ) -> torch.Tensor:
59
+ """Preserve native aspect ratio under explicit raw-pixel bounds."""
60
+
61
+ if not enabled:
62
+ return image.contiguous()
63
+ if max_tokens is None or int(max_tokens) <= 0:
64
+ raise ValueError("native_smart_resize requires a positive image-token budget")
65
+ max_pixels = int(max_tokens) * int(patch_size) * int(patch_size)
66
+ min_pixels = int(min_pixels)
67
+ if min_pixels <= 0:
68
+ raise ValueError("native_smart_resize requires positive min_image_pixels")
69
+ target_hw = smart_resize_pixels(
70
+ int(image.shape[-2]),
71
+ int(image.shape[-1]),
72
+ factor=int(patch_size),
73
+ min_pixels=min_pixels,
74
+ max_pixels=max_pixels,
75
+ )
76
+ return resize_image_to_exact_hw(image, target_hw)
data/pixelumm_smart_resize.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Shared aspect-preserving smart-resize geometry for PixelUMM media."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+
10
+
11
+ _MAX_ASPECT_RATIO = 200.0
12
+
13
+
14
+ def smart_resize_pixels(
15
+ height: int,
16
+ width: int,
17
+ *,
18
+ factor: int,
19
+ min_pixels: int,
20
+ max_pixels: int,
21
+ ) -> tuple[int, int]:
22
+ """Resize H/W to a factor-aligned grid within explicit pixel bounds."""
23
+
24
+ height = int(height)
25
+ width = int(width)
26
+ factor = int(factor)
27
+ min_pixels = int(min_pixels)
28
+ max_pixels = int(max_pixels)
29
+ if height <= 0 or width <= 0 or factor <= 0:
30
+ raise ValueError(
31
+ f"Invalid resize geometry height={height} width={width} factor={factor}"
32
+ )
33
+ if min_pixels <= 0 or max_pixels < min_pixels:
34
+ raise ValueError(f"Invalid pixel bounds min={min_pixels} max={max_pixels}")
35
+ aspect_ratio = max(height, width) / min(height, width)
36
+ if aspect_ratio > _MAX_ASPECT_RATIO:
37
+ raise ValueError(
38
+ f"absolute aspect ratio must be <= {_MAX_ASPECT_RATIO}, got {aspect_ratio}"
39
+ )
40
+
41
+ resized_height = max(factor, round(height / factor) * factor)
42
+ resized_width = max(factor, round(width / factor) * factor)
43
+ if resized_height * resized_width > max_pixels:
44
+ scale = math.sqrt((height * width) / max_pixels)
45
+ resized_height = max(
46
+ factor, math.floor(height / scale / factor) * factor
47
+ )
48
+ resized_width = max(
49
+ factor, math.floor(width / scale / factor) * factor
50
+ )
51
+ elif resized_height * resized_width < min_pixels:
52
+ scale = math.sqrt(min_pixels / (height * width))
53
+ resized_height = math.ceil(height * scale / factor) * factor
54
+ resized_width = math.ceil(width * scale / factor) * factor
55
+ return int(resized_height), int(resized_width)
data/pixelumm_video_decoder.py ADDED
@@ -0,0 +1,686 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """PixelUMM-native video sampling and smart-resize utilities."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from concurrent.futures import ThreadPoolExecutor, wait
9
+ from dataclasses import dataclass
10
+ import hashlib
11
+ import math
12
+ import re
13
+ from typing import TYPE_CHECKING
14
+
15
+ import torch
16
+ import torchvision.transforms.functional as transforms_F
17
+
18
+ if TYPE_CHECKING:
19
+ from torchcodec.decoders import VideoDecoder
20
+
21
+ from .pixelumm_smart_resize import smart_resize_pixels
22
+
23
+
24
+ _VIDEO_EXTENSIONS = frozenset({"mp4", "avi", "webm", "mov"})
25
+ # Keep the deterministic train-data split stable across the product rename.
26
+ _VIDEO_REPRESENTATION_COMPATIBILITY_SALT = bytes.fromhex(
27
+ "706978656c626167656c2d766964656f2d726570726573656e746174696f6e2d763100"
28
+ )
29
+
30
+
31
+ def _open_video_decoder(*args: object, **kwargs: object) -> "VideoDecoder":
32
+ """Import TorchCodec only when a caller actually decodes video media."""
33
+
34
+ try:
35
+ from torchcodec.decoders import VideoDecoder
36
+ except (ImportError, RuntimeError) as error:
37
+ raise RuntimeError(
38
+ "Video decoding requires the torchcodec version pinned in "
39
+ "requirements.txt and system FFmpeg libraries"
40
+ ) from error
41
+ return VideoDecoder(*args, **kwargs)
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class PixelUMMVideoLayout:
46
+ """Deterministic model-input layout selected before decoding frames."""
47
+
48
+ frame_indices: tuple[int, ...]
49
+ conditioning_fps: float
50
+ resized_height: int
51
+ resized_width: int
52
+ raw_patch_tokens: int
53
+ packed_patch_tokens: int
54
+
55
+
56
+ class PixelUMMVideoRejectedError(ValueError):
57
+ """Expected media rejection with a stable, machine-readable reason."""
58
+
59
+ def __init__(self, reason: str, message: str, **details: object) -> None:
60
+ super().__init__(message)
61
+ self.reason = str(reason)
62
+ self.details = dict(details)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class PixelUMMVideoRepresentation:
67
+ """Per-video temporal representation selected before frame decoding."""
68
+
69
+ mode: str
70
+ nominal_frame_count: int
71
+ max_sampled_frames: int
72
+ temporal_patch_size: int
73
+ selection_reason: str
74
+ dense_probability: float
75
+ selection_draw: float | None
76
+ target_fps: float
77
+ dense_target_fps: float
78
+ sparse_target_fps: float
79
+ dense_nominal_frame_count: int
80
+ sparse_nominal_frame_count: int
81
+ dense_grid_supported: bool
82
+ sampling_mode: str
83
+
84
+
85
+ def stable_video_representation_draw(identity: str) -> float:
86
+ """Map an immutable sample identity to a reproducible value in ``[0, 1)``."""
87
+
88
+ digest = hashlib.blake2b(
89
+ _VIDEO_REPRESENTATION_COMPATIBILITY_SALT + str(identity).encode("utf-8"),
90
+ digest_size=8,
91
+ ).digest()
92
+ return int.from_bytes(digest, "big") / float(1 << 64)
93
+
94
+
95
+ def _decode_video_frames_with_timeout(
96
+ decoder: VideoDecoder,
97
+ frame_indices: tuple[int, ...],
98
+ *,
99
+ timeout_seconds: int,
100
+ ) -> torch.Tensor | None:
101
+ """Decode an explicit frame list without leaking uncancellable C++ work.
102
+
103
+ TorchCodec frame decode is already executing in C++ when ``Future.cancel``
104
+ returns false. Returning at the timeout and calling ``shutdown(wait=False)``
105
+ therefore leaves a ghost decode consuming the DataLoader worker's CPU. The
106
+ configured timeout is a slow-operation diagnostic threshold, not a false
107
+ promise that a Python thread can hard-cancel native decode.
108
+ """
109
+
110
+ executor = ThreadPoolExecutor(max_workers=1)
111
+ future = executor.submit(decoder.get_frames_at, indices=list(frame_indices))
112
+ try:
113
+ _, unfinished = wait((future,), timeout=int(timeout_seconds))
114
+ if unfinished:
115
+ wait((future,))
116
+ batch = future.result()
117
+ finally:
118
+ executor.shutdown(wait=True, cancel_futures=True)
119
+ frames = batch.data
120
+ if frames.ndim != 4 or int(frames.shape[0]) != len(frame_indices):
121
+ raise ValueError(
122
+ f"Unexpected torchcodec output shape {tuple(frames.shape)} for "
123
+ f"{len(frame_indices)} requested frames"
124
+ )
125
+ return frames
126
+
127
+
128
+ def select_video_representation(
129
+ *,
130
+ total_frames: int,
131
+ source_fps: float,
132
+ target_fps: float,
133
+ dense_target_fps: float | None = None,
134
+ sparse_target_fps: float | None = None,
135
+ dense_max_sampled_frames: int,
136
+ dense_temporal_patch_size: int,
137
+ sparse_max_sampled_frames: int = 0,
138
+ dense_representation_probability: float = 1.0,
139
+ representation_draw: float | None = None,
140
+ start_frame: int = 0,
141
+ end_frame: int | None = None,
142
+ ) -> PixelUMMVideoRepresentation:
143
+ """Choose dense tubes or sparse frame-as-image encoding for one clip.
144
+
145
+ ``target_fps`` is used when explicit dense/sparse cadences are absent.
146
+ The release uses 24 FPS for generated 4-frame tubes and 1 FPS for sparse
147
+ frame-as-image understanding. A dense route is legal only
148
+ when the selected clip fits the dense cap and the source provides every
149
+ requested timestamp as a unique frame; low-FPS media is never duplicated.
150
+ A short clip that cannot provide 4 FPS but can provide 1 FPS falls back to
151
+ the strict sparse timestamp grid. Full-clip uniform sampling is used when
152
+ the nominal dense grid exceeds its frame cap or the source cannot provide
153
+ even the sparse cadence. A zero sparse cap selects dense-only behavior.
154
+ """
155
+
156
+ dense_max_sampled_frames = int(dense_max_sampled_frames)
157
+ dense_temporal_patch_size = int(dense_temporal_patch_size)
158
+ sparse_max_sampled_frames = int(sparse_max_sampled_frames)
159
+ dense_representation_probability = float(dense_representation_probability)
160
+ dense_target_fps = float(target_fps if dense_target_fps is None else dense_target_fps)
161
+ sparse_target_fps = float(target_fps if sparse_target_fps is None else sparse_target_fps)
162
+ if dense_max_sampled_frames <= 0 or dense_temporal_patch_size <= 0:
163
+ raise ValueError(
164
+ "Dense video frame and temporal-patch values must be positive: "
165
+ f"frames={dense_max_sampled_frames} patch={dense_temporal_patch_size}"
166
+ )
167
+ if sparse_max_sampled_frames < 0:
168
+ raise ValueError(
169
+ "sparse_max_sampled_frames must be non-negative, got "
170
+ f"{sparse_max_sampled_frames}"
171
+ )
172
+ if not 0.0 <= dense_representation_probability <= 1.0:
173
+ raise ValueError(
174
+ "dense_representation_probability must be in [0, 1], got "
175
+ f"{dense_representation_probability}"
176
+ )
177
+ if representation_draw is not None and not 0.0 <= float(representation_draw) < 1.0:
178
+ raise ValueError(f"representation_draw must be in [0, 1), got {representation_draw}")
179
+ if dense_target_fps <= 0 or sparse_target_fps <= 0:
180
+ raise ValueError(
181
+ "dense_target_fps and sparse_target_fps must be positive: "
182
+ f"dense={dense_target_fps} sparse={sparse_target_fps}"
183
+ )
184
+
185
+ # total_frames is a strict upper bound on unique sampled source frames, so
186
+ # this returns the exact uncapped target-FPS grid without a second policy.
187
+ dense_nominal_indices, _ = sample_frame_indices(
188
+ total_frames=total_frames,
189
+ source_fps=source_fps,
190
+ target_fps=dense_target_fps,
191
+ max_sampled_frames=int(total_frames),
192
+ start_frame=start_frame,
193
+ end_frame=end_frame,
194
+ )
195
+ sparse_nominal_indices, _ = sample_frame_indices(
196
+ total_frames=total_frames,
197
+ source_fps=source_fps,
198
+ target_fps=sparse_target_fps,
199
+ max_sampled_frames=int(total_frames),
200
+ start_frame=start_frame,
201
+ end_frame=end_frame,
202
+ )
203
+ clip_frames = (int(total_frames) if end_frame is None else int(end_frame)) - int(start_frame)
204
+ clip_duration = clip_frames / float(source_fps)
205
+ requested_dense_count = max(1, int(math.ceil(clip_duration * dense_target_fps - 1e-12)))
206
+ requested_sparse_count = max(1, int(math.ceil(clip_duration * sparse_target_fps - 1e-12)))
207
+ dense_nominal_frame_count = len(dense_nominal_indices)
208
+ sparse_nominal_frame_count = len(sparse_nominal_indices)
209
+ dense_grid_supported = (
210
+ dense_nominal_frame_count == requested_dense_count
211
+ and dense_nominal_frame_count >= dense_temporal_patch_size
212
+ )
213
+ sparse_grid_supported = sparse_nominal_frame_count == requested_sparse_count
214
+
215
+ def sparse(reason: str) -> PixelUMMVideoRepresentation:
216
+ return PixelUMMVideoRepresentation(
217
+ mode="sparse_frame_image",
218
+ nominal_frame_count=sparse_nominal_frame_count,
219
+ max_sampled_frames=sparse_max_sampled_frames,
220
+ temporal_patch_size=1,
221
+ selection_reason=reason,
222
+ dense_probability=dense_representation_probability,
223
+ selection_draw=(
224
+ float(representation_draw) if representation_draw is not None else None
225
+ ),
226
+ target_fps=sparse_target_fps,
227
+ dense_target_fps=dense_target_fps,
228
+ sparse_target_fps=sparse_target_fps,
229
+ dense_nominal_frame_count=dense_nominal_frame_count,
230
+ sparse_nominal_frame_count=sparse_nominal_frame_count,
231
+ dense_grid_supported=dense_grid_supported,
232
+ sampling_mode=(
233
+ "uniform_clip"
234
+ if reason == "dense_grid_above_cap" or not sparse_grid_supported
235
+ else "timestamp_grid"
236
+ ),
237
+ )
238
+
239
+ # Decide the long-clip fallback from the requested dense cadence, not from
240
+ # the number of unique frames a low-FPS source happens to provide.
241
+ if sparse_max_sampled_frames > 0 and requested_dense_count > dense_max_sampled_frames:
242
+ return sparse("dense_grid_above_cap")
243
+ if sparse_max_sampled_frames > 0 and not dense_grid_supported:
244
+ return sparse("dense_grid_unavailable")
245
+ if sparse_max_sampled_frames > 0 and dense_representation_probability < 1.0:
246
+ if representation_draw is None and dense_representation_probability not in {0.0, 1.0}:
247
+ raise ValueError(
248
+ "A deterministic representation_draw is required when short-video "
249
+ "dense_representation_probability is strictly between 0 and 1"
250
+ )
251
+ draw = float(representation_draw or 0.0)
252
+ if draw >= dense_representation_probability:
253
+ return sparse("short_probability_sparse")
254
+ return PixelUMMVideoRepresentation(
255
+ mode=(
256
+ f"dense_tube_t{dense_temporal_patch_size}"
257
+ if dense_temporal_patch_size > 1
258
+ else "frame_image"
259
+ ),
260
+ nominal_frame_count=dense_nominal_frame_count,
261
+ max_sampled_frames=dense_max_sampled_frames,
262
+ temporal_patch_size=dense_temporal_patch_size,
263
+ selection_reason=(
264
+ "short_probability_dense"
265
+ if sparse_max_sampled_frames > 0 and dense_representation_probability < 1.0
266
+ else "dense_default"
267
+ ),
268
+ dense_probability=dense_representation_probability,
269
+ selection_draw=(
270
+ float(representation_draw) if representation_draw is not None else None
271
+ ),
272
+ target_fps=dense_target_fps,
273
+ dense_target_fps=dense_target_fps,
274
+ sparse_target_fps=sparse_target_fps,
275
+ dense_nominal_frame_count=dense_nominal_frame_count,
276
+ sparse_nominal_frame_count=sparse_nominal_frame_count,
277
+ dense_grid_supported=dense_grid_supported,
278
+ sampling_mode="timestamp_grid",
279
+ )
280
+
281
+
282
+ def sample_frame_indices(
283
+ *,
284
+ total_frames: int,
285
+ source_fps: float,
286
+ target_fps: float,
287
+ max_sampled_frames: int,
288
+ start_frame: int = 0,
289
+ end_frame: int | None = None,
290
+ align_to_multiple: int = 1,
291
+ uniform_over_clip: bool = False,
292
+ ) -> tuple[tuple[int, ...], float]:
293
+ """Sample a strict timestamp grid, uniformly thinning only above the cap.
294
+
295
+ A nominal 1 FPS contract means timestamps ``0s, 1s, 2s, ...`` rather than
296
+ choosing ``round(duration)`` frames with an endpoint-inclusive linspace.
297
+ This preserves the requested cadence for short/medium clips. Only when
298
+ the timestamp grid exceeds ``max_sampled_frames`` do we uniformly thin the
299
+ grid while retaining full-clip coverage. Temporal-tube divisibility is a
300
+ later padding concern and must never remove valid sampled frames here.
301
+ """
302
+
303
+ total_frames = int(total_frames)
304
+ source_fps = float(source_fps)
305
+ target_fps = float(target_fps)
306
+ max_sampled_frames = int(max_sampled_frames)
307
+ align_to_multiple = int(align_to_multiple)
308
+ start_frame = int(start_frame)
309
+ end_frame = total_frames if end_frame is None else int(end_frame)
310
+ if total_frames <= 0:
311
+ raise ValueError(f"total_frames must be positive, got {total_frames}")
312
+ if not math.isfinite(source_fps) or source_fps <= 0:
313
+ raise ValueError(f"source_fps must be positive and finite, got {source_fps}")
314
+ if not math.isfinite(target_fps) or target_fps <= 0:
315
+ raise ValueError(f"target_fps must be positive and finite, got {target_fps}")
316
+ if max_sampled_frames <= 0:
317
+ raise ValueError(f"max_sampled_frames must be positive, got {max_sampled_frames}")
318
+ if align_to_multiple <= 0:
319
+ raise ValueError(f"align_to_multiple must be positive, got {align_to_multiple}")
320
+ if start_frame < 0 or end_frame > total_frames or start_frame >= end_frame:
321
+ raise ValueError(
322
+ f"Invalid frame range [{start_frame}, {end_frame}) for total_frames={total_frames}"
323
+ )
324
+
325
+ available_frames = end_frame - start_frame
326
+ duration = available_frames / source_fps
327
+ if uniform_over_clip:
328
+ count = min(available_frames, max_sampled_frames)
329
+ indices_tensor = torch.linspace(
330
+ start_frame,
331
+ end_frame - 1,
332
+ count,
333
+ dtype=torch.float64,
334
+ ).round().to(torch.int64)
335
+ else:
336
+ sample_period = 1.0 / target_fps
337
+ requested_timestamps = torch.arange(
338
+ 0.0,
339
+ duration,
340
+ sample_period,
341
+ dtype=torch.float64,
342
+ )
343
+ if requested_timestamps.numel() == 0:
344
+ requested_timestamps = torch.zeros(1, dtype=torch.float64)
345
+ # The timestamp grid is mathematically exact at equal source/target
346
+ # rates, but binary floating point can place values such as 7/24 * 24
347
+ # infinitesimally below 7. Flooring those values silently duplicates
348
+ # the preceding frame (96 @ 24 FPS previously collapsed to 85 unique
349
+ # indices). A tiny boundary tolerance preserves the intended integer
350
+ # grid without moving any real sub-frame timestamp across a boundary.
351
+ indices_tensor = torch.floor(
352
+ requested_timestamps * source_fps + 1e-9
353
+ ).to(torch.int64)
354
+ indices_tensor = (indices_tensor + start_frame).clamp(
355
+ min=start_frame,
356
+ max=end_frame - 1,
357
+ )
358
+ indices_tensor = torch.unique_consecutive(indices_tensor)
359
+ if indices_tensor.numel() > max_sampled_frames:
360
+ keep = torch.linspace(
361
+ 0,
362
+ indices_tensor.numel() - 1,
363
+ max_sampled_frames,
364
+ ).round().to(torch.int64)
365
+ indices_tensor = indices_tensor[keep]
366
+ if align_to_multiple > 1 and indices_tensor.numel() >= align_to_multiple:
367
+ aligned = (indices_tensor.numel() // align_to_multiple) * align_to_multiple
368
+ indices_tensor = indices_tensor[:aligned]
369
+ indices = indices_tensor.tolist()
370
+ sampled_frames = len(indices)
371
+ conditioning_fps = sampled_frames / duration
372
+ return tuple(int(index) for index in indices), float(conditioning_fps)
373
+
374
+
375
+ def compute_video_layout(
376
+ *,
377
+ total_frames: int,
378
+ source_fps: float,
379
+ source_height: int,
380
+ source_width: int,
381
+ target_fps: float,
382
+ max_sampled_frames: int,
383
+ max_raw_patch_tokens: int,
384
+ patch_size: int,
385
+ resize_multiple: int,
386
+ min_frame_pixels: int,
387
+ max_frame_pixels: int | None = None,
388
+ temporal_patch_size: int = 1,
389
+ start_frame: int = 0,
390
+ end_frame: int | None = None,
391
+ align_to_temporal_patch: bool = False,
392
+ uniform_over_clip: bool = False,
393
+ ) -> PixelUMMVideoLayout:
394
+ """Resolve sampled frames and H/W under a valid-frame raw-patch budget.
395
+
396
+ Temporal repeat padding changes the number of packed tube tokens, but must
397
+ not lower the spatial resolution selected for the valid sampled frames.
398
+ """
399
+
400
+ max_raw_patch_tokens = int(max_raw_patch_tokens)
401
+ patch_size = int(patch_size)
402
+ resize_multiple = int(resize_multiple)
403
+ min_frame_pixels = int(min_frame_pixels)
404
+ temporal_patch_size = int(temporal_patch_size)
405
+ if (
406
+ max_raw_patch_tokens <= 0
407
+ or patch_size <= 0
408
+ or resize_multiple <= 0
409
+ or temporal_patch_size <= 0
410
+ ):
411
+ raise ValueError(
412
+ "PixelUMM video token, patch, resize, and temporal values must be positive: "
413
+ f"raw_tokens={max_raw_patch_tokens} patch={patch_size} "
414
+ f"multiple={resize_multiple} "
415
+ f"temporal_patch_size={temporal_patch_size}"
416
+ )
417
+ if resize_multiple % patch_size != 0:
418
+ raise ValueError(
419
+ f"resize_multiple={resize_multiple} must be divisible by patch_size={patch_size}"
420
+ )
421
+ if min_frame_pixels <= 0:
422
+ raise ValueError(f"min_frame_pixels must be positive, got {min_frame_pixels}")
423
+
424
+ frame_indices, conditioning_fps = sample_frame_indices(
425
+ total_frames=total_frames,
426
+ source_fps=source_fps,
427
+ target_fps=target_fps,
428
+ max_sampled_frames=max_sampled_frames,
429
+ start_frame=start_frame,
430
+ end_frame=end_frame,
431
+ align_to_multiple=(temporal_patch_size if align_to_temporal_patch else 1),
432
+ uniform_over_clip=uniform_over_clip,
433
+ )
434
+ num_valid_frames = len(frame_indices)
435
+ temporal_groups = math.ceil(num_valid_frames / temporal_patch_size)
436
+ per_frame_max_pixels = max_raw_patch_tokens * patch_size**2 // num_valid_frames
437
+ if max_frame_pixels is not None:
438
+ if int(max_frame_pixels) <= 0:
439
+ raise ValueError(
440
+ f"max_frame_pixels must be positive when set, got {max_frame_pixels}"
441
+ )
442
+ per_frame_max_pixels = min(per_frame_max_pixels, int(max_frame_pixels))
443
+ if per_frame_max_pixels < min_frame_pixels:
444
+ raise ValueError(
445
+ "Video frame cap and minimum spatial budget exceed the total PixelUMM budget: "
446
+ f"frames={num_valid_frames} temporal_groups={temporal_groups} "
447
+ f"per_frame_max_pixels={per_frame_max_pixels} "
448
+ f"min_frame_pixels={min_frame_pixels} "
449
+ f"max_raw_tokens={max_raw_patch_tokens}"
450
+ )
451
+ resized_height, resized_width = smart_resize_pixels(
452
+ int(source_height),
453
+ int(source_width),
454
+ factor=resize_multiple,
455
+ min_pixels=min_frame_pixels,
456
+ max_pixels=per_frame_max_pixels,
457
+ )
458
+ spatial_patch_tokens = (
459
+ (int(resized_height) // patch_size) * (int(resized_width) // patch_size)
460
+ )
461
+ raw_patch_tokens = num_valid_frames * spatial_patch_tokens
462
+ packed_patch_tokens = temporal_groups * spatial_patch_tokens
463
+ if raw_patch_tokens > max_raw_patch_tokens:
464
+ raise RuntimeError(
465
+ "PixelUMM smart resize exceeded its raw-patch budget: "
466
+ f"actual={raw_patch_tokens} max={max_raw_patch_tokens} "
467
+ f"frames={num_valid_frames} temporal_groups={temporal_groups} "
468
+ f"size={resized_height}x{resized_width}"
469
+ )
470
+ if packed_patch_tokens > raw_patch_tokens:
471
+ raise RuntimeError(
472
+ "Packed tube tokens cannot exceed valid-frame raw patch tokens: "
473
+ f"packed={packed_patch_tokens} raw={raw_patch_tokens} "
474
+ f"frames={num_valid_frames} temporal_patch_size={temporal_patch_size}"
475
+ )
476
+ return PixelUMMVideoLayout(
477
+ frame_indices=frame_indices,
478
+ conditioning_fps=conditioning_fps,
479
+ resized_height=int(resized_height),
480
+ resized_width=int(resized_width),
481
+ raw_patch_tokens=int(raw_patch_tokens),
482
+ packed_patch_tokens=int(packed_patch_tokens),
483
+ )
484
+
485
+
486
+ def decode_pixelumm_video(
487
+ *,
488
+ key: str,
489
+ data: bytes,
490
+ min_fps: float,
491
+ max_fps: float,
492
+ target_fps: float,
493
+ dense_target_fps: float | None = None,
494
+ sparse_target_fps: float | None = None,
495
+ max_sampled_frames: int,
496
+ max_raw_patch_tokens: int,
497
+ patch_size: int,
498
+ resize_multiple: int,
499
+ min_frame_pixels: int,
500
+ max_frame_pixels: int | None = None,
501
+ temporal_patch_size: int = 1,
502
+ sparse_max_sampled_frames: int = 0,
503
+ dense_representation_probability: float = 1.0,
504
+ drop_incomplete_dense_tube: bool = False,
505
+ representation_selector_key: str | None = None,
506
+ start_seconds: float | None = None,
507
+ end_seconds: float | None = None,
508
+ required_sampled_frames: int = 0,
509
+ num_threads: int = 1,
510
+ decoding_timeout: int = 60,
511
+ ) -> dict[str, object] | None:
512
+ """Decode a video directly into the PixelUMM V2T model-input layout."""
513
+
514
+ extension = re.sub(r".*[.]", "", str(key)).lower()
515
+ if extension not in _VIDEO_EXTENSIONS:
516
+ return None
517
+ video_reader = _open_video_decoder(data, num_ffmpeg_threads=int(num_threads))
518
+ total_frames = video_reader.metadata.num_frames
519
+ source_fps = video_reader.metadata.average_fps
520
+ if total_frames is None or source_fps is None:
521
+ raise ValueError(
522
+ "torchcodec missing video metadata "
523
+ f"(num_frames={total_frames}, average_fps={source_fps})"
524
+ )
525
+ total_frames = int(total_frames)
526
+ source_fps = float(source_fps)
527
+ required_sampled_frames = int(required_sampled_frames)
528
+ if required_sampled_frames < 0:
529
+ raise ValueError(
530
+ "required_sampled_frames must be non-negative, got "
531
+ f"{required_sampled_frames}"
532
+ )
533
+ if source_fps < float(min_fps) or source_fps > float(max_fps):
534
+ raise PixelUMMVideoRejectedError(
535
+ "source_fps",
536
+ f"Video source_fps={source_fps} is outside "
537
+ f"[{float(min_fps)}, {float(max_fps)}]",
538
+ source_fps=source_fps,
539
+ min_fps=float(min_fps),
540
+ max_fps=float(max_fps),
541
+ )
542
+
543
+ if (start_seconds is None) != (end_seconds is None):
544
+ raise ValueError("start_seconds and end_seconds must be provided together")
545
+ start_frame = 0
546
+ end_frame = total_frames
547
+ if start_seconds is not None:
548
+ start = max(0.0, float(start_seconds))
549
+ end = min(total_frames / source_fps, float(end_seconds))
550
+ if not start < end:
551
+ raise ValueError(
552
+ f"invalid video clip [{start}, {end}] for {total_frames} "
553
+ f"frames at {source_fps} FPS"
554
+ )
555
+ start_frame = min(total_frames - 1, int(math.floor(start * source_fps)))
556
+ end_frame = min(
557
+ total_frames,
558
+ max(start_frame + 1, int(math.ceil(end * source_fps))),
559
+ )
560
+
561
+ representation_draw = (
562
+ stable_video_representation_draw(representation_selector_key)
563
+ if 0.0 < float(dense_representation_probability) < 1.0
564
+ and representation_selector_key is not None
565
+ else None
566
+ )
567
+ representation = select_video_representation(
568
+ total_frames=total_frames,
569
+ source_fps=source_fps,
570
+ target_fps=target_fps,
571
+ dense_target_fps=dense_target_fps,
572
+ sparse_target_fps=sparse_target_fps,
573
+ dense_max_sampled_frames=max_sampled_frames,
574
+ dense_temporal_patch_size=temporal_patch_size,
575
+ sparse_max_sampled_frames=sparse_max_sampled_frames,
576
+ dense_representation_probability=dense_representation_probability,
577
+ representation_draw=representation_draw,
578
+ start_frame=start_frame,
579
+ end_frame=end_frame,
580
+ )
581
+
582
+ frame_indices, _ = sample_frame_indices(
583
+ total_frames=total_frames,
584
+ source_fps=source_fps,
585
+ target_fps=representation.target_fps,
586
+ max_sampled_frames=representation.max_sampled_frames,
587
+ start_frame=start_frame,
588
+ end_frame=end_frame,
589
+ align_to_multiple=(
590
+ representation.temporal_patch_size
591
+ if drop_incomplete_dense_tube and representation.temporal_patch_size > 1
592
+ else 1
593
+ ),
594
+ uniform_over_clip=representation.sampling_mode == "uniform_clip",
595
+ )
596
+
597
+ # This is the authoritative fixed-window filter. It uses the same
598
+ # source metadata, clip bounds, representation policy, timestamp grid,
599
+ # and temporal alignment as the real decode, but runs before any frame
600
+ # payload is materialized. Short dense samples therefore never pay the
601
+ # expensive get_frames_at/decode cost, while acceptance semantics remain
602
+ # exactly identical to the former post-decode shape check.
603
+ sampled_frame_count = len(frame_indices)
604
+ if (
605
+ required_sampled_frames
606
+ and sampled_frame_count != required_sampled_frames
607
+ ):
608
+ raise PixelUMMVideoRejectedError(
609
+ "sampled_frame_count",
610
+ "PixelUMM fixed-window sample has "
611
+ f"{sampled_frame_count} sampled frames; required "
612
+ f"{required_sampled_frames}",
613
+ sampled_frame_count=sampled_frame_count,
614
+ required_sampled_frames=required_sampled_frames,
615
+ total_frames=total_frames,
616
+ source_fps=source_fps,
617
+ clip_start_frame=start_frame,
618
+ clip_end_frame=end_frame,
619
+ )
620
+
621
+ frames = _decode_video_frames_with_timeout(
622
+ video_reader,
623
+ frame_indices,
624
+ timeout_seconds=int(decoding_timeout),
625
+ )
626
+ if frames is None:
627
+ return None
628
+ source_height = int(frames.shape[-2])
629
+ source_width = int(frames.shape[-1])
630
+ layout = compute_video_layout(
631
+ total_frames=total_frames,
632
+ source_fps=source_fps,
633
+ source_height=source_height,
634
+ source_width=source_width,
635
+ target_fps=representation.target_fps,
636
+ max_sampled_frames=representation.max_sampled_frames,
637
+ max_raw_patch_tokens=max_raw_patch_tokens,
638
+ patch_size=patch_size,
639
+ resize_multiple=resize_multiple,
640
+ min_frame_pixels=min_frame_pixels,
641
+ max_frame_pixels=max_frame_pixels,
642
+ temporal_patch_size=representation.temporal_patch_size,
643
+ start_frame=start_frame,
644
+ end_frame=end_frame,
645
+ align_to_temporal_patch=bool(drop_incomplete_dense_tube),
646
+ uniform_over_clip=representation.sampling_mode == "uniform_clip",
647
+ )
648
+ if tuple(frames.shape[-2:]) != (layout.resized_height, layout.resized_width):
649
+ frames = transforms_F.resize(
650
+ frames,
651
+ [layout.resized_height, layout.resized_width],
652
+ interpolation=transforms_F.InterpolationMode.BICUBIC,
653
+ antialias=True,
654
+ )
655
+ return {
656
+ "videos": frames.permute(1, 0, 2, 3).contiguous(),
657
+ "source_fps": source_fps,
658
+ "conditioning_fps": layout.conditioning_fps,
659
+ "frame_indices": layout.frame_indices,
660
+ "original_num_frames": total_frames,
661
+ "source_height": source_height,
662
+ "source_width": source_width,
663
+ "source_duration_seconds": total_frames / source_fps,
664
+ "clip_start_frame": start_frame,
665
+ "clip_end_frame": end_frame,
666
+ "clip_start_seconds": start_frame / source_fps,
667
+ "clip_end_seconds": end_frame / source_fps,
668
+ "sampling_mode": representation.sampling_mode,
669
+ "video_representation": representation.mode,
670
+ "nominal_target_fps_frame_count": representation.nominal_frame_count,
671
+ "dense_nominal_frame_count": representation.dense_nominal_frame_count,
672
+ "sparse_nominal_frame_count": representation.sparse_nominal_frame_count,
673
+ "dense_grid_supported": representation.dense_grid_supported,
674
+ "dense_max_sampled_frames": int(max_sampled_frames),
675
+ "sparse_max_sampled_frames": int(sparse_max_sampled_frames),
676
+ "dense_representation_probability": representation.dense_probability,
677
+ "representation_selection_draw": representation.selection_draw,
678
+ "representation_selection_reason": representation.selection_reason,
679
+ "temporal_patch_size": representation.temporal_patch_size,
680
+ "drop_incomplete_dense_tube": bool(drop_incomplete_dense_tube),
681
+ "target_fps": representation.target_fps,
682
+ "dense_target_fps": representation.dense_target_fps,
683
+ "sparse_target_fps": representation.sparse_target_fps,
684
+ "raw_patch_tokens": layout.raw_patch_tokens,
685
+ "packed_patch_tokens": layout.packed_patch_tokens,
686
+ }
eval/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
eval/vlm/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
eval/vlm/release_preprocess_contract.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """First-party preprocessing constants for the released R07 VLM suites."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ImagePreprocessContract:
13
+ resize_mode: str = "native_smart_resize"
14
+ min_image_pixels: int = 3_136
15
+ max_image_tokens: int = 2_560_000 // (16**2)
16
+ contract_name: str = "pixelumm-r07-image-vlm-v1"
17
+
18
+ @property
19
+ def recipe_names(self) -> tuple[str, ...]:
20
+ """Compatibility label consumed by existing eval receipts."""
21
+
22
+ return (self.contract_name,)
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class VideoPreprocessContract:
27
+ target_fps: float = 1.0
28
+ dense_target_fps: float = 4.0
29
+ sparse_target_fps: float = 1.0
30
+ max_sampled_frames: int = 96
31
+ sparse_max_sampled_frames: int = 96
32
+ dense_representation_probability: float = 0.0
33
+ drop_incomplete_dense_tube: bool = False
34
+ max_raw_patch_tokens: int = 96 * (448 // 16) ** 2
35
+ max_frame_pixels: int = 448**2
36
+ min_frame_pixels: int = 16**2
37
+ temporal_patch_size: int = 4
38
+ timestamp_mode: str = "tube_start"
39
+ mrope_mode: str = "sequence_hw"
40
+ contract_name: str = "pixelumm-r07-video4-sparse-v1"
41
+
42
+
43
+ IMAGE_VLM_CONTRACT = ImagePreprocessContract()
44
+ VIDEO_VLM_CONTRACT = VideoPreprocessContract()
examples/cats.jpg ADDED

Git LFS Details

  • SHA256: dea9e7ef97386345f7cff32f9055da4982da5471c48d575146c796ab4563b04e
  • Pointer size: 131 Bytes
  • Size of remote file: 173 kB
examples/street.jpg ADDED

Git LFS Details

  • SHA256: ffe0f0cec3b2e27aab1967229cdf0a0d7751dcdd5800322f0b8ac0dffb3b8a8d
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
experiments/s8_f22_r07/release.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ schema: pixelumm_scientific_config_v1
2
+
3
+ # Model configuration for the default PixelUMM checkpoint.
4
+ model:
5
+ llm_backend: qwen3
6
+ qwen_mlp_sequence_chunk_size: 32768
7
+ qwen3_mrope_type: sensenova
8
+ qwen3_rope_theta: 1000000.0
9
+ llm_path: Qwen/Qwen3-8B
10
+ layer_module: Qwen3MoTDecoderLayer
11
+ enable_pixel_video: true
12
+ pixel_token_patch_size: 16
13
+ pixel_embedder_type: image_raw_patch_linear
14
+ pixel_separate_und_gen_embedder: true
15
+ pixel_video_embedder_type: video_raw_tube_linear
16
+ pixel_video_separate_und_gen_embedder: true
17
+ pixel_video_temporal_patch_size: 4
18
+ pixel_image_additive_pos_embed_type: none
19
+ pixel_video_additive_pos_embed_type: none
20
+ pixel_head_type: minit2i_linear
21
+ pixel_video_head_type: jit_style
22
+ sensenova_vision_rope_theta: 10000.0
23
+ sensenova_vision_max_position_embeddings: 10000
24
+ add_timestep_embedding: false
25
+ add_noise_scale_embedding: false
26
+ noise_scale: 1.0
27
+ noise_scale_mode: constant
28
+ packed_attention_impl: flex
29
+ inference_attention_backend: flash_attn_varlen
30
+ packed_expert_routing: pixelumm_mot
31
+ ce_loss_checkpoint_chunk_size: 4096
32
+ text_cond_dropout_prob: 0.1
33
+
34
+ # Only the sequence layout is part of model construction. Dataset identity and
35
+ # distributed data-position semantics are deliberately absent from the release path.
36
+ data:
37
+ packed_sequence_layout: pixelumm_mot
38
+
39
+ # Objective fields are retained because they are serialized into PixelUMMConfig
40
+ # and are also reused by the local toy-training example.
41
+ training:
42
+ gen_space: pixel
43
+ und_space: pixel
44
+ prediction_type: x
45
+ loss_type: v_loss
46
+ x_pred_t_min: 0.05
47
+ visual_gen: true
48
+ visual_und: true
49
+ freeze_und: false
50
+ conditional_branch_gradient_contract: pixelumm_zero_v1
51
+ noise_time_logit_mean: 0.0
52
+ noise_time_logit_std: 1.0
53
+ time_schedule: pixelumm_resolution
54
+ mse_loss_reduction: sample
experiments/s8_f22_r07/t2v_negative_prompt.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ The video captures a series of frames showing ugly scenes, static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Overall, the video is of poor quality.
modeling/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from . import pixelumm, qwen3
6
+
7
+ __all__ = ["pixelumm", "qwen3"]
modeling/pixelumm/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+
6
+ from modeling.qwen3.configuration_qwen3 import Qwen3Config
7
+
8
+ from .pixelumm import PixelUMM, PixelUMMConfig
9
+ from .qwen3_navit import Qwen3ForCausalLM
10
+
11
+
12
+ __all__ = [
13
+ "PixelUMMConfig",
14
+ "PixelUMM",
15
+ "Qwen3Config",
16
+ "Qwen3ForCausalLM",
17
+ ]
modeling/pixelumm/dpm_solver.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 NVIDIA CORPORATION & AFFILIATES
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Minimal flow-matching DPM-Solver++ used by PixelUMM image/video inference.
18
+
19
+ This is the exact release path: flow prediction, shifted uniform-flow time
20
+ steps, and second-order multistep DPM-Solver++. Generic VP schedules,
21
+ classifier/PAG guidance, inverse solving, adaptive stepping, and third-order
22
+ solvers are intentionally not part of the public runtime.
23
+ """
24
+
25
+ from collections.abc import Callable
26
+
27
+ import torch
28
+ from tqdm import tqdm
29
+
30
+
31
+ def sample_flow_dpm(
32
+ velocity_fn: Callable[[torch.Tensor, float], torch.Tensor],
33
+ x: torch.Tensor,
34
+ *,
35
+ num_steps: int,
36
+ shift: float,
37
+ ) -> torch.Tensor:
38
+ """Sample image or video flow; the caller owns conditioning and CFG."""
39
+ noise_schedule = NoiseScheduleFlow(schedule="discrete_flow")
40
+
41
+ def model(x: torch.Tensor, t_input: torch.Tensor) -> torch.Tensor:
42
+ t_value = t_input.flatten()[0].item() / noise_schedule.total_N
43
+ return velocity_fn(x, t_value)
44
+
45
+ model_fn = model_wrapper(
46
+ model, noise_schedule, model_type="flow", guidance_type="uncond"
47
+ )
48
+ solver = DPM_Solver(model_fn, noise_schedule, algorithm_type="dpmsolver++")
49
+ return solver.sample(
50
+ x,
51
+ steps=num_steps,
52
+ order=2,
53
+ skip_type="time_uniform_flow",
54
+ method="multistep",
55
+ flow_shift=shift,
56
+ t_start=1.0,
57
+ t_end=0.001,
58
+ )
59
+
60
+
61
+ class NoiseScheduleFlow:
62
+ """Rectified-flow schedule used by the released PixelUMM checkpoint."""
63
+
64
+ def __init__(self, schedule: str = "discrete_flow") -> None:
65
+ if schedule != "discrete_flow":
66
+ raise ValueError(f"PixelUMM requires schedule='discrete_flow', got {schedule!r}")
67
+ self.T = 1.0
68
+ self.t0 = 0.001
69
+ self.schedule = schedule
70
+ self.total_N = 1000
71
+
72
+ @staticmethod
73
+ def marginal_alpha(t: torch.Tensor) -> torch.Tensor:
74
+ return 1 - t
75
+
76
+ def marginal_log_mean_coeff(self, t: torch.Tensor) -> torch.Tensor:
77
+ return torch.log(self.marginal_alpha(t))
78
+
79
+ @staticmethod
80
+ def marginal_std(t: torch.Tensor) -> torch.Tensor:
81
+ return t
82
+
83
+ def marginal_lambda(self, t: torch.Tensor) -> torch.Tensor:
84
+ return self.marginal_log_mean_coeff(t) - torch.log(self.marginal_std(t))
85
+
86
+
87
+ def _expand_dims(value: torch.Tensor, dims: int) -> torch.Tensor:
88
+ return value[(...,) + (None,) * (dims - 1)]
89
+
90
+
91
+ def model_wrapper(
92
+ model: Callable[..., torch.Tensor],
93
+ noise_schedule: NoiseScheduleFlow,
94
+ *,
95
+ model_type: str = "flow",
96
+ guidance_type: str = "uncond",
97
+ ) -> Callable[[torch.Tensor, torch.Tensor], torch.Tensor]:
98
+ """Convert PixelUMM's flow velocity into DPM-Solver noise prediction."""
99
+
100
+ if model_type != "flow":
101
+ raise ValueError(f"PixelUMM DPM inference requires model_type='flow', got {model_type!r}")
102
+ if guidance_type != "uncond":
103
+ raise ValueError(f"PixelUMM applies CFG inside the model; got guidance_type={guidance_type!r}")
104
+ if noise_schedule.schedule != "discrete_flow":
105
+ raise ValueError(f"Unsupported noise schedule {noise_schedule.schedule!r}")
106
+
107
+ def model_fn(x: torch.Tensor, t_continuous: torch.Tensor) -> torch.Tensor:
108
+ t_input = t_continuous * noise_schedule.total_N
109
+ velocity = model(x, t_input)
110
+ if not isinstance(velocity, torch.Tensor):
111
+ raise TypeError(f"PixelUMM flow model must return a Tensor, got {type(velocity).__name__}")
112
+ sigma_t = noise_schedule.marginal_std(t_continuous)
113
+ return (1 - _expand_dims(sigma_t, x.dim()).to(x)) * velocity + x
114
+
115
+ return model_fn
116
+
117
+
118
+ class DPM_Solver:
119
+ """Second-order multistep DPM-Solver++ for the PixelUMM flow schedule."""
120
+
121
+ def __init__(
122
+ self,
123
+ model_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor],
124
+ noise_schedule: NoiseScheduleFlow,
125
+ algorithm_type: str = "dpmsolver++",
126
+ ) -> None:
127
+ if algorithm_type != "dpmsolver++":
128
+ raise ValueError(f"PixelUMM requires algorithm_type='dpmsolver++', got {algorithm_type!r}")
129
+ if noise_schedule.schedule != "discrete_flow":
130
+ raise ValueError(f"Unsupported noise schedule {noise_schedule.schedule!r}")
131
+ self.model = lambda x, t: model_fn(x, t.expand(x.shape[0]))
132
+ self.noise_schedule = noise_schedule
133
+
134
+ def _data_prediction(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
135
+ noise = self.model(x, t)
136
+ alpha_t = self.noise_schedule.marginal_alpha(t)
137
+ sigma_t = self.noise_schedule.marginal_std(t)
138
+ return (x - sigma_t * noise) / alpha_t
139
+
140
+ @staticmethod
141
+ def _time_steps(
142
+ *,
143
+ t_start: float,
144
+ t_end: float,
145
+ steps: int,
146
+ device: torch.device,
147
+ flow_shift: float,
148
+ ) -> torch.Tensor:
149
+ betas = torch.linspace(t_start, t_end, steps + 1, device=device)
150
+ sigmas = 1.0 - betas
151
+ return (flow_shift * sigmas / (1 + (flow_shift - 1) * sigmas)).flip(dims=[0])
152
+
153
+ def _first_order_update(
154
+ self,
155
+ x: torch.Tensor,
156
+ s: torch.Tensor,
157
+ t: torch.Tensor,
158
+ model_s: torch.Tensor,
159
+ ) -> torch.Tensor:
160
+ schedule = self.noise_schedule
161
+ h = schedule.marginal_lambda(t) - schedule.marginal_lambda(s)
162
+ sigma_s = schedule.marginal_std(s)
163
+ sigma_t = schedule.marginal_std(t)
164
+ alpha_t = torch.exp(schedule.marginal_log_mean_coeff(t))
165
+ phi_1 = torch.expm1(-h)
166
+ return sigma_t / sigma_s * x - alpha_t * phi_1 * model_s
167
+
168
+ def _second_order_update(
169
+ self,
170
+ x: torch.Tensor,
171
+ model_prev: list[torch.Tensor],
172
+ t_prev: list[torch.Tensor],
173
+ t: torch.Tensor,
174
+ ) -> torch.Tensor:
175
+ schedule = self.noise_schedule
176
+ model_prev_1, model_prev_0 = model_prev[-2], model_prev[-1]
177
+ t_prev_1, t_prev_0 = t_prev[-2], t_prev[-1]
178
+ lambda_prev_1 = schedule.marginal_lambda(t_prev_1)
179
+ lambda_prev_0 = schedule.marginal_lambda(t_prev_0)
180
+ lambda_t = schedule.marginal_lambda(t)
181
+ sigma_prev_0 = schedule.marginal_std(t_prev_0)
182
+ sigma_t = schedule.marginal_std(t)
183
+ alpha_t = torch.exp(schedule.marginal_log_mean_coeff(t))
184
+
185
+ h_0 = lambda_prev_0 - lambda_prev_1
186
+ h = lambda_t - lambda_prev_0
187
+ d1 = (h / h_0) * (model_prev_0 - model_prev_1)
188
+ phi_1 = torch.expm1(-h)
189
+ return (
190
+ (sigma_t / sigma_prev_0) * x
191
+ - (alpha_t * phi_1) * model_prev_0
192
+ - 0.5 * (alpha_t * phi_1) * d1
193
+ )
194
+
195
+ @torch.no_grad()
196
+ def sample(
197
+ self,
198
+ x: torch.Tensor,
199
+ *,
200
+ steps: int = 20,
201
+ t_start: float = 1.0,
202
+ t_end: float = 0.001,
203
+ order: int = 2,
204
+ skip_type: str = "time_uniform_flow",
205
+ method: str = "multistep",
206
+ lower_order_final: bool = True,
207
+ solver_type: str = "dpmsolver",
208
+ flow_shift: float = 1.0,
209
+ ) -> torch.Tensor:
210
+ """Sample with the one solver configuration shipped by PixelUMM."""
211
+
212
+ if steps < 2:
213
+ raise ValueError(f"DPM-Solver++ requires at least two steps, got {steps}")
214
+ if order != 2:
215
+ raise ValueError(f"PixelUMM DPM inference requires order=2, got {order}")
216
+ if skip_type != "time_uniform_flow":
217
+ raise ValueError(f"PixelUMM requires skip_type='time_uniform_flow', got {skip_type!r}")
218
+ if method != "multistep":
219
+ raise ValueError(f"PixelUMM requires method='multistep', got {method!r}")
220
+ if solver_type != "dpmsolver":
221
+ raise ValueError(f"PixelUMM requires solver_type='dpmsolver', got {solver_type!r}")
222
+ if not lower_order_final:
223
+ raise ValueError("PixelUMM requires lower_order_final=True")
224
+ if t_start <= 0 or t_end <= 0:
225
+ raise ValueError("DPM-Solver time range must be positive")
226
+ if flow_shift <= 0:
227
+ raise ValueError(f"flow_shift must be positive, got {flow_shift}")
228
+
229
+ timesteps = self._time_steps(
230
+ t_start=t_start,
231
+ t_end=t_end,
232
+ steps=steps,
233
+ device=x.device,
234
+ flow_shift=flow_shift,
235
+ )
236
+ t_prev = [timesteps[0]]
237
+ model_prev = [self._data_prediction(x, timesteps[0])]
238
+
239
+ x = self._first_order_update(x, t_prev[-1], timesteps[1], model_prev[-1])
240
+ t_prev.append(timesteps[1])
241
+ model_prev.append(self._data_prediction(x, timesteps[1]))
242
+
243
+ for step in tqdm(range(2, steps + 1), desc="DPM-Solver++"):
244
+ t = timesteps[step]
245
+ if step == steps:
246
+ x = self._first_order_update(x, t_prev[-1], t, model_prev[-1])
247
+ else:
248
+ x = self._second_order_update(x, model_prev, t_prev, t)
249
+ t_prev[0], t_prev[1] = t_prev[1], t
250
+ model_prev[0] = model_prev[1]
251
+ if step < steps:
252
+ model_prev[1] = self._data_prediction(x, t)
253
+ return x
modeling/pixelumm/flow_unipc.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """PixelUMM flow-UniPC inference loop with no PixelUMM runtime dependency."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Callable
9
+
10
+ import torch
11
+
12
+ from .flow_unipc_scheduler import FlowUniPCScheduler
13
+
14
+
15
+ _NUM_TRAIN_TIMESTEPS = 1000
16
+
17
+
18
+ @torch.no_grad()
19
+ def sample_flow_unipc(
20
+ velocity_fn: Callable[[torch.Tensor, float], torch.Tensor],
21
+ initial_noise: torch.Tensor,
22
+ *,
23
+ num_steps: int = 35,
24
+ shift: float = 1.0,
25
+ ) -> torch.Tensor:
26
+ """Integrate a PixelUMM flow-prediction trajectory with UniPC.
27
+
28
+ The public callback receives normalized timesteps in ``[0, 1]``. The
29
+ local scheduler retains the exact R07 discrete ``[0, 1000]`` convention
30
+ internally and keeps the comparatively small solver state in FP32.
31
+ """
32
+ if not torch.is_floating_point(initial_noise):
33
+ raise TypeError("UniPC initial_noise must be floating point")
34
+ if num_steps < 1:
35
+ raise ValueError(f"UniPC num_steps must be positive, got {num_steps}")
36
+ if not torch.isfinite(torch.tensor(shift)) or shift <= 0:
37
+ raise ValueError(f"UniPC shift must be finite and positive, got {shift}")
38
+
39
+ solver_state = (
40
+ initial_noise.float()
41
+ if initial_noise.dtype in (torch.float16, torch.bfloat16)
42
+ else initial_noise
43
+ )
44
+ scheduler = FlowUniPCScheduler(num_train_timesteps=_NUM_TRAIN_TIMESTEPS)
45
+ scheduler.set_timesteps(
46
+ int(num_steps),
47
+ device=initial_noise.device,
48
+ shift=float(shift),
49
+ )
50
+
51
+ latent = solver_state
52
+ for timestep in scheduler.timesteps:
53
+ normalized_timestep = float(timestep.item()) / _NUM_TRAIN_TIMESTEPS
54
+ velocity = velocity_fn(latent, normalized_timestep)
55
+ if velocity.shape != latent.shape:
56
+ raise ValueError(
57
+ "UniPC velocity_fn must preserve sample shape: "
58
+ f"expected {tuple(latent.shape)}, got {tuple(velocity.shape)}"
59
+ )
60
+ latent = scheduler.step(
61
+ model_output=velocity,
62
+ timestep=timestep,
63
+ sample=latent.unsqueeze(0),
64
+ ).squeeze(0)
65
+
66
+ return latent
67
+
68
+
69
+ __all__ = ["sample_flow_unipc"]
modeling/pixelumm/flow_unipc_scheduler.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Minimal flow-prediction UniPC scheduler used by PixelUMM inference.
5
+
6
+ Adapted from the Apache-2.0 licensed Wan2.2 flow-UniPC implementation at
7
+ ``wan/utils/fm_solvers_unipc.py`` (Wan-Video/Wan2.2 commit
8
+ 42bf4cfaa384bc21833865abc2f9e6c0e67233dc), itself adapted from Diffusers
9
+ v0.31.0. This modified file intentionally retains only the fixed execution
10
+ path used by the released PixelUMM R07 inference contract: second-order UniPC,
11
+ flow prediction, B(h) solver type ``bh2``, no dynamic thresholding, and a
12
+ zero terminal sigma.
13
+
14
+ Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
15
+ Licensed under the Apache License, Version 2.0.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+
22
+ import numpy as np
23
+ import torch
24
+
25
+
26
+ class FlowUniPCScheduler:
27
+ """Fixed-contract flow-UniPC scheduler for released PixelUMM models."""
28
+
29
+ solver_order = 2
30
+
31
+ def __init__(self, *, num_train_timesteps: int = 1000) -> None:
32
+ if num_train_timesteps < 1:
33
+ raise ValueError("num_train_timesteps must be positive")
34
+ self.num_train_timesteps = int(num_train_timesteps)
35
+
36
+ alphas = np.linspace(
37
+ 1,
38
+ 1 / self.num_train_timesteps,
39
+ self.num_train_timesteps,
40
+ )[::-1].copy()
41
+ sigmas = 1.0 - alphas
42
+ self.sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32, device="cpu")
43
+ self.sigma_min = self.sigmas[-1].item()
44
+ self.sigma_max = self.sigmas[0].item()
45
+
46
+ self.timesteps = self.sigmas * self.num_train_timesteps
47
+ self.model_outputs: list[torch.Tensor | None] = [None] * self.solver_order
48
+ self.timestep_list: list[torch.Tensor | None] = [None] * self.solver_order
49
+ self.lower_order_nums = 0
50
+ self.last_sample: torch.Tensor | None = None
51
+ self.this_order = 0
52
+ self.step_index: int | None = None
53
+ self.num_inference_steps: int | None = None
54
+
55
+ def set_timesteps(
56
+ self,
57
+ num_inference_steps: int,
58
+ *,
59
+ device: torch.device,
60
+ shift: float,
61
+ ) -> None:
62
+ if num_inference_steps < 1:
63
+ raise ValueError("num_inference_steps must be positive")
64
+ if not math.isfinite(shift) or shift <= 0:
65
+ raise ValueError("shift must be finite and positive")
66
+
67
+ sigmas = np.linspace(
68
+ self.sigma_max,
69
+ self.sigma_min,
70
+ num_inference_steps + 1,
71
+ ).copy()[:-1]
72
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
73
+ timesteps = sigmas * self.num_train_timesteps
74
+ sigmas = np.concatenate([sigmas, [0.0]]).astype(np.float32)
75
+
76
+ self.sigmas = torch.from_numpy(sigmas).to("cpu")
77
+ self.timesteps = torch.from_numpy(timesteps).to(device=device, dtype=torch.int64)
78
+ self.num_inference_steps = len(timesteps)
79
+ self.model_outputs = [None] * self.solver_order
80
+ self.timestep_list = [None] * self.solver_order
81
+ self.lower_order_nums = 0
82
+ self.last_sample = None
83
+ self.this_order = 0
84
+ self.step_index = None
85
+
86
+ @staticmethod
87
+ def _alpha_sigma(sigma: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
88
+ return 1 - sigma, sigma
89
+
90
+ def _convert_velocity(
91
+ self,
92
+ velocity: torch.Tensor,
93
+ sample: torch.Tensor,
94
+ ) -> torch.Tensor:
95
+ assert self.step_index is not None
96
+ sigma = self.sigmas[self.step_index]
97
+ return sample - sigma * velocity
98
+
99
+ def _predictor_update(
100
+ self,
101
+ sample: torch.Tensor,
102
+ *,
103
+ order: int,
104
+ ) -> torch.Tensor:
105
+ assert self.step_index is not None
106
+ if order not in (1, 2):
107
+ raise ValueError(f"unsupported UniPC predictor order: {order}")
108
+
109
+ model_output_list = self.model_outputs
110
+ s0 = self.timestep_list[-1]
111
+ m0 = model_output_list[-1]
112
+ if s0 is None or m0 is None:
113
+ raise RuntimeError("UniPC predictor state is incomplete")
114
+
115
+ sigma_t_raw = self.sigmas[self.step_index + 1]
116
+ sigma_s0_raw = self.sigmas[self.step_index]
117
+ alpha_t, sigma_t = self._alpha_sigma(sigma_t_raw)
118
+ alpha_s0, sigma_s0 = self._alpha_sigma(sigma_s0_raw)
119
+ lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
120
+ lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
121
+ h = lambda_t - lambda_s0
122
+
123
+ hh = -h
124
+ h_phi_1 = torch.expm1(hh)
125
+ b_h = torch.expm1(hh)
126
+
127
+ correction: torch.Tensor | int = 0
128
+ if order == 2:
129
+ previous = model_output_list[-2]
130
+ if previous is None:
131
+ raise RuntimeError("UniPC second-order predictor state is incomplete")
132
+ previous_sigma = self.sigmas[self.step_index - 1]
133
+ previous_alpha, previous_sigma = self._alpha_sigma(previous_sigma)
134
+ previous_lambda = torch.log(previous_alpha) - torch.log(previous_sigma)
135
+ rk = (previous_lambda - lambda_s0) / h
136
+ d1 = (previous - m0) / rk
137
+ correction = 0.5 * d1
138
+
139
+ predicted = sigma_t / sigma_s0 * sample - alpha_t * h_phi_1 * m0
140
+ predicted = predicted - alpha_t * b_h * correction
141
+ return predicted.to(sample.dtype)
142
+
143
+ def _corrector_update(
144
+ self,
145
+ this_model_output: torch.Tensor,
146
+ *,
147
+ last_sample: torch.Tensor,
148
+ this_sample: torch.Tensor,
149
+ order: int,
150
+ ) -> torch.Tensor:
151
+ assert self.step_index is not None
152
+ if order not in (1, 2):
153
+ raise ValueError(f"unsupported UniPC corrector order: {order}")
154
+
155
+ model_output_list = self.model_outputs
156
+ m0 = model_output_list[-1]
157
+ if m0 is None:
158
+ raise RuntimeError("UniPC corrector state is incomplete")
159
+
160
+ sigma_t_raw = self.sigmas[self.step_index]
161
+ sigma_s0_raw = self.sigmas[self.step_index - 1]
162
+ alpha_t, sigma_t = self._alpha_sigma(sigma_t_raw)
163
+ alpha_s0, sigma_s0 = self._alpha_sigma(sigma_s0_raw)
164
+ lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
165
+ lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
166
+ h = lambda_t - lambda_s0
167
+
168
+ hh = -h
169
+ h_phi_1 = torch.expm1(hh)
170
+ h_phi_2 = h_phi_1 / hh - 1
171
+ b_h = torch.expm1(hh)
172
+
173
+ if order == 1:
174
+ rhos = torch.tensor([0.5], dtype=last_sample.dtype, device=last_sample.device)
175
+ correction: torch.Tensor | int = 0
176
+ else:
177
+ previous = model_output_list[-2]
178
+ if previous is None:
179
+ raise RuntimeError("UniPC second-order corrector state is incomplete")
180
+ previous_sigma = self.sigmas[self.step_index - 2]
181
+ previous_alpha, previous_sigma = self._alpha_sigma(previous_sigma)
182
+ previous_lambda = torch.log(previous_alpha) - torch.log(previous_sigma)
183
+ rk = (previous_lambda - lambda_s0) / h
184
+ d1 = (previous - m0) / rk
185
+
186
+ rks = torch.tensor([rk, 1.0], device=last_sample.device)
187
+ matrix = torch.stack([torch.ones_like(rks), rks])
188
+ h_phi_3 = h_phi_2 / hh - 0.5
189
+ rhs = torch.tensor(
190
+ [h_phi_2 / b_h, h_phi_3 * 2 / b_h],
191
+ device=last_sample.device,
192
+ )
193
+ rhos = torch.linalg.solve(matrix, rhs).to(last_sample.dtype)
194
+ correction = rhos[0] * d1
195
+
196
+ d1_t = this_model_output - m0
197
+ corrected = sigma_t / sigma_s0 * last_sample - alpha_t * h_phi_1 * m0
198
+ corrected = corrected - alpha_t * b_h * (correction + rhos[-1] * d1_t)
199
+ return corrected.to(last_sample.dtype)
200
+
201
+ def _index_for_timestep(self, timestep: torch.Tensor) -> int:
202
+ indices = (self.timesteps == timestep.to(self.timesteps.device)).nonzero()
203
+ if len(indices) == 0:
204
+ raise ValueError(f"timestep {timestep.item()} is not in the inference schedule")
205
+ position = 1 if len(indices) > 1 else 0
206
+ return indices[position].item()
207
+
208
+ def step(
209
+ self,
210
+ *,
211
+ model_output: torch.Tensor,
212
+ timestep: torch.Tensor,
213
+ sample: torch.Tensor,
214
+ ) -> torch.Tensor:
215
+ if self.num_inference_steps is None:
216
+ raise RuntimeError("set_timesteps must be called before step")
217
+ if self.step_index is None:
218
+ self.step_index = self._index_for_timestep(timestep)
219
+
220
+ use_corrector = self.step_index > 0 and self.last_sample is not None
221
+ converted = self._convert_velocity(model_output, sample)
222
+ if use_corrector:
223
+ sample = self._corrector_update(
224
+ converted,
225
+ last_sample=self.last_sample,
226
+ this_sample=sample,
227
+ order=self.this_order,
228
+ )
229
+
230
+ self.model_outputs[0] = self.model_outputs[1]
231
+ self.timestep_list[0] = self.timestep_list[1]
232
+ self.model_outputs[1] = converted
233
+ self.timestep_list[1] = timestep
234
+
235
+ remaining_steps = len(self.timesteps) - self.step_index
236
+ order = min(self.solver_order, remaining_steps)
237
+ self.this_order = min(order, self.lower_order_nums + 1)
238
+ if self.this_order <= 0:
239
+ raise RuntimeError("invalid UniPC solver order")
240
+
241
+ self.last_sample = sample
242
+ previous = self._predictor_update(sample, order=self.this_order)
243
+ if self.lower_order_nums < self.solver_order:
244
+ self.lower_order_nums += 1
245
+ self.step_index += 1
246
+ return previous
247
+
248
+
249
+ __all__ = ["FlowUniPCScheduler"]
modeling/pixelumm/loss_reduction.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Loss weights used by released PixelUMM training."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Sequence
9
+
10
+ import torch
11
+
12
+
13
+ def sample_mean_token_weights(
14
+ group_lens: torch.Tensor | Sequence[int],
15
+ *,
16
+ output_size: int,
17
+ device: torch.device,
18
+ ) -> torch.Tensor:
19
+ """Expand per-group ``1/N`` weights to aligned token weights.
20
+
21
+ Summing one group's weighted token losses yields that media item's mean
22
+ loss. Dividing by the global weight denominator therefore computes an
23
+ exact global mean over generated media items, even when ranks pack
24
+ different numbers of examples.
25
+ """
26
+ if not torch.is_tensor(group_lens):
27
+ group_lens = torch.tensor(group_lens, dtype=torch.long, device=device)
28
+ else:
29
+ group_lens = group_lens.to(device=device, dtype=torch.long)
30
+ group_lens = group_lens.reshape(-1)
31
+ if group_lens.numel() == 0 and output_size != 0:
32
+ raise ValueError("Non-empty token output requires at least one loss group")
33
+ if group_lens.device.type == "cpu" and bool((group_lens <= 0).any()):
34
+ raise ValueError(f"Loss group lengths must be positive, got {group_lens.tolist()}")
35
+ token_weights = torch.repeat_interleave(
36
+ torch.reciprocal(group_lens.to(dtype=torch.float32)),
37
+ group_lens,
38
+ output_size=int(output_size),
39
+ )
40
+ if token_weights.shape[0] != int(output_size):
41
+ raise ValueError(
42
+ "Loss group lengths do not match output size: "
43
+ f"{token_weights.shape[0]} vs {output_size}"
44
+ )
45
+ return token_weights
46
+
47
+
48
+ def loss_weights_for_reduction(
49
+ token_losses: torch.Tensor,
50
+ reduction: str,
51
+ *,
52
+ non_token_weights: torch.Tensor | None = None,
53
+ ) -> torch.Tensor:
54
+ """Return aligned weights for an explicit loss-reduction contract."""
55
+ if reduction == "token":
56
+ return torch.ones(
57
+ token_losses.numel(),
58
+ dtype=torch.float32,
59
+ device=token_losses.device,
60
+ )
61
+ if non_token_weights is None:
62
+ raise ValueError(f"{reduction} reduction requires aligned loss weights")
63
+ non_token_weights = non_token_weights.reshape(-1).to(
64
+ device=token_losses.device,
65
+ dtype=torch.float32,
66
+ )
67
+ if non_token_weights.numel() != token_losses.numel():
68
+ raise ValueError(
69
+ f"{reduction} weight length mismatch: "
70
+ f"{non_token_weights.numel()} vs {token_losses.numel()}"
71
+ )
72
+ return non_token_weights
modeling/pixelumm/modeling_utils.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 Facebook, Inc. and its affiliates.
2
+ # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: CC BY-NC 4.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 CC BY-NC 4.0, with the full license text
8
+ # available at https://github.com/facebookresearch/DiT/blob/main/LICENSE.txt.
9
+ #
10
+ # This modified file is released under the same license.
11
+
12
+ import math
13
+
14
+ import numpy as np
15
+ import torch
16
+ from torch import nn
17
+
18
+ # --------------------------------------------------------
19
+ # TimestepEmbedder
20
+ # Reference:
21
+ # DiT: https://github.com/facebookresearch/DiT/blob/main/models.py
22
+ # --------------------------------------------------------
23
+ class TimestepEmbedder(nn.Module):
24
+ """
25
+ Embeds scalar timesteps into vector representations.
26
+ """
27
+ def __init__(self, hidden_size, frequency_embedding_size=256):
28
+ super().__init__()
29
+ self.mlp = nn.Sequential(
30
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
31
+ nn.SiLU(),
32
+ nn.Linear(hidden_size, hidden_size, bias=True),
33
+ )
34
+ self.frequency_embedding_size = frequency_embedding_size
35
+
36
+ @staticmethod
37
+ def timestep_embedding(t, dim, max_period=10000):
38
+ """
39
+ Create sinusoidal timestep embeddings.
40
+ :param t: a 1-D Tensor of N indices, one per batch element.
41
+ These may be fractional.
42
+ :param dim: the dimension of the output.
43
+ :param max_period: controls the minimum frequency of the embeddings.
44
+ :return: an (N, D) Tensor of positional embeddings.
45
+ """
46
+ half = dim // 2
47
+ freqs = torch.exp(
48
+ -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
49
+ ).to(device=t.device)
50
+ args = t[:, None].float() * freqs[None]
51
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
52
+ if dim % 2:
53
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
54
+ return embedding
55
+
56
+ def forward(self, t):
57
+ t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
58
+ t_emb = self.mlp(t_freq)
59
+ return t_emb
60
+
61
+
62
+ # ======================== Pixel-space modules ========================
63
+
64
+ def patchify_image(image, patch_size: int, max_size: int):
65
+ """Patchify a PIL image into flat pixel patches in [-1, 1].
66
+
67
+ Args:
68
+ image: PIL.Image (any size)
69
+ patch_size: patch size (e.g. 16)
70
+ max_size: longest edge resize target
71
+ Returns:
72
+ patches: (num_patches, patch_size*patch_size*3)
73
+ shape: (h_patches, w_patches)
74
+ """
75
+ from PIL import Image
76
+ img = image.convert("RGB")
77
+ orig_w, orig_h = img.size
78
+ scale = max_size / max(orig_w, orig_h)
79
+ new_w = max(patch_size, round(orig_w * scale / patch_size) * patch_size)
80
+ new_h = max(patch_size, round(orig_h * scale / patch_size) * patch_size)
81
+ img = img.resize((new_w, new_h), Image.BICUBIC)
82
+ img_tensor = torch.from_numpy(np.array(img)).float() / 255.0 * 2.0 - 1.0
83
+ H, W, C = img_tensor.shape
84
+ h_patches = H // patch_size
85
+ w_patches = W // patch_size
86
+ patches = img_tensor.reshape(h_patches, patch_size, w_patches, patch_size, C)
87
+ patches = patches.permute(0, 2, 1, 3, 4).reshape(-1, patch_size * patch_size * C)
88
+ return patches, (h_patches, w_patches)
89
+
90
+
91
+
92
+
93
+ class RawPixelPatchLinearEmbed(nn.Module):
94
+ """Single-linear image patch embedding: raw pixel patch -> hidden_size."""
95
+
96
+ def __init__(self, patch_size=32, in_channels=3, hidden_size=3584):
97
+ super().__init__()
98
+ self.patch_size = patch_size
99
+ self.in_channels = in_channels
100
+ self.patch_dim = patch_size * patch_size * in_channels
101
+ self.proj = nn.Linear(self.patch_dim, hidden_size, bias=True)
102
+
103
+ def forward(self, pixel_patches):
104
+ """pixel_patches: (num_patches, patch_size*patch_size*3) -> (num_patches, hidden_size)"""
105
+ if pixel_patches.shape[-1] != self.patch_dim:
106
+ raise ValueError(
107
+ f"RawPixelPatchLinearEmbed expected last dim {self.patch_dim} "
108
+ f"for {self.patch_size}x{self.patch_size}x{self.in_channels} patches, "
109
+ f"got {pixel_patches.shape[-1]}"
110
+ )
111
+ return self.proj(pixel_patches)
112
+
113
+
114
+ class RawPixelVideoTubeLinearEmbed(nn.Module):
115
+ """Single-linear video tube embedding: raw pixel tube -> hidden_size."""
116
+
117
+ def __init__(self, patch_size=32, in_channels=3, temporal_patch_size=4, hidden_size=3584):
118
+ super().__init__()
119
+ self.patch_size = patch_size
120
+ self.in_channels = in_channels
121
+ self.temporal_patch_size = temporal_patch_size
122
+ self.patch_dim = patch_size * patch_size * in_channels
123
+ self.tube_dim = temporal_patch_size * self.patch_dim
124
+ self.proj = nn.Linear(self.tube_dim, hidden_size, bias=True)
125
+
126
+ def forward(self, pixel_video_tubes):
127
+ """pixel_video_tubes: (num_tubes, temporal_patch_size*patch_size*patch_size*3) -> hidden."""
128
+ if pixel_video_tubes.shape[-1] != self.tube_dim:
129
+ raise ValueError(
130
+ f"RawPixelVideoTubeLinearEmbed expected last dim {self.tube_dim} "
131
+ f"for {self.temporal_patch_size}x{self.patch_size}x{self.patch_size}x{self.in_channels} tubes, "
132
+ f"got {pixel_video_tubes.shape[-1]}"
133
+ )
134
+ return self.proj(pixel_video_tubes)
135
+
136
+
137
+
138
+ class UnpatchifyHead(nn.Module):
139
+ """Predict pixel patches from hidden states. Zero-initialized output."""
140
+ def __init__(self, hidden_size=3584, patch_size=16, out_channels=3):
141
+ super().__init__()
142
+ self.patch_dim = patch_size * patch_size * out_channels
143
+ self.norm = nn.RMSNorm(hidden_size, eps=1e-6)
144
+ self.linear = nn.Linear(hidden_size, self.patch_dim, bias=True)
145
+ nn.init.constant_(self.linear.weight, 0)
146
+ nn.init.constant_(self.linear.bias, 0)
147
+
148
+ def forward(self, hidden_states):
149
+ """Project hidden states to raw RGB patches."""
150
+ return self.linear(self.norm(hidden_states))
151
+
152
+
153
+ class VideoUnpatchifyHead(nn.Module):
154
+ """Predict raw video tube patches from hidden states."""
155
+
156
+ def __init__(self, hidden_size=3584, patch_size=16, temporal_patch_size=4, out_channels=3):
157
+ super().__init__()
158
+ self.patch_dim = temporal_patch_size * patch_size * patch_size * out_channels
159
+ self.norm = nn.RMSNorm(hidden_size, eps=1e-6)
160
+ self.linear = nn.Linear(hidden_size, self.patch_dim, bias=True)
161
+ nn.init.constant_(self.linear.weight, 0)
162
+ nn.init.constant_(self.linear.bias, 0)
163
+
164
+ def forward(self, hidden_states):
165
+ return self.linear(self.norm(hidden_states))
modeling/pixelumm/pixelumm.py ADDED
@@ -0,0 +1,1043 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from typing import List, Tuple, Optional
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from torch import nn
9
+ from torch.nn.attention.flex_attention import create_block_mask
10
+ from torch.utils.checkpoint import checkpoint
11
+ from transformers.configuration_utils import PretrainedConfig
12
+ from transformers.modeling_utils import PreTrainedModel
13
+ from data.data_utils import PIXELUMM_VIDEO_MROPE_BASE_FPS, build_pixelumm_vision_mrope_position_ids, create_sparse_mask, tubeify
14
+ from .qwen3_navit import NaiveCache
15
+ from .loss_reduction import sample_mean_token_weights
16
+ from .modeling_utils import TimestepEmbedder, RawPixelPatchLinearEmbed, RawPixelVideoTubeLinearEmbed, UnpatchifyHead, VideoUnpatchifyHead, patchify_image
17
+ from .dpm_solver import sample_flow_dpm
18
+ from .flow_unipc import sample_flow_unipc
19
+ from .time_schedule import flow_shift_noise_timestep, pixelumm_resolution_shift, sample_logit_normal_noise_time
20
+ _CHATML_USER_START = '<|im_start|>user\n'
21
+ _CHATML_ASSISTANT_START = '<|im_start|>assistant\n'
22
+ _CHATML_SYSTEM_START = '<|im_start|>system\n'
23
+ _CHATML_END = '<|im_end|>'
24
+ _CHATML_SEP = '\n'
25
+ _DEFAULT_SYSTEM_PROMPT = 'You are a helpful assistant.'
26
+ _PIXELUMM_TEXT_TOKEN_LIMIT = 4096
27
+
28
+ def _checkpointed_chunked_lm_head_cross_entropy(lm_head: nn.Module, hidden_states: torch.Tensor, labels: torch.Tensor, chunk_size: int) -> torch.Tensor:
29
+ """Compute token CE without retaining one full ``tokens x vocab`` tensor.
30
+
31
+ Each chunk is activation-checkpointed, so its LM-head logits are discarded
32
+ after the forward pass and recomputed during backward. This preserves the
33
+ exact per-token CE contract used by task logging/reweighting while bounding
34
+ peak logits/workspace memory by ``chunk_size x vocab_size``.
35
+ """
36
+ chunk_size = int(chunk_size)
37
+ if chunk_size <= 0 or hidden_states.shape[0] <= chunk_size:
38
+ logits = lm_head(hidden_states)
39
+ return F.cross_entropy(logits, labels, reduction='none')
40
+
41
+ def chunk_loss(chunk_hidden: torch.Tensor, chunk_labels: torch.Tensor) -> torch.Tensor:
42
+ logits = lm_head(chunk_hidden)
43
+ return F.cross_entropy(logits, chunk_labels, reduction='none')
44
+ losses = []
45
+ for start in range(0, hidden_states.shape[0], chunk_size):
46
+ end = min(start + chunk_size, hidden_states.shape[0])
47
+ chunk_hidden = hidden_states[start:end]
48
+ chunk_labels = labels[start:end]
49
+ if torch.is_grad_enabled():
50
+ loss = checkpoint(chunk_loss, chunk_hidden, chunk_labels, use_reentrant=False)
51
+ else:
52
+ loss = chunk_loss(chunk_hidden, chunk_labels)
53
+ losses.append(loss)
54
+ return torch.cat(losses, dim=0)
55
+
56
+ class PixelUMMConfig(PretrainedConfig):
57
+
58
+ def __init__(
59
+ self,
60
+ visual_gen=True,
61
+ visual_und=True,
62
+ llm_config=None,
63
+ llm_backend='qwen3',
64
+ gen_space='pixel',
65
+ und_space='pixel',
66
+ patch_size=16,
67
+ pixel_embedder_type='image_raw_patch_linear',
68
+ pixel_image_additive_pos_embed_type='none',
69
+ pixel_video_additive_pos_embed_type='none',
70
+ add_timestep_embedding=False,
71
+ add_noise_scale_embedding=False,
72
+ noise_scale=1.0,
73
+ noise_scale_mode='constant',
74
+ enable_pixel_video=True,
75
+ pixel_video_embedder_type='video_raw_tube_linear',
76
+ pixel_separate_und_gen_embedder=True,
77
+ pixel_video_separate_und_gen_embedder=True,
78
+ pixel_video_temporal_patch_size=4,
79
+ noise_time_logit_mean=0.0,
80
+ noise_time_logit_std=1.0,
81
+ time_schedule='pixelumm_resolution',
82
+ prediction_type='x',
83
+ loss_type='v_loss',
84
+ x_pred_t_min=0.05,
85
+ pixel_head_type='minit2i_linear',
86
+ pixel_video_head_type='jit_style',
87
+ ce_loss_checkpoint_chunk_size=0,
88
+ mse_loss_reduction='sample',
89
+ packed_attention_impl='flex',
90
+ packed_expert_routing='pixelumm_mot',
91
+ packed_sequence_layout='pixelumm_mot',
92
+ conditional_branch_gradient_contract='pixelumm_zero_v1',
93
+ **kwargs,
94
+ ):
95
+ super().__init__(**kwargs)
96
+ self.visual_gen = visual_gen
97
+ self.visual_und = visual_und
98
+ if not self.visual_gen or not self.visual_und:
99
+ raise ValueError('PixelUMM-release requires both R07 visual GEN and visual UND branches')
100
+ self.llm_config = llm_config
101
+ self.llm_backend = llm_backend
102
+ if self.llm_backend != 'qwen3':
103
+ raise ValueError(f'PixelUMM-release requires the R07 Qwen3 backend; got llm_backend={self.llm_backend!r}')
104
+ self.gen_space = gen_space
105
+ self.und_space = und_space
106
+ if self.gen_space != 'pixel' or self.und_space != 'pixel':
107
+ raise ValueError(f'PixelUMM-release requires the R07 raw-pixel GEN/UND spaces; got gen_space={self.gen_space!r}, und_space={self.und_space!r}')
108
+ self.conditional_branch_gradient_contract = conditional_branch_gradient_contract
109
+ if self.conditional_branch_gradient_contract != 'pixelumm_zero_v1':
110
+ raise ValueError(f"PixelUMM-release requires the R07 conditional gradient contract 'pixelumm_zero_v1', got {self.conditional_branch_gradient_contract!r}")
111
+ self.patch_size = patch_size
112
+ self.pixel_embedder_type = pixel_embedder_type
113
+ self.pixel_image_additive_pos_embed_type = pixel_image_additive_pos_embed_type
114
+ self.pixel_video_additive_pos_embed_type = pixel_video_additive_pos_embed_type
115
+ if self.pixel_embedder_type != 'image_raw_patch_linear':
116
+ raise ValueError(f"PixelUMM-release only supports the R07 image embedder pixel_embedder_type='image_raw_patch_linear', got {self.pixel_embedder_type!r}")
117
+ if self.pixel_image_additive_pos_embed_type != 'none':
118
+ raise ValueError("PixelUMM-release requires the R07 image positional policy pixel_image_additive_pos_embed_type='none'")
119
+ if self.pixel_video_additive_pos_embed_type != 'none':
120
+ raise ValueError("PixelUMM-release requires the R07 video positional policy pixel_video_additive_pos_embed_type='none'")
121
+ if add_noise_scale_embedding and (not add_timestep_embedding):
122
+ raise ValueError('add_noise_scale_embedding=True requires add_timestep_embedding=True')
123
+ if getattr(llm_config, 'qwen3_mrope_type', None) != 'sensenova':
124
+ raise ValueError("PixelUMM-release requires qwen3_mrope_type='sensenova'")
125
+ self.add_timestep_embedding = add_timestep_embedding
126
+ self.add_noise_scale_embedding = add_noise_scale_embedding
127
+ if self.add_timestep_embedding or self.add_noise_scale_embedding:
128
+ raise ValueError('PixelUMM-release requires the R07 embedding policy: add_timestep_embedding=False and add_noise_scale_embedding=False')
129
+ self.noise_scale = noise_scale
130
+ self.noise_scale_mode = noise_scale_mode
131
+ if self.noise_scale_mode != 'constant' or float(self.noise_scale) != 1.0:
132
+ raise ValueError("PixelUMM-release requires the R07 unit Gaussian noise scale: noise_scale_mode='constant' and noise_scale=1.0")
133
+ self.enable_pixel_video = enable_pixel_video
134
+ if not self.enable_pixel_video:
135
+ raise ValueError('PixelUMM-release requires the R07 pixel-video path')
136
+ self.pixel_video_embedder_type = pixel_video_embedder_type
137
+ self.pixel_separate_und_gen_embedder = bool(pixel_separate_und_gen_embedder)
138
+ if not isinstance(pixel_video_separate_und_gen_embedder, bool):
139
+ raise ValueError('pixel_video_separate_und_gen_embedder must be a boolean')
140
+ self.pixel_video_separate_und_gen_embedder = pixel_video_separate_und_gen_embedder
141
+ if not self.pixel_separate_und_gen_embedder:
142
+ raise ValueError('PixelUMM-release requires separate UND/GEN image embedders')
143
+ if self.pixel_video_embedder_type != 'video_raw_tube_linear':
144
+ raise ValueError(f"PixelUMM-release only supports the R07 video embedder pixel_video_embedder_type='video_raw_tube_linear', got {self.pixel_video_embedder_type!r}")
145
+ self.pixel_video_temporal_patch_size = pixel_video_temporal_patch_size
146
+ self.noise_time_logit_mean = noise_time_logit_mean
147
+ self.noise_time_logit_std = noise_time_logit_std
148
+ self.time_schedule = time_schedule
149
+ if self.time_schedule != 'pixelumm_resolution':
150
+ raise ValueError("PixelUMM-release requires time_schedule='pixelumm_resolution'")
151
+ self.prediction_type = prediction_type
152
+ self.loss_type = loss_type
153
+ if self.prediction_type != 'x' or self.loss_type != 'v_loss':
154
+ raise ValueError("PixelUMM-release requires prediction_type='x' and loss_type='v_loss'")
155
+ self.x_pred_t_min = x_pred_t_min
156
+ self.pixel_head_type = pixel_head_type
157
+ self.pixel_video_head_type = pixel_video_head_type
158
+ if self.pixel_head_type != 'minit2i_linear':
159
+ raise ValueError("PixelUMM-release requires pixel_head_type='minit2i_linear'")
160
+ if self.pixel_video_head_type != 'jit_style':
161
+ raise ValueError("PixelUMM-release requires pixel_video_head_type='jit_style'")
162
+ self.ce_loss_checkpoint_chunk_size = int(ce_loss_checkpoint_chunk_size)
163
+ if self.ce_loss_checkpoint_chunk_size < 0:
164
+ raise ValueError(f'ce_loss_checkpoint_chunk_size must be non-negative; got {self.ce_loss_checkpoint_chunk_size}.')
165
+ self.mse_loss_reduction = mse_loss_reduction
166
+ if self.mse_loss_reduction != 'sample':
167
+ raise ValueError('PixelUMM-release requires the R07 sample-mean MSE reduction')
168
+ self.packed_attention_impl = packed_attention_impl
169
+ if self.packed_attention_impl != 'flex':
170
+ raise ValueError('PixelUMM-release requires the R07 FlexAttention backend')
171
+ self.packed_expert_routing = packed_expert_routing
172
+ self.packed_sequence_layout = packed_sequence_layout
173
+ if self.packed_expert_routing != 'pixelumm_mot':
174
+ raise ValueError("PixelUMM-release requires packed_expert_routing='pixelumm_mot'")
175
+ if self.packed_sequence_layout != 'pixelumm_mot':
176
+ raise ValueError("PixelUMM-release requires packed_sequence_layout='pixelumm_mot'")
177
+
178
+ class PixelUMM(PreTrainedModel):
179
+ config_class = PixelUMMConfig
180
+ base_model_prefix = 'pixelumm'
181
+
182
+ def __init__(self, language_model, config: PixelUMMConfig):
183
+ super().__init__(config)
184
+ self.language_model = language_model
185
+ self.hidden_size = config.llm_config.hidden_size
186
+ if getattr(config.llm_config, 'layer_module', None) != 'Qwen3MoTDecoderLayer':
187
+ raise ValueError("PixelUMM-release requires layer_module='Qwen3MoTDecoderLayer'")
188
+ self.num_heads = config.llm_config.num_attention_heads
189
+ self.time_embedder = TimestepEmbedder(self.hidden_size)
190
+ self.time_embedder.requires_grad_(False)
191
+
192
+ def build_pixel_image_embedder():
193
+ return RawPixelPatchLinearEmbed(patch_size=config.patch_size, in_channels=3, hidden_size=self.hidden_size)
194
+ self.pixel_patch_embed = build_pixel_image_embedder()
195
+ self.pixel_patch_embed_mot_gen = build_pixel_image_embedder()
196
+ self.pixel_video_patch_embed_mot_gen = RawPixelVideoTubeLinearEmbed(patch_size=config.patch_size, in_channels=3, temporal_patch_size=config.pixel_video_temporal_patch_size, hidden_size=self.hidden_size)
197
+ if config.pixel_video_separate_und_gen_embedder:
198
+ self.pixel_video_patch_embed = RawPixelVideoTubeLinearEmbed(patch_size=config.patch_size, in_channels=3, temporal_patch_size=config.pixel_video_temporal_patch_size, hidden_size=self.hidden_size)
199
+ self.unpatchify_head = UnpatchifyHead(hidden_size=self.hidden_size, patch_size=config.patch_size, out_channels=3)
200
+ self.video_unpatchify_head = VideoUnpatchifyHead(hidden_size=self.hidden_size, patch_size=config.patch_size, temporal_patch_size=config.pixel_video_temporal_patch_size, out_channels=3)
201
+ self.config = config
202
+
203
+ def _image_patch_embedder(self, gen_model=False):
204
+ return self.pixel_patch_embed_mot_gen if gen_model else self.pixel_patch_embed
205
+
206
+ def embed_pixel_video_tubes(self, pixel_video_tubes: torch.Tensor, gen_model: bool=True) -> torch.Tensor:
207
+ if not gen_model:
208
+ raise RuntimeError('Pixel video embedding is generation-only; video understanding uses image UND frames.')
209
+ return self.pixel_video_patch_embed_mot_gen(pixel_video_tubes)
210
+
211
+ def _sample_train_timesteps(self, raw_timesteps: torch.Tensor, modality: Optional[str]=None) -> torch.Tensor:
212
+ if modality not in {None, 'image', 'video'}:
213
+ raise ValueError(f'Unsupported timestep modality={modality!r}')
214
+ return sample_logit_normal_noise_time(
215
+ raw_timesteps,
216
+ logit_mean=float(self.config.noise_time_logit_mean),
217
+ logit_std=float(self.config.noise_time_logit_std),
218
+ )
219
+
220
+ def _apply_time_schedule(self, t: torch.Tensor, spatial_hw: Optional[Tuple[int, int]]=None) -> torch.Tensor:
221
+ """Shift PixelUMM noise-time ``t`` toward noise when shift > 1.
222
+
223
+ Unlike the upstream SenseNova implementation, PixelUMM defines
224
+ ``z_t = (1 - t) * clean + t * noise``. ``t`` is therefore already
225
+ the noise fraction and must not be complemented before shifting.
226
+ """
227
+ if spatial_hw is None:
228
+ raise ValueError('time_schedule=pixelumm_resolution requires spatial_hw')
229
+ return flow_shift_noise_timestep(t, pixelumm_resolution_shift(*spatial_hw))
230
+
231
+ def _apply_time_schedule_per_sample(self, timesteps: torch.Tensor, token_lens, grid_hw, *, patch_size: int) -> torch.Tensor:
232
+ """Apply a spatially selected schedule without mixing packed samples."""
233
+ token_lens = self._tensor_to_int_list(token_lens)
234
+ if torch.is_tensor(grid_hw):
235
+ grid_hw = [(int(h), int(w)) for (h, w) in grid_hw.tolist()]
236
+ else:
237
+ grid_hw = [(int(h), int(w)) for (h, w) in grid_hw]
238
+ if len(token_lens) != len(grid_hw):
239
+ raise ValueError(f'Per-sample timestep metadata length mismatch: token_lens={len(token_lens)} grid_hw={len(grid_hw)}')
240
+ if sum(token_lens) != int(timesteps.numel()):
241
+ raise ValueError(f'Per-sample timestep token lengths do not match packed timesteps: {sum(token_lens)} vs {int(timesteps.numel())}')
242
+ chunks = []
243
+ cursor = 0
244
+ for (token_len, (grid_h, grid_w)) in zip(token_lens, grid_hw):
245
+ chunk = timesteps[cursor:cursor + token_len]
246
+ chunks.append(self._apply_time_schedule(chunk, spatial_hw=(grid_h * int(patch_size), grid_w * int(patch_size))))
247
+ cursor += token_len
248
+ return torch.cat(chunks, dim=0) if chunks else timesteps
249
+
250
+ def _text_mrope_position_ids(self, start_position, length, device=None):
251
+ pos = torch.arange(start_position, start_position + length, dtype=torch.long, device=device)
252
+ zeros = torch.zeros_like(pos)
253
+ return torch.stack([pos, zeros, zeros], dim=0)
254
+
255
+ @staticmethod
256
+ def _pixelumm_vfm_vision_mrope_position_ids(temporal_offset, t_tokens, h_tokens, w_tokens, device=None, fps=None, base_fps=PIXELUMM_VIDEO_MROPE_BASE_FPS, temporal_compression_factor=4):
257
+ return build_pixelumm_vision_mrope_position_ids(temporal_offset, t_tokens, h_tokens, w_tokens, device=device, fps=fps, base_fps=base_fps, temporal_compression_factor=temporal_compression_factor)
258
+
259
+ def _image_mrope_position_ids(self, start_position, h_tokens, w_tokens, device=None, include_start=True, include_end=True):
260
+ grid_t = start_position + int(include_start)
261
+ rows = torch.arange(h_tokens, dtype=torch.long, device=device).repeat_interleave(w_tokens)
262
+ cols = torch.arange(w_tokens, dtype=torch.long, device=device).repeat(h_tokens)
263
+ t = torch.full((h_tokens * w_tokens,), grid_t, dtype=torch.long, device=device)
264
+ (t_chunks, h_chunks, w_chunks) = ([], [], [])
265
+ if include_start:
266
+ start = torch.full((1,), start_position, dtype=torch.long, device=device)
267
+ t_chunks.append(start)
268
+ h_chunks.append(start.new_zeros(1))
269
+ w_chunks.append(start.new_zeros(1))
270
+ t_chunks.append(t)
271
+ h_chunks.append(rows)
272
+ w_chunks.append(cols)
273
+ next_position = grid_t + 1
274
+ if include_end:
275
+ end = torch.full((1,), next_position, dtype=torch.long, device=device)
276
+ t_chunks.append(end)
277
+ h_chunks.append(end.new_zeros(1))
278
+ w_chunks.append(end.new_zeros(1))
279
+ next_position += 1
280
+ legacy_t = torch.cat(t_chunks)
281
+ axes = [legacy_t, torch.cat(h_chunks), torch.cat(w_chunks)]
282
+ return (torch.stack(axes, dim=0), next_position)
283
+
284
+ def _video_mrope_position_ids(self, start_position, temporal_groups, h_tokens, w_tokens, device=None, fps=None, temporal_compression_factor=None, include_start=True, include_end=True):
285
+ grid_start = start_position + int(include_start)
286
+ if fps is not None:
287
+ (vision_ids, next_position) = self._pixelumm_vfm_vision_mrope_position_ids(grid_start, temporal_groups, h_tokens, w_tokens, device=device, fps=fps, base_fps=PIXELUMM_VIDEO_MROPE_BASE_FPS, temporal_compression_factor=temporal_compression_factor if temporal_compression_factor is not None else self.config.pixel_video_temporal_patch_size)
288
+ t = vision_ids[0]
289
+ h = vision_ids[1]
290
+ w = vision_ids[2]
291
+ else:
292
+ t = torch.arange(temporal_groups, dtype=torch.long, device=device)[:, None, None]
293
+ h = torch.arange(h_tokens, dtype=torch.long, device=device)[None, :, None]
294
+ w = torch.arange(w_tokens, dtype=torch.long, device=device)[None, None, :]
295
+ t = t.expand(temporal_groups, h_tokens, w_tokens).reshape(-1) + grid_start
296
+ h = h.expand(temporal_groups, h_tokens, w_tokens).reshape(-1)
297
+ w = w.expand(temporal_groups, h_tokens, w_tokens).reshape(-1)
298
+ next_position = grid_start + max(1, temporal_groups)
299
+ (t_chunks, h_chunks, w_chunks) = ([], [], [])
300
+ if include_start:
301
+ start = torch.full((1,), start_position, dtype=torch.long, device=device)
302
+ t_chunks.append(start.to(dtype=t.dtype))
303
+ h_chunks.append(torch.zeros(1, dtype=h.dtype, device=device))
304
+ w_chunks.append(torch.zeros(1, dtype=w.dtype, device=device))
305
+ t_chunks.append(t)
306
+ h_chunks.append(h)
307
+ w_chunks.append(w)
308
+ if include_end:
309
+ end = torch.full((1,), next_position, dtype=t.dtype, device=device)
310
+ t_chunks.append(end)
311
+ h_chunks.append(torch.zeros(1, dtype=h.dtype, device=device))
312
+ w_chunks.append(torch.zeros(1, dtype=w.dtype, device=device))
313
+ next_position += 1
314
+ legacy_t = torch.cat(t_chunks)
315
+ axes = [legacy_t, torch.cat(h_chunks), torch.cat(w_chunks)]
316
+ return (torch.stack(axes, dim=0), next_position)
317
+
318
+ def _advance_text_query_position_ids(self, packed_query_position_ids):
319
+ next_position_ids = packed_query_position_ids.clone()
320
+ next_position_ids[0] = next_position_ids[0] + 1
321
+ return next_position_ids
322
+
323
+ def _pixel_video_token_lens(self, packed_pixel_video_patches, pixel_video_patch_seqlens=None):
324
+ if pixel_video_patch_seqlens is not None:
325
+ return [int(x) for x in pixel_video_patch_seqlens.tolist()]
326
+ return [int(packed_pixel_video_patches.shape[0])]
327
+
328
+ @staticmethod
329
+ def _tensor_to_int_list(values):
330
+ if values is None:
331
+ return None
332
+ if torch.is_tensor(values):
333
+ values = values.detach().cpu().flatten().tolist()
334
+ return [int(x) for x in values]
335
+
336
+ @staticmethod
337
+ def _has_trainable_parameters(module: Optional[nn.Module]) -> bool:
338
+ return module is not None and any((parameter.requires_grad for parameter in module.parameters()))
339
+
340
+ @staticmethod
341
+ def _zero_graph_anchor(value: torch.Tensor) -> torch.Tensor:
342
+ return value.sum() * 0.0
343
+
344
+ def _dummy_pixel_image_embedder_anchor(self, module: nn.Module, reference: torch.Tensor) -> torch.Tensor:
345
+ patch_dim = getattr(module, 'patch_dim', None)
346
+ if patch_dim is None:
347
+ raise RuntimeError(f'pixelumm_zero_v1 cannot build an image-embedder dummy for {type(module).__name__}: missing patch_dim')
348
+ dummy = reference.new_zeros((1, int(patch_dim)))
349
+ output = module(dummy)
350
+ return self._zero_graph_anchor(output)
351
+
352
+ def _dummy_pixel_video_embedder_anchor(self, module: nn.Module, reference: torch.Tensor, *, gen_model: bool) -> torch.Tensor:
353
+ tube_dim = getattr(module, 'tube_dim', None)
354
+ if tube_dim is None:
355
+ tube_dim = getattr(module, 'video_tube_dim', None)
356
+ if tube_dim is None:
357
+ tube_dim = getattr(module, 'patch_dim', None)
358
+ if tube_dim is None:
359
+ raise RuntimeError(f'pixelumm_zero_v1 cannot build a video-embedder dummy for {type(module).__name__}: missing tube_dim/video_tube_dim/patch_dim')
360
+ dummy = reference.new_zeros((1, int(tube_dim)))
361
+ position_ids = torch.zeros((1, 3), dtype=torch.long, device=reference.device)
362
+ if gen_model:
363
+ output = self.embed_pixel_video_tubes(dummy, gen_model=True)
364
+ else:
365
+ try:
366
+ output = module(dummy, position_ids=position_ids)
367
+ except TypeError:
368
+ output = module(dummy)
369
+ return self._zero_graph_anchor(output)
370
+
371
+ def _dummy_pixel_image_head_anchor(self, reference: torch.Tensor) -> torch.Tensor:
372
+ head = self.unpatchify_head
373
+ dummy_hidden = reference.new_zeros((1, self.hidden_size))
374
+ output = head(dummy_hidden)
375
+ return self._zero_graph_anchor(output)
376
+
377
+ def _dummy_pixel_video_head_anchor(self, reference: torch.Tensor) -> torch.Tensor:
378
+ dummy_hidden = reference.new_zeros((1, self.hidden_size))
379
+ output = self.video_unpatchify_head(dummy_hidden)
380
+ return self._zero_graph_anchor(output)
381
+
382
+ def _pixelumm_zero_root_graph_anchor(self, last_hidden_state: torch.Tensor, *, has_und_image: bool, has_und_video: bool, has_gen_image: bool, has_gen_video: bool, has_text_output: bool) -> torch.Tensor:
383
+ """Keep every inactive *trainable* root branch in the autograd graph.
384
+
385
+ This mirrors PixelUMM VFM's dummy-forward contract. Frozen coarse
386
+ branches remain excluded because their parameters have
387
+ ``requires_grad=False``; the helper therefore cannot create optimizer
388
+ state or parameter updates for F18/F19's disabled branch.
389
+ """
390
+ anchor = last_hidden_state.new_zeros(())
391
+ if not has_text_output and self._has_trainable_parameters(self.language_model.lm_head):
392
+ anchor = anchor + self._zero_graph_anchor(self.language_model.lm_head(last_hidden_state[:0]))
393
+ image_und_embedder = getattr(self, 'pixel_patch_embed', None)
394
+ if not has_und_image and self._has_trainable_parameters(image_und_embedder):
395
+ anchor = anchor + self._dummy_pixel_image_embedder_anchor(image_und_embedder, last_hidden_state)
396
+ video_und_embedder = getattr(self, 'pixel_video_patch_embed', None)
397
+ if not has_und_video and self._has_trainable_parameters(video_und_embedder):
398
+ anchor = anchor + self._dummy_pixel_video_embedder_anchor(video_und_embedder, last_hidden_state, gen_model=False)
399
+ image_gen_embedder = self._image_patch_embedder(gen_model=True)
400
+ if not has_gen_image and self._has_trainable_parameters(image_gen_embedder):
401
+ anchor = anchor + self._dummy_pixel_image_embedder_anchor(image_gen_embedder, last_hidden_state)
402
+ if not has_gen_image and self._has_trainable_parameters(getattr(self, 'unpatchify_head', None)):
403
+ anchor = anchor + self._dummy_pixel_image_head_anchor(last_hidden_state)
404
+ video_gen_embedder = getattr(self, 'pixel_video_patch_embed_mot_gen', None)
405
+ if not has_gen_video and self._has_trainable_parameters(video_gen_embedder):
406
+ anchor = anchor + self._dummy_pixel_video_embedder_anchor(video_gen_embedder, last_hidden_state, gen_model=True)
407
+ video_head = getattr(self, 'video_unpatchify_head', None)
408
+ if not has_gen_video and self._has_trainable_parameters(video_head):
409
+ anchor = anchor + self._dummy_pixel_video_head_anchor(last_hidden_state)
410
+ return anchor
411
+
412
+ def forward(
413
+ self,
414
+ sequence_length: int,
415
+ packed_text_ids: torch.LongTensor,
416
+ packed_text_indexes: torch.LongTensor,
417
+ sample_lens: List[int],
418
+ packed_position_ids: torch.LongTensor,
419
+ packed_mrope_position_ids: Optional[torch.LongTensor] = None,
420
+ split_lens: List[int] = None,
421
+ attn_modes: List[str] = None,
422
+ ce_loss_indexes: Optional[torch.BoolTensor] = None,
423
+ packed_label_ids: Optional[torch.LongTensor] = None,
424
+ packed_pixel_patches_und: Optional[torch.Tensor] = None,
425
+ packed_pixel_patch_indexes_und: Optional[torch.LongTensor] = None,
426
+ packed_pixel_video_patches_und: Optional[torch.Tensor] = None,
427
+ packed_pixel_video_patch_indexes_und: Optional[torch.LongTensor] = None,
428
+ packed_pixel_patches_gen: Optional[torch.Tensor] = None,
429
+ packed_pixel_patch_indexes_gen: Optional[torch.LongTensor] = None,
430
+ packed_pixel_grid_hw_gen: Optional[torch.LongTensor] = None,
431
+ packed_gen_image_lens: Optional[torch.LongTensor] = None,
432
+ packed_pixel_video_patches_gen: Optional[torch.Tensor] = None,
433
+ packed_pixel_video_patch_indexes_gen: Optional[torch.LongTensor] = None,
434
+ pixel_video_patch_seqlens_gen: Optional[torch.IntTensor] = None,
435
+ pixel_video_grid_hw_gen: Optional[torch.LongTensor] = None,
436
+ packed_gen_video_lens: Optional[torch.LongTensor] = None,
437
+ packed_timesteps: Optional[torch.LongTensor] = None,
438
+ ) -> dict:
439
+ """
440
+ Args:
441
+ sequence_length: length of sequence.
442
+ packed_text_ids: 1-D int tensor, packed text token ids.
443
+ packed_text_indexes: 1-D int tensor, packed text token indexes in sequence.
444
+ sample_lens: A list of N ints, length of each sample in packed_sequence.
445
+ packed_position_ids: packed 1-D positions, an image has only one global position shared
446
+ by all latent tokens.
447
+
448
+ packed_label_ids: 1-D int tensor, packed label token ids.
449
+ ce_loss_indexes: 1-D bool tensor, where to compute ce loss.
450
+ packed_timesteps: 1-D float tensor, flow timesteps. 0 indicates use clean image.
451
+ """
452
+ packed_text_embedding = self.language_model.model.embed_tokens(packed_text_ids)
453
+ packed_sequence = packed_text_embedding.new_zeros(size=(sequence_length, self.hidden_size))
454
+ packed_sequence[packed_text_indexes] = packed_text_embedding
455
+ logical_seqlen = sum(sample_lens)
456
+ flex_seqlen = (int(sequence_length) + 127) // 128 * 128
457
+ if flex_seqlen < logical_seqlen:
458
+ raise ValueError((logical_seqlen, flex_seqlen))
459
+ sparse_mask = create_sparse_mask(
460
+ sample_lens,
461
+ split_lens,
462
+ attn_modes,
463
+ packed_text_embedding.device,
464
+ total_length=flex_seqlen,
465
+ )
466
+ attention_mask = create_block_mask(
467
+ sparse_mask,
468
+ B=1,
469
+ H=1,
470
+ Q_LEN=flex_seqlen,
471
+ KV_LEN=flex_seqlen,
472
+ device=packed_text_embedding.device,
473
+ BLOCK_SIZE=128,
474
+ _compile=True,
475
+ )
476
+ packed_und_image_indexes = None
477
+ if packed_pixel_patches_und is not None:
478
+ und_embed = self.pixel_patch_embed(packed_pixel_patches_und)
479
+ packed_sequence[packed_pixel_patch_indexes_und] = und_embed
480
+ packed_und_image_indexes = packed_pixel_patch_indexes_und
481
+ if packed_pixel_video_patches_und is not None:
482
+ if not hasattr(self, 'pixel_video_patch_embed'):
483
+ raise ValueError('R07 video UND patches require the dedicated video embedder')
484
+ video_und_embed = self.pixel_video_patch_embed(packed_pixel_video_patches_und)
485
+ packed_sequence[packed_pixel_video_patch_indexes_und] = video_und_embed
486
+ if packed_und_image_indexes is None:
487
+ packed_und_image_indexes = packed_pixel_video_patch_indexes_und
488
+ else:
489
+ packed_und_image_indexes = torch.cat([packed_und_image_indexes, packed_pixel_video_patch_indexes_und], dim=0).sort().values
490
+ packed_gen_token_indexes = None
491
+ image_clean_target = None
492
+ image_noisy_patches = None
493
+ image_timesteps = None
494
+ video_clean_target = None
495
+ video_noisy_patches = None
496
+ video_timesteps = None
497
+ has_gen_tokens = packed_pixel_patches_gen is not None or packed_pixel_video_patches_gen is not None
498
+ if has_gen_tokens and packed_timesteps is None:
499
+ raise ValueError('Generation tokens are present but packed_timesteps is missing')
500
+ raw_timesteps = packed_timesteps if has_gen_tokens else None
501
+ if not has_gen_tokens and raw_timesteps is not None:
502
+ raise ValueError('PixelUMM-release received timesteps without R07 pixel GEN tokens')
503
+ all_gen_indexes = []
504
+ if packed_pixel_patches_gen is not None:
505
+ all_gen_indexes.append(packed_pixel_patch_indexes_gen)
506
+ if packed_pixel_video_patches_gen is not None:
507
+ all_gen_indexes.append(packed_pixel_video_patch_indexes_gen)
508
+ if all_gen_indexes:
509
+ sorted_gen_indexes = torch.cat(all_gen_indexes, dim=0).sort().values
510
+ if sorted_gen_indexes.numel() != raw_timesteps.numel():
511
+ raise ValueError(f'packed_timesteps length does not match image/video generation token count: {raw_timesteps.numel()} vs {sorted_gen_indexes.numel()}')
512
+ timestep_by_seq = raw_timesteps.new_zeros(sequence_length)
513
+ timestep_by_seq[sorted_gen_indexes] = raw_timesteps
514
+ if packed_pixel_video_patches_gen is not None:
515
+ clean_patches = packed_pixel_video_patches_gen
516
+ video_timesteps = timestep_by_seq[packed_pixel_video_patch_indexes_gen]
517
+ video_timesteps = self._sample_train_timesteps(video_timesteps, modality='video')
518
+ video_token_lens = self._pixel_video_token_lens(packed_pixel_video_patches_gen, pixel_video_patch_seqlens_gen)
519
+ if sum(video_token_lens) != int(video_timesteps.shape[0]):
520
+ raise ValueError(f'pixel_video_patch_seqlens_gen does not match packed video gen token count: {sum(video_token_lens)} vs {int(video_timesteps.shape[0])}')
521
+ if pixel_video_grid_hw_gen is None:
522
+ raise ValueError('time_schedule=pixelumm_resolution requires pixel_video_grid_hw_gen')
523
+ video_timesteps = self._apply_time_schedule_per_sample(video_timesteps, video_token_lens, pixel_video_grid_hw_gen, patch_size=self.config.patch_size)
524
+ noise = torch.randn_like(clean_patches)
525
+ noisy_patches = (1 - video_timesteps[:, None]) * clean_patches + video_timesteps[:, None] * noise
526
+ gen_embed = self.embed_pixel_video_tubes(noisy_patches)
527
+ packed_sequence[packed_pixel_video_patch_indexes_gen] = gen_embed
528
+ packed_gen_token_indexes = packed_pixel_video_patch_indexes_gen
529
+ video_clean_target = clean_patches
530
+ video_noisy_patches = noisy_patches
531
+ if packed_pixel_patches_gen is not None:
532
+ clean_patches = packed_pixel_patches_gen
533
+ image_timesteps = timestep_by_seq[packed_pixel_patch_indexes_gen]
534
+ image_timesteps = self._sample_train_timesteps(image_timesteps, modality='image')
535
+ if packed_pixel_grid_hw_gen is None:
536
+ raise ValueError('time_schedule=pixelumm_resolution requires packed_pixel_grid_hw_gen')
537
+ schedule_token_lens = [int(h) * int(w) for (h, w) in packed_pixel_grid_hw_gen.tolist()]
538
+ image_timesteps = self._apply_time_schedule_per_sample(image_timesteps, schedule_token_lens, packed_pixel_grid_hw_gen, patch_size=self.config.patch_size)
539
+ noise = torch.randn_like(clean_patches)
540
+ noisy_patches = (1 - image_timesteps[:, None]) * clean_patches + image_timesteps[:, None] * noise
541
+ gen_embed = self._image_patch_embedder(gen_model=True)(noisy_patches)
542
+ packed_sequence[packed_pixel_patch_indexes_gen] = gen_embed
543
+ if packed_gen_token_indexes is None:
544
+ packed_gen_token_indexes = packed_pixel_patch_indexes_gen
545
+ else:
546
+ packed_gen_token_indexes = torch.cat([packed_gen_token_indexes, packed_pixel_patch_indexes_gen], dim=0).sort().values
547
+ image_clean_target = clean_patches
548
+ image_noisy_patches = noisy_patches
549
+ packed_und_token_indexes = packed_text_indexes
550
+ if packed_und_image_indexes is not None:
551
+ packed_und_token_indexes = torch.cat([packed_und_token_indexes, packed_und_image_indexes], dim=0)
552
+ extra_inputs = {'packed_und_token_indexes': packed_und_token_indexes, 'packed_gen_token_indexes': packed_gen_token_indexes}
553
+ llm_position_ids = packed_mrope_position_ids if packed_mrope_position_ids is not None else packed_position_ids
554
+ if llm_position_ids is not None:
555
+ llm_position_ids = llm_position_ids.contiguous()
556
+ last_hidden_state = self.language_model(packed_sequence=packed_sequence, sample_lens=sample_lens, attention_mask=attention_mask, packed_position_ids=llm_position_ids, **extra_inputs)
557
+ mse = None
558
+ mse_loss_reduction_weights = None
559
+ if image_clean_target is not None or video_clean_target is not None:
560
+ mse_parts = []
561
+ mse_weight_parts = []
562
+
563
+ def per_token_mse(pred, target, loss_mask=None):
564
+ sq = (pred.float() - target.float()) ** 2
565
+ if loss_mask is None:
566
+ return sq.mean(dim=-1, keepdim=True)
567
+ mask = loss_mask.to(device=sq.device, dtype=sq.dtype)
568
+ denom = mask.sum(dim=-1, keepdim=True).clamp_min(1.0)
569
+ return (sq * mask).sum(dim=-1, keepdim=True) / denom
570
+ if image_clean_target is not None:
571
+ image_has_mse = image_timesteps > 0
572
+ image_hidden = last_hidden_state[packed_pixel_patch_indexes_gen[image_has_mse]]
573
+ image_model_out = self.unpatchify_head(image_hidden)
574
+ t = image_timesteps[:, None]
575
+ t_f32 = t[image_has_mse].float().clamp(min=self.config.x_pred_t_min)
576
+ v_pred = (image_noisy_patches[image_has_mse].float() - image_model_out.float()) / t_f32
577
+ v_target = (image_noisy_patches[image_has_mse].float() - image_clean_target[image_has_mse].float()) / t_f32
578
+ image_mse = per_token_mse(v_pred, v_target)
579
+ mse_parts.append(image_mse)
580
+ if packed_gen_image_lens is None:
581
+ raise ValueError('R07 sample-mean MSE requires packed_gen_image_lens')
582
+ mse_weight_parts.append(sample_mean_token_weights(packed_gen_image_lens, output_size=image_mse.shape[0], device=image_mse.device))
583
+ if video_clean_target is not None:
584
+ video_has_mse = video_timesteps > 0
585
+ video_hidden = last_hidden_state[packed_pixel_video_patch_indexes_gen[video_has_mse]]
586
+ video_model_out = self.video_unpatchify_head(video_hidden)
587
+ t = video_timesteps[:, None]
588
+ t_f32 = t[video_has_mse].float().clamp(min=self.config.x_pred_t_min)
589
+ v_pred = (video_noisy_patches[video_has_mse].float() - video_model_out.float()) / t_f32
590
+ v_target = (video_noisy_patches[video_has_mse].float() - video_clean_target[video_has_mse].float()) / t_f32
591
+ video_mse = per_token_mse(v_pred, v_target)
592
+ mse_parts.append(video_mse)
593
+ if packed_gen_video_lens is None:
594
+ raise ValueError('R07 sample-mean MSE requires packed_gen_video_lens')
595
+ mse_weight_parts.append(sample_mean_token_weights(packed_gen_video_lens, output_size=video_mse.shape[0], device=video_mse.device))
596
+ mse = torch.cat(mse_parts, dim=0)
597
+ if mse_weight_parts:
598
+ mse_loss_reduction_weights = torch.cat(mse_weight_parts)
599
+ ce = None
600
+ if ce_loss_indexes is not None:
601
+ ce = _checkpointed_chunked_lm_head_cross_entropy(self.language_model.lm_head, last_hidden_state[ce_loss_indexes], packed_label_ids, self.config.ce_loss_checkpoint_chunk_size)
602
+
603
+ def _has_elements(value: Optional[torch.Tensor]) -> bool:
604
+ return value is not None and int(value.numel()) > 0
605
+ conditional_graph_anchor = self._pixelumm_zero_root_graph_anchor(last_hidden_state, has_und_image=_has_elements(packed_pixel_patches_und), has_und_video=_has_elements(packed_pixel_video_patches_und), has_gen_image=_has_elements(packed_pixel_patches_gen), has_gen_video=_has_elements(packed_pixel_video_patches_gen), has_text_output=ce is not None and int(ce.numel()) > 0)
606
+ return dict(mse=mse, ce=ce, mse_loss_reduction_weights=mse_loss_reduction_weights, conditional_graph_anchor=conditional_graph_anchor)
607
+
608
+ def _encode_prompt_text(self, tokenizer, prompt: str) -> list[int]:
609
+ try:
610
+ return tokenizer.encode(prompt, add_special_tokens=False)
611
+ except TypeError:
612
+ return tokenizer.encode(prompt)
613
+
614
+ def _text_to_visual_chatml_prompt_ids(self, tokenizer, prompt: str) -> list[int]:
615
+ caption_ids = self._encode_prompt_text(tokenizer, prompt)[:_PIXELUMM_TEXT_TOKEN_LIMIT]
616
+ return self._encode_prompt_text(tokenizer, _CHATML_USER_START) + caption_ids + self._encode_prompt_text(tokenizer, f'{_CHATML_END}{_CHATML_SEP}{_CHATML_ASSISTANT_START}')
617
+
618
+ def _image_to_text_chatml_prefix_ids(self, tokenizer) -> list[int]:
619
+ return self._encode_prompt_text(tokenizer, f'{_CHATML_SYSTEM_START}{_DEFAULT_SYSTEM_PROMPT}{_CHATML_END}{_CHATML_SEP}{_CHATML_USER_START}')
620
+
621
+ def _image_to_text_chatml_suffix_ids(self, tokenizer, instruction: str) -> list[int]:
622
+ return self._encode_prompt_text(tokenizer, f'{instruction}{_CHATML_END}{_CHATML_SEP}{_CHATML_ASSISTANT_START}')
623
+
624
+ def _prepare_token_ids_as_text_cache_input(self, curr_kvlens, curr_rope, prompt_ids_list):
625
+ packed_text_ids = list()
626
+ packed_text_position_ids = list()
627
+ text_token_lens = list()
628
+ packed_text_indexes = list()
629
+ packed_key_value_indexes = list()
630
+ curr = 0
631
+ (newlens, new_rope) = (list(), list())
632
+ for (text_ids, curr_kvlen, curr_position_id) in zip(prompt_ids_list, curr_kvlens, curr_rope):
633
+ packed_key_value_indexes.extend(range(curr, curr + curr_kvlen))
634
+ curr += curr_kvlen
635
+ text_token_lens.append(len(text_ids))
636
+ packed_text_ids.extend(text_ids)
637
+ packed_text_position_ids.append(self._text_mrope_position_ids(curr_position_id, len(text_ids)))
638
+ packed_text_indexes.extend(range(curr, curr + len(text_ids)))
639
+ newlens.append(curr_kvlen + len(text_ids))
640
+ new_rope.append(curr_position_id + len(text_ids))
641
+ curr += len(text_ids)
642
+ packed_text_position_ids = torch.cat(packed_text_position_ids, dim=1)
643
+ generation_input = {'text_token_lens': torch.tensor(text_token_lens, dtype=torch.int), 'packed_text_ids': torch.tensor(packed_text_ids, dtype=torch.long), 'packed_text_position_ids': packed_text_position_ids, 'packed_text_indexes': torch.tensor(packed_text_indexes, dtype=torch.long), 'packed_key_value_indexes': torch.tensor(packed_key_value_indexes, dtype=torch.long), 'key_values_lens': torch.tensor(curr_kvlens, dtype=torch.int)}
644
+ return (generation_input, newlens, new_rope)
645
+
646
+ def prepare_text_to_visual_chatml_prompts(self, curr_kvlens, curr_rope, prompts, tokenizer, new_token_ids):
647
+ """T2I/T2V inference prefill matching the ChatML training prompt."""
648
+ prompt_ids_list = [self._text_to_visual_chatml_prompt_ids(tokenizer, prompt) + [new_token_ids['eos_token_id']] for prompt in prompts]
649
+ return self._prepare_token_ids_as_text_cache_input(curr_kvlens, curr_rope, prompt_ids_list)
650
+
651
+ def prepare_image_to_text_chatml_prefixes(self, curr_kvlens, curr_rope, tokenizer):
652
+ """I2T inference prefill before the image tokens."""
653
+ prompt_ids = self._image_to_text_chatml_prefix_ids(tokenizer)
654
+ prompt_ids_list = [prompt_ids for _ in curr_kvlens]
655
+ return self._prepare_token_ids_as_text_cache_input(curr_kvlens, curr_rope, prompt_ids_list)
656
+
657
+ def prepare_image_to_text_chatml_suffix_contexts_for_decode(self, curr_kvlens, curr_rope, instructions, tokenizer):
658
+ """I2T inference prefill after image tokens, aligned with train-time CE shift."""
659
+ suffix_ids_list = [self._image_to_text_chatml_suffix_ids(tokenizer, instruction) for instruction in instructions]
660
+ if any((len(ids) == 0 for ids in suffix_ids_list)):
661
+ raise ValueError('image-to-text ChatML suffix cannot be empty')
662
+ context_ids_list = [ids[:-1] for ids in suffix_ids_list]
663
+ start_token_ids = [ids[-1] for ids in suffix_ids_list]
664
+ (generation_input, newlens, new_rope) = self._prepare_token_ids_as_text_cache_input(curr_kvlens, curr_rope, context_ids_list)
665
+ return (generation_input, newlens, new_rope, start_token_ids)
666
+
667
+ @torch.no_grad
668
+ def forward_cache_update_text(self, past_key_values: NaiveCache, packed_text_ids: torch.IntTensor, packed_text_position_ids: torch.LongTensor, text_token_lens: torch.LongTensor, packed_text_indexes: torch.LongTensor, packed_key_value_indexes: torch.LongTensor, key_values_lens: torch.IntTensor):
669
+ packed_text_embedding = self.language_model.model.embed_tokens(packed_text_ids)
670
+ output = self.language_model.forward_inference(packed_query_sequence=packed_text_embedding, query_lens=text_token_lens, packed_query_position_ids=packed_text_position_ids, packed_query_indexes=packed_text_indexes, past_key_values=past_key_values, packed_key_value_indexes=packed_key_value_indexes, key_values_lens=key_values_lens, update_past_key_values=True, is_causal=True, mode='und')
671
+ past_key_values = output.past_key_values
672
+ return past_key_values
673
+
674
+ def _validate_pixel_generation_prefix_contract(self, visual_start_prefilled: bool) -> None:
675
+ if not visual_start_prefilled:
676
+ raise ValueError('pixelumm_mot pixel generation requires one causal <vision_start> in the KV cache')
677
+
678
+ def prepare_pixel_noise(self, curr_kvlens, curr_rope, image_sizes, *, visual_start_prefilled: bool=False):
679
+ """Generate image noise and construct the denoising query.
680
+
681
+ For ``pixelumm_mot``, callers must first cache exactly one causal
682
+ ``<vision_start>`` with :meth:`prepare_visual_delimiter_tokens`. This
683
+ method then returns a patch-only, bidirectional noise query, matching
684
+ the train-time ``causal start -> full/noise patches`` mask exactly.
685
+ """
686
+ self._validate_pixel_generation_prefix_contract(visual_start_prefilled)
687
+ (packed_text_ids, packed_text_indexes) = (list(), list())
688
+ (packed_pixel_token_indexes, packed_init_noises) = (list(), list())
689
+ (packed_position_ids, packed_seqlens, packed_indexes) = (list(), list(), list())
690
+ packed_key_value_indexes = list()
691
+ query_curr = curr = 0
692
+ for ((H, W), curr_kvlen, curr_position_id) in zip(image_sizes, curr_kvlens, curr_rope):
693
+ packed_key_value_indexes.extend(range(curr, curr + curr_kvlen))
694
+ curr += curr_kvlen
695
+ (h, w) = (H // self.config.patch_size, W // self.config.patch_size)
696
+ num_image_tokens = h * w
697
+ patch_dim = self.config.patch_size * self.config.patch_size * 3
698
+ packed_init_noises.append(torch.randn(num_image_tokens, patch_dim))
699
+ packed_pixel_token_indexes.extend(range(query_curr, query_curr + num_image_tokens))
700
+ packed_indexes.extend(range(curr, curr + num_image_tokens))
701
+ curr += num_image_tokens
702
+ query_curr += num_image_tokens
703
+ (mrope_ids, _) = self._image_mrope_position_ids(curr_position_id, h, w, include_start=False, include_end=False)
704
+ packed_position_ids.append(mrope_ids)
705
+ packed_seqlens.append(num_image_tokens + 0)
706
+ packed_position_ids = torch.cat(packed_position_ids, dim=1)
707
+ generation_input = {'packed_text_ids': torch.tensor(packed_text_ids, dtype=torch.long), 'packed_text_indexes': torch.tensor(packed_text_indexes, dtype=torch.long), 'packed_init_noises': torch.cat(packed_init_noises, dim=0), 'packed_pixel_token_indexes': torch.tensor(packed_pixel_token_indexes, dtype=torch.long), 'packed_seqlens': torch.tensor(packed_seqlens, dtype=torch.int), 'packed_position_ids': packed_position_ids, 'key_values_lens': torch.tensor(curr_kvlens, dtype=torch.int), 'packed_indexes': torch.tensor(packed_indexes, dtype=torch.long), 'packed_key_value_indexes': torch.tensor(packed_key_value_indexes, dtype=torch.long)}
708
+ return generation_input
709
+
710
+ def prepare_pixel_video_noise(self, curr_kvlens, curr_rope, video_sizes, fps=None, *, visual_start_prefilled: bool=False):
711
+ """Pixel-video version of :meth:`prepare_pixel_noise`."""
712
+ self._validate_pixel_generation_prefix_contract(visual_start_prefilled)
713
+ (packed_text_ids, packed_text_indexes) = (list(), list())
714
+ (packed_pixel_video_token_indexes, packed_init_noises) = (list(), list())
715
+ (packed_position_ids, packed_seqlens, packed_indexes) = (list(), list(), list())
716
+ packed_key_value_indexes = list()
717
+ query_curr = curr = 0
718
+ tube_frames = self.config.pixel_video_temporal_patch_size
719
+ patch_dim = tube_frames * self.config.patch_size * self.config.patch_size * 3
720
+ for ((num_frames, H, W), curr_kvlen, curr_position_id) in zip(video_sizes, curr_kvlens, curr_rope):
721
+ if num_frames % tube_frames != 0:
722
+ raise ValueError(f'Expected num_frames to be divisible by tube_frames={tube_frames}, got {num_frames}')
723
+ else:
724
+ temporal_groups = num_frames // tube_frames
725
+ packed_key_value_indexes.extend(range(curr, curr + curr_kvlen))
726
+ curr += curr_kvlen
727
+ (h, w) = (H // self.config.patch_size, W // self.config.patch_size)
728
+ num_video_tokens = h * w * temporal_groups
729
+ packed_init_noises.append(torch.randn(num_video_tokens, patch_dim))
730
+ packed_pixel_video_token_indexes.extend(range(query_curr, query_curr + num_video_tokens))
731
+ packed_indexes.extend(range(curr, curr + num_video_tokens))
732
+ curr += num_video_tokens
733
+ query_curr += num_video_tokens
734
+ (mrope_ids, _) = self._video_mrope_position_ids(curr_position_id, temporal_groups, h, w, fps=fps, temporal_compression_factor=tube_frames, include_start=False, include_end=False)
735
+ packed_position_ids.append(mrope_ids)
736
+ packed_seqlens.append(num_video_tokens + 0)
737
+ packed_position_ids = torch.cat(packed_position_ids, dim=1)
738
+ generation_input = {'packed_text_ids': torch.tensor(packed_text_ids, dtype=torch.long), 'packed_text_indexes': torch.tensor(packed_text_indexes, dtype=torch.long), 'packed_init_noises': torch.cat(packed_init_noises, dim=0), 'packed_pixel_token_indexes': torch.tensor(packed_pixel_video_token_indexes, dtype=torch.long), 'packed_seqlens': torch.tensor(packed_seqlens, dtype=torch.int), 'packed_position_ids': packed_position_ids, 'key_values_lens': torch.tensor(curr_kvlens, dtype=torch.int), 'packed_indexes': torch.tensor(packed_indexes, dtype=torch.long), 'packed_key_value_indexes': torch.tensor(packed_key_value_indexes, dtype=torch.long)}
739
+ return generation_input
740
+
741
+ def prepare_pixel_images(self, curr_kvlens, curr_rope, images):
742
+ """Prepare clean pixel patches for understanding (KV cache prefill).
743
+
744
+ Pixel-space version of prepare_vit_images. No ViT — PIL image → pixel patchify.
745
+ Uses 1D RoPE (same as rest of unified PixelUMM).
746
+ """
747
+ packed_pixel_token_indexes = list()
748
+ packed_pixel_patches = []
749
+ (packed_text_ids, packed_text_indexes) = (list(), list())
750
+ (packed_seqlens, packed_position_ids, packed_indexes) = (list(), list(), list())
751
+ packed_key_value_indexes = list()
752
+ _curr = curr = 0
753
+ (newlens, new_rope) = (list(), list())
754
+ for (image, curr_kvlen, curr_position_id) in zip(images, curr_kvlens, curr_rope):
755
+ packed_key_value_indexes.extend(range(curr, curr + curr_kvlen))
756
+ curr += curr_kvlen
757
+ ps = self.config.patch_size
758
+ (patches, (h_patches, w_patches)) = patchify_image(image, ps, max_size=max(image.size))
759
+ num_img_tokens = patches.shape[0]
760
+ (mrope_h, mrope_w) = (h_patches, w_patches)
761
+ packed_pixel_patches.append(patches)
762
+ packed_pixel_token_indexes.extend(range(_curr, _curr + num_img_tokens))
763
+ packed_indexes.extend(range(curr, curr + num_img_tokens))
764
+ curr += num_img_tokens
765
+ _curr += num_img_tokens
766
+ (mrope_ids, next_position_id) = self._image_mrope_position_ids(curr_position_id, mrope_h, mrope_w, include_start=False, include_end=False)
767
+ packed_position_ids.append(mrope_ids)
768
+ new_rope.append(next_position_id)
769
+ packed_seqlens.append(num_img_tokens)
770
+ newlens.append(curr_kvlen + num_img_tokens)
771
+ packed_position_ids = torch.cat(packed_position_ids, dim=1)
772
+ generation_input = {'packed_text_ids': torch.tensor(packed_text_ids, dtype=torch.long), 'packed_text_indexes': torch.tensor(packed_text_indexes, dtype=torch.long), 'packed_pixel_patches': torch.cat(packed_pixel_patches, dim=0), 'packed_pixel_token_indexes': torch.tensor(packed_pixel_token_indexes, dtype=torch.long), 'packed_position_ids': packed_position_ids, 'packed_seqlens': torch.tensor(packed_seqlens, dtype=torch.int), 'packed_indexes': torch.tensor(packed_indexes, dtype=torch.long), 'packed_key_value_indexes': torch.tensor(packed_key_value_indexes, dtype=torch.long), 'key_values_lens': torch.tensor(curr_kvlens, dtype=torch.int)}
773
+ return (generation_input, newlens, new_rope)
774
+
775
+ def prepare_text_spans_for_cache(self, curr_kvlens, curr_rope, texts, tokenizer):
776
+ """Causal mid-sequence text spans (e.g. V2T tube timestamp markers)."""
777
+ ids_list = [self._encode_prompt_text(tokenizer, text) for text in texts]
778
+ if any((len(ids) == 0 for ids in ids_list)):
779
+ raise ValueError('Cache text span cannot be empty')
780
+ return self._prepare_token_ids_as_text_cache_input(curr_kvlens, curr_rope, ids_list)
781
+
782
+ def prepare_pixel_video_und_tubes_for_text(self, curr_kvlens, curr_rope, tube_tensors):
783
+ """Pack video-UND tubes with the released 3D sequence/H/W coordinates."""
784
+ if not self.config.pixel_video_separate_und_gen_embedder:
785
+ raise ValueError('This checkpoint profile has no video UND embedder; video-VLM tubes are unsupported')
786
+ if not len(curr_kvlens) == len(curr_rope) == len(tube_tensors):
787
+ raise ValueError('V2T tube batches must match cache batch size')
788
+ temporal_patch_size = int(self.config.pixel_video_temporal_patch_size)
789
+ ps = self.config.patch_size
790
+ packed_pixel_video_patches = []
791
+ packed_pixel_video_patch_indexes = []
792
+ packed_position_ids = []
793
+ packed_seqlens = []
794
+ packed_indexes = []
795
+ packed_key_value_indexes = []
796
+ newlens = []
797
+ new_rope = []
798
+ query_curr = 0
799
+ combined_curr = 0
800
+ for (curr_kvlen, curr_position_id, tube) in zip(curr_kvlens, curr_rope, tube_tensors):
801
+ if tube.dim() != 4 or tube.shape[0] != temporal_patch_size:
802
+ raise ValueError(f'V2T tube must be (temporal_patch_size, C, H, W) with temporal_patch_size={temporal_patch_size}, got {tuple(tube.shape)}')
803
+ (_, _, H, W) = tube.shape
804
+ if H % ps or W % ps:
805
+ raise ValueError(f'V2T tube spatial size {(H, W)} not divisible by patch={ps}')
806
+ h = H // ps
807
+ w = W // ps
808
+ num_tokens = h * w
809
+ packed_key_value_indexes.extend(range(combined_curr, combined_curr + curr_kvlen))
810
+ combined_curr += curr_kvlen
811
+ tokens = tubeify(tube, ps, temporal_patch_size)
812
+ packed_pixel_video_patches.append(tokens)
813
+ packed_pixel_video_patch_indexes.extend(range(query_curr, query_curr + num_tokens))
814
+ packed_indexes.extend(range(combined_curr, combined_curr + num_tokens))
815
+ query_curr += num_tokens
816
+ combined_curr += num_tokens
817
+ grid_s = float(curr_position_id)
818
+ position_axes = [torch.full((num_tokens,), grid_s, dtype=torch.float32)]
819
+ position_axes.extend([torch.arange(h, dtype=torch.float32).repeat_interleave(w), torch.arange(w, dtype=torch.float32).repeat(h)])
820
+ packed_position_ids.append(torch.stack(position_axes, dim=0))
821
+ packed_seqlens.append(num_tokens)
822
+ newlens.append(curr_kvlen + num_tokens)
823
+ new_rope.append(int(curr_position_id) + 1)
824
+ generation_input = {'packed_text_ids': torch.zeros(0, dtype=torch.long), 'packed_text_indexes': torch.zeros(0, dtype=torch.long), 'packed_pixel_video_patches': torch.cat(packed_pixel_video_patches, dim=0), 'packed_pixel_video_patch_indexes': torch.tensor(packed_pixel_video_patch_indexes, dtype=torch.long), 'packed_position_ids': torch.cat(packed_position_ids, dim=1), 'packed_seqlens': torch.tensor(packed_seqlens, dtype=torch.int), 'packed_indexes': torch.tensor(packed_indexes, dtype=torch.long), 'packed_key_value_indexes': torch.tensor(packed_key_value_indexes, dtype=torch.long), 'key_values_lens': torch.tensor(curr_kvlens, dtype=torch.int)}
825
+ return (generation_input, newlens, new_rope)
826
+
827
+ @torch.no_grad
828
+ def forward_cache_update_pixel(self, past_key_values: NaiveCache, packed_text_ids: torch.LongTensor, packed_text_indexes: torch.LongTensor, packed_pixel_patches: Optional[torch.Tensor]=None, packed_pixel_token_indexes: Optional[torch.LongTensor]=None, packed_position_ids: Optional[torch.LongTensor]=None, packed_seqlens: Optional[torch.IntTensor]=None, packed_indexes: Optional[torch.LongTensor]=None, packed_key_value_indexes: Optional[torch.LongTensor]=None, key_values_lens: Optional[torch.IntTensor]=None, packed_pixel_video_patches: Optional[torch.Tensor]=None, packed_pixel_video_patch_indexes: Optional[torch.LongTensor]=None):
829
+ """Prefill clean pixel patches into KV cache (understanding)."""
830
+ if packed_pixel_video_patches is not None and not self.config.pixel_video_separate_und_gen_embedder:
831
+ raise ValueError('This checkpoint profile has no video UND embedder; video-VLM tubes are unsupported')
832
+ packed_text_embedding = self.language_model.model.embed_tokens(packed_text_ids)
833
+ packed_sequence = packed_text_embedding.new_zeros((sum(packed_seqlens), self.hidden_size))
834
+ packed_sequence[packed_text_indexes] = packed_text_embedding
835
+ model_dtype = packed_text_embedding.dtype
836
+ if packed_pixel_video_patches is not None:
837
+ if packed_pixel_video_patch_indexes is None:
838
+ raise ValueError('Video pixel prefill requires video patch indexes')
839
+ pixel_embed = self.pixel_video_patch_embed(packed_pixel_video_patches.to(model_dtype))
840
+ pixel_token_indexes = packed_pixel_video_patch_indexes
841
+ else:
842
+ if packed_pixel_patches is None or packed_pixel_token_indexes is None:
843
+ raise ValueError('Pixel prefill requires image or video pixel patches')
844
+ pixel_embed = self.pixel_patch_embed(packed_pixel_patches.to(model_dtype))
845
+ pixel_token_indexes = packed_pixel_token_indexes
846
+ if pixel_embed.dtype != packed_sequence.dtype:
847
+ pixel_embed = pixel_embed.to(packed_sequence.dtype)
848
+ packed_sequence[pixel_token_indexes] = pixel_embed
849
+ output = self.language_model.forward_inference(packed_query_sequence=packed_sequence, query_lens=packed_seqlens, packed_query_position_ids=packed_position_ids, packed_query_indexes=packed_indexes, past_key_values=past_key_values, packed_key_value_indexes=packed_key_value_indexes, key_values_lens=key_values_lens, update_past_key_values=True, is_causal=False, mode='und')
850
+ return output.past_key_values
851
+
852
+ def generate_image(self, packed_text_ids: torch.LongTensor, packed_text_indexes: torch.LongTensor, packed_init_noises: torch.Tensor, packed_pixel_token_indexes: torch.LongTensor, packed_seqlens: torch.IntTensor, packed_position_ids: torch.LongTensor, packed_indexes: torch.LongTensor, past_key_values: NaiveCache, key_values_lens: torch.IntTensor, packed_key_value_indexes: torch.LongTensor, num_timesteps: int=24, timestep_shift: float=1.0, sampler: str='dpm-solver', cfg_renorm_min: float=0.0, cfg_renorm_type: str='global', cfg_interval: Optional[Tuple[float, float]]=[0, 1], cfg_text_scale: float=1.0, cfg_text_packed_query_indexes: Optional[torch.LongTensor]=None, cfg_text_packed_position_ids: Optional[torch.LongTensor]=None, cfg_text_past_key_values: Optional[NaiveCache]=None, cfg_text_key_values_lens: Optional[torch.IntTensor]=None, cfg_text_packed_key_value_indexes: Optional[torch.LongTensor]=None, cfg_img_scale: float=1.0, cfg_img_packed_query_indexes: Optional[torch.LongTensor]=None, cfg_img_packed_position_ids: Optional[torch.LongTensor]=None, cfg_img_past_key_values: Optional[NaiveCache]=None, cfg_img_key_values_lens: Optional[torch.IntTensor]=None, cfg_img_packed_key_value_indexes: Optional[torch.LongTensor]=None):
853
+ x_t = packed_init_noises
854
+
855
+ def _v_at(x, t_scalar):
856
+ """Compute velocity at state x and timestep t_scalar (with CFG gating)."""
857
+ ts = torch.tensor([t_scalar] * x.shape[0], device=x.device)
858
+ if t_scalar > cfg_interval[0] and t_scalar <= cfg_interval[1]:
859
+ (cts, cis) = (cfg_text_scale, cfg_img_scale)
860
+ else:
861
+ (cts, cis) = (1.0, 1.0)
862
+ return self._forward_flow(x_t=x, timestep=ts, packed_pixel_token_indexes=packed_pixel_token_indexes, packed_text_ids=packed_text_ids, packed_text_indexes=packed_text_indexes, packed_position_ids=packed_position_ids, packed_indexes=packed_indexes, packed_seqlens=packed_seqlens, key_values_lens=key_values_lens, past_key_values=past_key_values, packed_key_value_indexes=packed_key_value_indexes, cfg_renorm_min=cfg_renorm_min, cfg_renorm_type=cfg_renorm_type, cfg_text_scale=cts, cfg_text_packed_position_ids=cfg_text_packed_position_ids, cfg_text_packed_query_indexes=cfg_text_packed_query_indexes, cfg_text_key_values_lens=cfg_text_key_values_lens, cfg_text_past_key_values=cfg_text_past_key_values, cfg_text_packed_key_value_indexes=cfg_text_packed_key_value_indexes, cfg_img_scale=cis, cfg_img_packed_position_ids=cfg_img_packed_position_ids, cfg_img_packed_query_indexes=cfg_img_packed_query_indexes, cfg_img_key_values_lens=cfg_img_key_values_lens, cfg_img_past_key_values=cfg_img_past_key_values, cfg_img_packed_key_value_indexes=cfg_img_packed_key_value_indexes)
863
+ if sampler != 'dpm-solver':
864
+ raise ValueError(f"PixelUMM-release T2I requires sampler='dpm-solver', got {sampler!r}")
865
+ x_t = sample_flow_dpm(_v_at, x_t, num_steps=num_timesteps, shift=timestep_shift)
866
+ delimiter_tokens = 0
867
+ generated_lens = packed_seqlens - delimiter_tokens
868
+ unpacked_latent = x_t.split(generated_lens.tolist())
869
+ return unpacked_latent
870
+
871
+ @torch.no_grad
872
+ def generate_video(self, packed_text_ids: torch.LongTensor, packed_text_indexes: torch.LongTensor, packed_init_noises: torch.Tensor, packed_pixel_token_indexes: torch.LongTensor, packed_seqlens: torch.IntTensor, packed_position_ids: torch.LongTensor, packed_indexes: torch.LongTensor, past_key_values: NaiveCache, key_values_lens: torch.IntTensor, packed_key_value_indexes: torch.LongTensor, num_timesteps: int=24, timestep_shift: float=1.0, sampler: str='unipc', cfg_renorm_min: float=0.0, cfg_renorm_type: str='global', cfg_interval: Optional[Tuple[float, float]]=[0, 1], cfg_text_scale: float=1.0, cfg_text_packed_query_indexes: Optional[torch.LongTensor]=None, cfg_text_packed_position_ids: Optional[torch.LongTensor]=None, cfg_text_past_key_values: Optional[NaiveCache]=None, cfg_text_key_values_lens: Optional[torch.IntTensor]=None, cfg_text_packed_key_value_indexes: Optional[torch.LongTensor]=None, cfg_img_scale: float=1.0, cfg_img_packed_query_indexes: Optional[torch.LongTensor]=None, cfg_img_packed_position_ids: Optional[torch.LongTensor]=None, cfg_img_past_key_values: Optional[NaiveCache]=None, cfg_img_key_values_lens: Optional[torch.IntTensor]=None, cfg_img_packed_key_value_indexes: Optional[torch.LongTensor]=None):
873
+ x_t = packed_init_noises
874
+
875
+ def _v_at(x, t_scalar):
876
+ ts = torch.tensor([t_scalar] * x.shape[0], device=x.device)
877
+ if t_scalar > cfg_interval[0] and t_scalar <= cfg_interval[1]:
878
+ (cts, cis) = (cfg_text_scale, cfg_img_scale)
879
+ else:
880
+ (cts, cis) = (1.0, 1.0)
881
+ return self._forward_flow(x_t=x, timestep=ts, packed_pixel_token_indexes=packed_pixel_token_indexes, packed_text_ids=packed_text_ids, packed_text_indexes=packed_text_indexes, packed_position_ids=packed_position_ids, packed_indexes=packed_indexes, packed_seqlens=packed_seqlens, key_values_lens=key_values_lens, past_key_values=past_key_values, packed_key_value_indexes=packed_key_value_indexes, cfg_renorm_min=cfg_renorm_min, cfg_renorm_type=cfg_renorm_type, cfg_text_scale=cts, cfg_text_packed_position_ids=cfg_text_packed_position_ids, cfg_text_packed_query_indexes=cfg_text_packed_query_indexes, cfg_text_key_values_lens=cfg_text_key_values_lens, cfg_text_past_key_values=cfg_text_past_key_values, cfg_text_packed_key_value_indexes=cfg_text_packed_key_value_indexes, cfg_img_scale=cis, cfg_img_packed_position_ids=cfg_img_packed_position_ids, cfg_img_packed_query_indexes=cfg_img_packed_query_indexes, cfg_img_key_values_lens=cfg_img_key_values_lens, cfg_img_past_key_values=cfg_img_past_key_values, cfg_img_packed_key_value_indexes=cfg_img_packed_key_value_indexes, use_pixel_video=True)
882
+ if sampler == 'unipc':
883
+ x_t = sample_flow_unipc(_v_at, x_t, num_steps=num_timesteps, shift=timestep_shift)
884
+ elif sampler == 'dpm-solver':
885
+ if num_timesteps < 3:
886
+ raise ValueError('T2V DPM-Solver requires at least 3 steps')
887
+ x_t = sample_flow_dpm(_v_at, x_t, num_steps=num_timesteps, shift=timestep_shift)
888
+ else:
889
+ raise ValueError(f"PixelUMM T2V supports 'unipc' or 'dpm-solver', got {sampler!r}")
890
+ delimiter_tokens = 0
891
+ generated_lens = packed_seqlens - delimiter_tokens
892
+ return x_t.split(generated_lens.tolist())
893
+
894
+ @torch.no_grad
895
+ def _forward_flow(self, x_t: torch.Tensor, timestep: torch.LongTensor, packed_pixel_token_indexes: torch.LongTensor, packed_text_ids: torch.LongTensor, packed_text_indexes: torch.LongTensor, packed_indexes: torch.LongTensor, packed_position_ids: torch.LongTensor, packed_seqlens: torch.IntTensor, key_values_lens: torch.IntTensor, past_key_values: NaiveCache, packed_key_value_indexes: torch.LongTensor, cfg_renorm_min: float=0.0, cfg_renorm_type: str='global', cfg_text_scale: float=1.0, cfg_text_packed_position_ids: Optional[torch.LongTensor]=None, cfg_text_packed_query_indexes: Optional[torch.LongTensor]=None, cfg_text_key_values_lens: Optional[torch.Tensor]=None, cfg_text_past_key_values: Optional[NaiveCache]=None, cfg_text_packed_key_value_indexes: Optional[torch.LongTensor]=None, cfg_img_scale: float=1.0, cfg_img_packed_position_ids: Optional[torch.LongTensor]=None, cfg_img_packed_query_indexes: Optional[torch.LongTensor]=None, cfg_img_key_values_lens: Optional[torch.Tensor]=None, cfg_img_past_key_values: Optional[NaiveCache]=None, cfg_img_packed_key_value_indexes: Optional[torch.LongTensor]=None, use_pixel_video: bool=False):
896
+ packed_text_embedding = self.language_model.model.embed_tokens(packed_text_ids)
897
+ packed_sequence = packed_text_embedding.new_zeros((sum(packed_seqlens), self.hidden_size))
898
+ packed_sequence[packed_text_indexes] = packed_text_embedding
899
+ assert timestep.unique().shape[0] == 1
900
+ t_val = timestep[0].item()
901
+ x_t_raw = x_t
902
+ if use_pixel_video:
903
+ x_t = self.embed_pixel_video_tubes(x_t)
904
+ else:
905
+ image_embedder = self._image_patch_embedder(gen_model=True)
906
+ x_t = image_embedder(x_t)
907
+ if x_t.dtype != packed_sequence.dtype:
908
+ x_t = x_t.to(packed_sequence.dtype)
909
+ packed_generation_token_indexes = packed_pixel_token_indexes
910
+ packed_sequence[packed_generation_token_indexes] = x_t
911
+ extra_inputs = {'mode': 'gen', 'packed_gen_token_indexes': packed_generation_token_indexes, 'packed_und_token_indexes': packed_text_indexes}
912
+ output = self.language_model.forward_inference(packed_query_sequence=packed_sequence, query_lens=packed_seqlens, packed_query_position_ids=packed_position_ids, packed_query_indexes=packed_indexes, past_key_values=past_key_values, key_values_lens=key_values_lens, packed_key_value_indexes=packed_key_value_indexes, update_past_key_values=False, is_causal=False, **extra_inputs)
913
+
914
+ def _pixel_head(hidden_seq):
915
+ hidden_sub = hidden_seq[packed_pixel_token_indexes]
916
+ if use_pixel_video:
917
+ return self.video_unpatchify_head(hidden_sub)
918
+ return self.unpatchify_head(hidden_sub)
919
+ model_out = _pixel_head(output.packed_query_sequence)
920
+ v_t = (x_t_raw.float() - model_out.float()) / max(t_val, self.config.x_pred_t_min)
921
+ if cfg_text_scale > 1.0:
922
+ cfg_text_output = self.language_model.forward_inference(packed_query_sequence=packed_sequence, query_lens=packed_seqlens, packed_query_position_ids=cfg_text_packed_position_ids, packed_query_indexes=cfg_text_packed_query_indexes, past_key_values=cfg_text_past_key_values, key_values_lens=cfg_text_key_values_lens, packed_key_value_indexes=cfg_text_packed_key_value_indexes, update_past_key_values=False, is_causal=False, **extra_inputs)
923
+ cfg_text_out = _pixel_head(cfg_text_output.packed_query_sequence)
924
+ cfg_text_v_t = (x_t_raw.float() - cfg_text_out.float()) / max(t_val, self.config.x_pred_t_min)
925
+ if cfg_img_scale > 1.0:
926
+ cfg_img_output = self.language_model.forward_inference(packed_query_sequence=packed_sequence, query_lens=packed_seqlens, packed_query_position_ids=cfg_img_packed_position_ids, packed_query_indexes=cfg_img_packed_query_indexes, past_key_values=cfg_img_past_key_values, key_values_lens=cfg_img_key_values_lens, packed_key_value_indexes=cfg_img_packed_key_value_indexes, update_past_key_values=False, is_causal=False, **extra_inputs)
927
+ cfg_img_out = _pixel_head(cfg_img_output.packed_query_sequence)
928
+ cfg_img_v_t = (x_t_raw.float() - cfg_img_out.float()) / max(t_val, self.config.x_pred_t_min)
929
+ if cfg_text_scale > 1.0:
930
+ if cfg_renorm_type == 'text_channel':
931
+ v_t_text_ = cfg_text_v_t + cfg_text_scale * (v_t - cfg_text_v_t)
932
+ norm_v_t = torch.norm(v_t, dim=-1, keepdim=True)
933
+ norm_v_t_text_ = torch.norm(v_t_text_, dim=-1, keepdim=True)
934
+ scale = (norm_v_t / (norm_v_t_text_ + 1e-08)).clamp(min=cfg_renorm_min, max=1.0)
935
+ v_t_text = v_t_text_ * scale
936
+ if cfg_img_scale > 1.0:
937
+ v_t = cfg_img_v_t + cfg_img_scale * (v_t_text - cfg_img_v_t)
938
+ else:
939
+ v_t = v_t_text
940
+ else:
941
+ v_t_text_ = cfg_text_v_t + cfg_text_scale * (v_t - cfg_text_v_t)
942
+ if cfg_img_scale > 1.0:
943
+ v_t_ = cfg_img_v_t + cfg_img_scale * (v_t_text_ - cfg_img_v_t)
944
+ else:
945
+ v_t_ = v_t_text_
946
+ if cfg_renorm_type == 'none':
947
+ v_t = v_t_
948
+ else:
949
+ if cfg_renorm_type == 'global':
950
+ norm_v_t = torch.norm(v_t)
951
+ norm_v_t_ = torch.norm(v_t_)
952
+ elif cfg_renorm_type == 'channel':
953
+ norm_v_t = torch.norm(v_t, dim=-1, keepdim=True)
954
+ norm_v_t_ = torch.norm(v_t_, dim=-1, keepdim=True)
955
+ else:
956
+ raise NotImplementedError(f'{cfg_renorm_type} is not supported')
957
+ scale = (norm_v_t / (norm_v_t_ + 1e-08)).clamp(min=cfg_renorm_min, max=1.0)
958
+ v_t = v_t_ * scale
959
+ return v_t
960
+
961
+ def prepare_text_start_tokens(self, curr_kvlens, curr_rope, start_token_ids):
962
+ (packed_start_tokens, packed_key_value_indexes) = (list(), list())
963
+ packed_query_position_ids = list()
964
+ curr = 0
965
+ for (curr_kvlen, curr_position_id, token_id) in zip(curr_kvlens, curr_rope, start_token_ids):
966
+ packed_key_value_indexes.extend(range(curr, curr + curr_kvlen))
967
+ packed_start_tokens.append(int(token_id))
968
+ packed_query_position_ids.append(self._text_mrope_position_ids(curr_position_id, 1))
969
+ curr += curr_kvlen
970
+ packed_query_position_ids = torch.cat(packed_query_position_ids, dim=1)
971
+ generation_input = {'packed_start_tokens': torch.tensor(packed_start_tokens, dtype=torch.long), 'packed_query_position_ids': packed_query_position_ids, 'key_values_lens': torch.tensor(curr_kvlens, dtype=torch.int), 'packed_key_value_indexes': torch.tensor(packed_key_value_indexes, dtype=torch.long)}
972
+ return generation_input
973
+
974
+ def prepare_visual_delimiter_tokens(self, curr_kvlens, curr_rope, new_token_ids, delimiter: str):
975
+ """Prepare a single causal visual delimiter per sample for pixelumm_mot inference.
976
+
977
+ ``<soi>`` and ``<eoi>`` are text embeddings routed through the und expert,
978
+ but their MRoPE coordinates are the visual marker coordinates. For both
979
+ SenseNova and Qwen-VL MRoPE, those marker coordinates match
980
+ ``_text_mrope_position_ids(curr_position_id, 1)``.
981
+ """
982
+ if delimiter == 'start':
983
+ token_id = new_token_ids['start_of_image']
984
+ elif delimiter == 'end':
985
+ token_id = new_token_ids['end_of_image']
986
+ else:
987
+ raise ValueError(f"Unknown visual delimiter {delimiter!r}; expected 'start' or 'end'")
988
+ (packed_text_ids, packed_text_indexes) = (list(), list())
989
+ (packed_text_position_ids, packed_key_value_indexes) = (list(), list())
990
+ (newlens, new_rope) = (list(), list())
991
+ curr = 0
992
+ for (curr_kvlen, curr_position_id) in zip(curr_kvlens, curr_rope):
993
+ packed_key_value_indexes.extend(range(curr, curr + curr_kvlen))
994
+ curr += curr_kvlen
995
+ packed_text_ids.append(token_id)
996
+ packed_text_indexes.append(curr)
997
+ packed_text_position_ids.append(self._text_mrope_position_ids(curr_position_id, 1))
998
+ curr += 1
999
+ newlens.append(curr_kvlen + 1)
1000
+ new_rope.append(curr_position_id + 1)
1001
+ packed_text_position_ids = torch.cat(packed_text_position_ids, dim=1)
1002
+ generation_input = {'text_token_lens': torch.ones(len(curr_kvlens), dtype=torch.int), 'packed_text_ids': torch.tensor(packed_text_ids, dtype=torch.long), 'packed_text_position_ids': packed_text_position_ids, 'packed_text_indexes': torch.tensor(packed_text_indexes, dtype=torch.long), 'packed_key_value_indexes': torch.tensor(packed_key_value_indexes, dtype=torch.long), 'key_values_lens': torch.tensor(curr_kvlens, dtype=torch.int)}
1003
+ return (generation_input, newlens, new_rope)
1004
+
1005
+ @torch.no_grad
1006
+ def generate_text(self, past_key_values: NaiveCache, packed_key_value_indexes: torch.LongTensor, key_values_lens: torch.IntTensor, packed_start_tokens: torch.LongTensor, packed_query_position_ids: torch.LongTensor, max_length: int, do_sample: bool=False, temperature: float=1.0, end_token_id: int=None, return_generation_metadata: bool=False):
1007
+ step = 0
1008
+ generated_sequence = []
1009
+ curr_tokens = packed_start_tokens
1010
+ hit_end_token = False
1011
+ while step < max_length:
1012
+ generated_sequence.append(curr_tokens)
1013
+ packed_text_embedding = self.language_model.model.embed_tokens(curr_tokens)
1014
+ query_lens = torch.ones_like(curr_tokens)
1015
+ packed_query_indexes = torch.cumsum(key_values_lens, dim=0) + torch.arange(0, len(key_values_lens), device=key_values_lens.device, dtype=key_values_lens.dtype)
1016
+ uppacked = list(packed_key_value_indexes.split(key_values_lens.tolist(), dim=0))
1017
+ for i in range(len(uppacked)):
1018
+ uppacked[i] += i
1019
+ packed_key_value_indexes = torch.cat(uppacked, dim=0)
1020
+ output = self.language_model.forward_inference(packed_query_sequence=packed_text_embedding, query_lens=query_lens, packed_query_position_ids=packed_query_position_ids, packed_query_indexes=packed_query_indexes, past_key_values=past_key_values, key_values_lens=key_values_lens, packed_key_value_indexes=packed_key_value_indexes, update_past_key_values=True, is_causal=True, mode='und')
1021
+ past_key_values = output.past_key_values
1022
+ packed_query_sequence = output.packed_query_sequence
1023
+ pred_logits = self.language_model.lm_head(packed_query_sequence)
1024
+ if do_sample:
1025
+ probs = nn.functional.softmax(pred_logits / temperature, dim=-1)
1026
+ curr_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)
1027
+ else:
1028
+ curr_tokens = torch.argmax(pred_logits, dim=-1)
1029
+ uppacked = list(packed_key_value_indexes.split(key_values_lens.tolist(), dim=0))
1030
+ for i in range(len(uppacked)):
1031
+ uppacked[i] = torch.cat([uppacked[i], torch.tensor([uppacked[i][-1] + 1], device=uppacked[i].device)], dim=0)
1032
+ packed_key_value_indexes = torch.cat(uppacked, dim=0)
1033
+ key_values_lens = key_values_lens + 1
1034
+ packed_query_position_ids = self._advance_text_query_position_ids(packed_query_position_ids)
1035
+ step += 1
1036
+ if end_token_id is not None and curr_tokens[0] == end_token_id:
1037
+ hit_end_token = True
1038
+ break
1039
+ output_device = generated_sequence[0].device
1040
+ generated_sequence = torch.stack([i.to(output_device) for i in generated_sequence], dim=0)
1041
+ if return_generation_metadata:
1042
+ return (generated_sequence, {'hit_end_token': hit_end_token})
1043
+ return generated_sequence
modeling/pixelumm/qwen3_navit.py ADDED
@@ -0,0 +1,1056 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-FileCopyrightText: Copyright (c) 2024 The Qwen Team and The HuggingFace Inc. team.
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 Bytedance Ltd. and/or its affiliates.
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ # Qwen3 MoT + NaViT runtime for PixelUMM
7
+ # Based on Bagel/modeling/bagel/qwen2_navit.py (ByteDance, Apache-2.0).
8
+ # PixelUMM keeps only the released SenseNova T/H/W MRoPE topology:
9
+ # upstream FlexAttention for packed training and public FlashAttention varlen
10
+ # for inference.
11
+
12
+ import copy
13
+ from dataclasses import dataclass
14
+ from typing import List, Optional
15
+
16
+ import torch
17
+ from torch import nn
18
+ from torch.nn.attention.flex_attention import flex_attention
19
+ from transformers.modeling_utils import PreTrainedModel
20
+ from transformers.models.qwen3.modeling_qwen3 import (
21
+ Qwen3Attention,
22
+ Qwen3MLP,
23
+ Qwen3RMSNorm,
24
+ )
25
+ from transformers.utils import ModelOutput
26
+
27
+ from modeling.qwen3.configuration_qwen3 import Qwen3Config
28
+
29
+ try:
30
+ from flash_attn import flash_attn_varlen_func
31
+ except ImportError:
32
+ flash_attn_varlen_func = None
33
+
34
+
35
+ torch._dynamo.config.cache_size_limit = 512
36
+ torch._dynamo.config.accumulated_cache_size_limit = 4096
37
+ from torch._inductor.runtime.hints import TRITON_MAX_BLOCK
38
+
39
+ TRITON_MAX_BLOCK["X"] = 65536
40
+ # Space patch: flex_attention is only used by the training path, and ZeroGPU
41
+ # does not support torch.compile, so it is left uncompiled here.
42
+
43
+ def _apply_sequence_chunked(
44
+ fn,
45
+ seq: torch.Tensor,
46
+ chunk_size: int,
47
+ ) -> torch.Tensor:
48
+ """Apply a token-wise module in bounded sequence chunks.
49
+
50
+ Qwen3MLP is independent along the sequence dimension, so splitting that
51
+ dimension preserves its mathematical contract while bounding the transient
52
+ SwiGLU intermediate from ``sequence_length * intermediate_size`` to
53
+ ``chunk_size * intermediate_size``. A non-positive chunk size keeps the
54
+ original path. Empty tensors still call ``fn`` so pixelumm-zero branches keep
55
+ their zero-gradient autograd edges.
56
+ """
57
+ if chunk_size <= 0 or seq.shape[0] <= chunk_size:
58
+ return fn(seq)
59
+ return torch.cat(
60
+ tuple(fn(chunk) for chunk in seq.split(chunk_size, dim=0)),
61
+ dim=0,
62
+ )
63
+
64
+
65
+ def _sdpa_varlen_attention(
66
+ query_states, key_states, value_states, query_lengths, key_lengths, *, causal,
67
+ ):
68
+ """Space patch: PyTorch SDPA replacement for flash_attn_varlen_func.
69
+
70
+ Matches FlashAttention semantics: GQA by head repetition and bottom-right
71
+ aligned causal masking when key_length > query_length.
72
+ """
73
+ import torch.nn.functional as F
74
+
75
+ n_rep = query_states.shape[1] // key_states.shape[1]
76
+ outputs = []
77
+ q_start = k_start = 0
78
+ for q_len, k_len in zip(query_lengths, key_lengths):
79
+ q = query_states[q_start:q_start + q_len].transpose(0, 1).unsqueeze(0)
80
+ k = key_states[k_start:k_start + k_len].transpose(0, 1)
81
+ v = value_states[k_start:k_start + k_len].transpose(0, 1)
82
+ if n_rep > 1:
83
+ k = k.repeat_interleave(n_rep, dim=0)
84
+ v = v.repeat_interleave(n_rep, dim=0)
85
+ k = k.unsqueeze(0)
86
+ v = v.unsqueeze(0)
87
+ mask = None
88
+ is_causal = False
89
+ if causal and q_len > 1:
90
+ if q_len == k_len:
91
+ is_causal = True
92
+ else:
93
+ offset = k_len - q_len
94
+ rows = torch.arange(q_len, device=q.device).unsqueeze(1)
95
+ cols = torch.arange(k_len, device=q.device).unsqueeze(0)
96
+ mask = cols <= rows + offset
97
+ out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, is_causal=is_causal)
98
+ outputs.append(out.squeeze(0).transpose(0, 1))
99
+ q_start += q_len
100
+ k_start += k_len
101
+ return torch.cat(outputs, dim=0)
102
+
103
+ def _flash_varlen_inference_attention(
104
+ query_states: torch.Tensor,
105
+ key_states: torch.Tensor,
106
+ value_states: torch.Tensor,
107
+ query_lens,
108
+ key_lens,
109
+ *,
110
+ causal: bool,
111
+ ) -> torch.Tensor:
112
+ """Run the public FlashAttention varlen API with packed Q/K/V tensors."""
113
+
114
+
115
+ def _as_int_list(lengths) -> list[int]:
116
+ if isinstance(lengths, torch.Tensor):
117
+ lengths = lengths.detach().cpu().tolist()
118
+ return [int(length) for length in lengths]
119
+
120
+ query_lengths = _as_int_list(query_lens)
121
+ key_lengths = _as_int_list(key_lens)
122
+ if len(query_lengths) != len(key_lengths):
123
+ raise ValueError("query_lens and key_lens must contain the same number of samples")
124
+ if not query_lengths or any(length <= 0 for length in query_lengths + key_lengths):
125
+ raise ValueError("inference attention segments must be non-empty")
126
+ if sum(query_lengths) != query_states.shape[0]:
127
+ raise ValueError("query_lens do not match the flattened query tensor")
128
+ if sum(key_lengths) != key_states.shape[0] or key_states.shape[0] != value_states.shape[0]:
129
+ raise ValueError("key_lens do not match the flattened key/value tensors")
130
+ if causal and any(
131
+ key_length < query_length
132
+ for query_length, key_length in zip(query_lengths, key_lengths)
133
+ ):
134
+ raise ValueError("causal inference requires key_length >= query_length")
135
+ if query_states.shape[1] % key_states.shape[1] != 0:
136
+ raise ValueError(
137
+ "FlashAttention GQA requires query heads divisible by KV heads: "
138
+ f"query_heads={query_states.shape[1]} kv_heads={key_states.shape[1]}"
139
+ )
140
+
141
+ def _cumulative_lengths(lengths: list[int]) -> torch.Tensor:
142
+ cumulative = torch.zeros(
143
+ len(lengths) + 1,
144
+ dtype=torch.int32,
145
+ device=query_states.device,
146
+ )
147
+ cumulative[1:] = torch.tensor(
148
+ lengths,
149
+ dtype=torch.int32,
150
+ device=query_states.device,
151
+ ).cumsum(dim=0)
152
+ return cumulative
153
+
154
+ if flash_attn_varlen_func is None:
155
+ return _sdpa_varlen_attention(
156
+ query_states, key_states, value_states,
157
+ query_lengths, key_lengths, causal=causal,
158
+ )
159
+ result = flash_attn_varlen_func(
160
+ q=query_states.contiguous(),
161
+ k=key_states.contiguous(),
162
+ v=value_states.contiguous(),
163
+ cu_seqlens_q=_cumulative_lengths(query_lengths),
164
+ cu_seqlens_k=_cumulative_lengths(key_lengths),
165
+ max_seqlen_q=max(query_lengths),
166
+ max_seqlen_k=max(key_lengths),
167
+ causal=causal,
168
+ )
169
+ return result[0] if isinstance(result, tuple) else result
170
+
171
+ class NaiveCache:
172
+ def __init__(self, num_layers):
173
+ self.key_cache = {k: None for k in range(num_layers)}
174
+ self.value_cache = {k: None for k in range(num_layers)}
175
+
176
+ @property
177
+ def num_layers(self):
178
+ return len(self.key_cache)
179
+
180
+ @dataclass
181
+ class BaseNavitOutputWithPast(ModelOutput):
182
+ packed_query_sequence: torch.FloatTensor = None
183
+ past_key_values: Optional[NaiveCache] = None
184
+
185
+ def _compute_default_rope_parameters(config, device=None, **_kwargs):
186
+ """Default RoPE frequencies, copied from SenseNova's Qwen3 fork."""
187
+ base = config.rope_theta
188
+ partial_rotary_factor = getattr(config, "partial_rotary_factor", 1.0)
189
+ head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
190
+ dim = int(head_dim * partial_rotary_factor)
191
+ attention_factor = 1.0
192
+ inv_freq = 1.0 / (
193
+ base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim)
194
+ )
195
+ return inv_freq, attention_factor
196
+
197
+ def rotate_half(x):
198
+ """Rotates half the hidden dims of the input, copied from SenseNova's Qwen3 fork."""
199
+ x1 = x[..., : x.shape[-1] // 2]
200
+ x2 = x[..., x.shape[-1] // 2 :]
201
+ return torch.cat((-x2, x1), dim=-1)
202
+
203
+ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
204
+ """Apply RoPE exactly like SenseNova's Qwen3 fork.
205
+
206
+ ``position_ids`` is kept for API parity with the fork; it is unused.
207
+ """
208
+ del position_ids
209
+ cos = cos.unsqueeze(unsqueeze_dim)
210
+ sin = sin.unsqueeze(unsqueeze_dim)
211
+ q_embed = (q * cos) + (rotate_half(q) * sin)
212
+ k_embed = (k * cos) + (rotate_half(k) * sin)
213
+ return q_embed, k_embed
214
+
215
+ class SenseNovaQwen3RotaryEmbedding(nn.Module):
216
+ """Qwen3 rotary embedding copied from SenseNova's fork.
217
+
218
+ This mirrors ``references/SenseNova-U1/.../modeling_qwen3.py``. SenseNova
219
+ computes RoPE frequencies for a 2x wider axis and keeps every other
220
+ frequency, instead of recomputing a fresh reduced-dim frequency range.
221
+ """
222
+
223
+ inv_freq: torch.Tensor
224
+
225
+ @staticmethod
226
+ def compute_default_rope_parameters(config, device=None, seq_len=None):
227
+ del seq_len
228
+ inv_freq, attention_scaling = _compute_default_rope_parameters(config, device)
229
+
230
+ cfg2 = copy.deepcopy(config)
231
+ head_dim = getattr(cfg2, "head_dim", None)
232
+ if head_dim is None:
233
+ head_dim = cfg2.hidden_size // cfg2.num_attention_heads
234
+ setattr(cfg2, "head_dim", head_dim)
235
+ cfg2.head_dim = int(head_dim) * 2
236
+
237
+ inv_freq_full, _ = _compute_default_rope_parameters(cfg2, device)
238
+ return inv_freq_full[::2], attention_scaling
239
+
240
+ def __init__(self, config, device=None):
241
+ super().__init__()
242
+ if hasattr(config, "rope_scaling") and isinstance(config.rope_scaling, dict):
243
+ self.rope_type = config.rope_scaling.get("rope_type", config.rope_scaling.get("type"))
244
+ else:
245
+ self.rope_type = "default"
246
+ self.config = config
247
+
248
+ if self.rope_type == "default" or self.rope_type is None:
249
+ base_rope_init_fn = self.compute_default_rope_parameters
250
+ else:
251
+ raise ValueError(
252
+ f"SenseNovaQwen3RotaryEmbedding only supports the default release RoPE, got {self.rope_type}"
253
+ )
254
+
255
+ def _rope_init_fn_keep_freq_range(cfg, dev=None):
256
+ return base_rope_init_fn(cfg, dev)
257
+
258
+ self.rope_init_fn = _rope_init_fn_keep_freq_range
259
+
260
+ inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device)
261
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
262
+
263
+ @torch.no_grad()
264
+ def forward(self, x, position_ids):
265
+ inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
266
+ position_ids_expanded = position_ids[:, None, :].float()
267
+
268
+ device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu"
269
+ with torch.autocast(device_type=device_type, enabled=False):
270
+ freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
271
+ emb = torch.cat((freqs, freqs), dim=-1)
272
+ cos = emb.cos() * self.attention_scaling
273
+ sin = emb.sin() * self.attention_scaling
274
+
275
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
276
+
277
+ def pad_sequence(tensor, pad_size):
278
+ H, L, D = tensor.shape
279
+ pad_tensor = tensor.new_zeros((H, pad_size, D))
280
+ return torch.cat([tensor, pad_tensor], dim=1)
281
+
282
+ class SenseNovaMRoPEMixin:
283
+ def _init_sensenova_mrope(self, config):
284
+ if config.qwen3_mrope_type != "sensenova":
285
+ raise ValueError("PixelUMM requires qwen3_mrope_type='sensenova'")
286
+ if self.head_dim % 4 != 0:
287
+ raise ValueError(
288
+ "SenseNova split Q/K norm requires head_dim divisible by 4, "
289
+ f"got {self.head_dim}"
290
+ )
291
+ self.qwen3_mrope_type = "sensenova"
292
+
293
+ t_config = copy.deepcopy(config)
294
+ t_config.head_dim = self.head_dim // 2
295
+ t_config.rope_parameters = copy.deepcopy(config.rope_parameters)
296
+ self.rotary_emb_t = SenseNovaQwen3RotaryEmbedding(config=t_config)
297
+
298
+ hw_config = copy.deepcopy(config)
299
+ hw_config.head_dim = self.head_dim // 4
300
+ hw_config.rope_theta = getattr(config, "rope_theta_hw", 10000.0)
301
+ hw_config.max_position_embeddings = getattr(
302
+ config, "max_position_embeddings_hw", 10000
303
+ )
304
+ hw_config.rope_parameters = copy.deepcopy(config.rope_parameters)
305
+ hw_config.rope_parameters["rope_theta"] = hw_config.rope_theta
306
+ self.rotary_emb_hw = SenseNovaQwen3RotaryEmbedding(config=hw_config)
307
+
308
+ norm_dim = self.head_dim // 2
309
+ self.q_norm_t = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
310
+ self.q_norm_hw = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
311
+ self.k_norm_t = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
312
+ self.k_norm_hw = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
313
+
314
+ def _remove_unused_base_qk_norms(self):
315
+ num_removed = int(self.q_norm is not None) + int(self.k_norm is not None)
316
+ self.q_norm = None
317
+ self.k_norm = None
318
+ return num_removed
319
+
320
+ def _init_sensenova_mot_norms(self, config):
321
+ norm_dim = self.head_dim // 2
322
+ self.q_norm_t_moe_gen = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
323
+ self.q_norm_hw_moe_gen = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
324
+ self.k_norm_t_moe_gen = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
325
+ self.k_norm_hw_moe_gen = Qwen3RMSNorm(norm_dim, eps=config.rms_norm_eps)
326
+
327
+ @staticmethod
328
+ def _apply_sensenova_qk_norm(
329
+ query_states, key_states, q_norm_t, q_norm_hw, k_norm_t, k_norm_hw
330
+ ):
331
+ query_states_t, query_states_hw = query_states.chunk(2, dim=-1)
332
+ key_states_t, key_states_hw = key_states.chunk(2, dim=-1)
333
+ return (
334
+ torch.cat(
335
+ [q_norm_t(query_states_t), q_norm_hw(query_states_hw)], dim=-1
336
+ ),
337
+ torch.cat(
338
+ [k_norm_t(key_states_t), k_norm_hw(key_states_hw)], dim=-1
339
+ ),
340
+ )
341
+
342
+ def _apply_axis_rope(
343
+ self, query_states, key_states, rotary_emb, position_ids
344
+ ):
345
+ query_states = query_states.transpose(0, 1).unsqueeze(0)
346
+ key_states = key_states.transpose(0, 1).unsqueeze(0)
347
+ cos, sin = rotary_emb(query_states, position_ids.unsqueeze(0))
348
+ query_states, key_states = apply_rotary_pos_emb(
349
+ query_states, key_states, cos, sin
350
+ )
351
+ return (
352
+ query_states.squeeze(0).transpose(0, 1),
353
+ key_states.squeeze(0).transpose(0, 1),
354
+ )
355
+
356
+ def _apply_sensenova_mrope(
357
+ self, query_states, key_states, packed_position_ids
358
+ ):
359
+ packed_position_ids = packed_position_ids[:, : query_states.shape[0]]
360
+ if packed_position_ids.ndim != 2 or packed_position_ids.shape[0] != 3:
361
+ raise ValueError(
362
+ "SenseNova MRoPE expects T/H/W position IDs shaped (3, seq), "
363
+ f"got {tuple(packed_position_ids.shape)}"
364
+ )
365
+
366
+ query_t, query_hw = query_states.chunk(2, dim=-1)
367
+ query_h, query_w = query_hw.chunk(2, dim=-1)
368
+ key_t, key_hw = key_states.chunk(2, dim=-1)
369
+ key_h, key_w = key_hw.chunk(2, dim=-1)
370
+
371
+ query_t, key_t = self._apply_axis_rope(
372
+ query_t, key_t, self.rotary_emb_t, packed_position_ids[0]
373
+ )
374
+ query_h, key_h = self._apply_axis_rope(
375
+ query_h, key_h, self.rotary_emb_hw, packed_position_ids[1]
376
+ )
377
+ query_w, key_w = self._apply_axis_rope(
378
+ query_w, key_w, self.rotary_emb_hw, packed_position_ids[2]
379
+ )
380
+ return (
381
+ torch.cat([query_t, query_h, query_w], dim=-1),
382
+ torch.cat([key_t, key_h, key_w], dim=-1),
383
+ )
384
+
385
+ class PackedAttentionMoT(SenseNovaMRoPEMixin, Qwen3Attention):
386
+ def __init__(self, config, layer_idx: Optional[int] = None):
387
+ super().__init__(config, layer_idx)
388
+ self.hidden_size = config.hidden_size
389
+ self.num_heads = config.num_attention_heads
390
+ self.num_key_value_heads = config.num_key_value_heads
391
+ self._init_sensenova_mrope(config)
392
+ self._init_sensenova_mot_norms(config)
393
+
394
+ self.q_proj_moe_gen = nn.Linear(
395
+ self.hidden_size,
396
+ self.num_heads * self.head_dim,
397
+ bias=config.attention_bias,
398
+ )
399
+ self.k_proj_moe_gen = nn.Linear(
400
+ self.hidden_size,
401
+ self.num_key_value_heads * self.head_dim,
402
+ bias=config.attention_bias,
403
+ )
404
+ self.v_proj_moe_gen = nn.Linear(
405
+ self.hidden_size,
406
+ self.num_key_value_heads * self.head_dim,
407
+ bias=config.attention_bias,
408
+ )
409
+ self.o_proj_moe_gen = nn.Linear(
410
+ self.num_heads * self.head_dim,
411
+ self.hidden_size,
412
+ bias=False,
413
+ )
414
+
415
+ def forward(self, *args, **kwargs):
416
+ if self.training:
417
+ return self.forward_train(*args, **kwargs)
418
+ return self.forward_inference(*args, **kwargs)
419
+
420
+ def forward_train(
421
+ self,
422
+ packed_sequence: torch.Tensor,
423
+ sample_lens: Optional[List[int]],
424
+ attention_mask,
425
+ packed_position_embeddings: torch.Tensor,
426
+ packed_und_token_indexes: torch.LongTensor,
427
+ packed_gen_token_indexes: torch.LongTensor,
428
+ ) -> torch.Tensor:
429
+ del sample_lens
430
+ if packed_und_token_indexes is None or packed_gen_token_indexes is None:
431
+ raise ValueError("MoT training requires UND and GEN token indexes")
432
+
433
+ query = packed_sequence.new_zeros(
434
+ packed_sequence.shape[0], self.num_heads * self.head_dim
435
+ )
436
+ key = packed_sequence.new_zeros(
437
+ packed_sequence.shape[0],
438
+ self.num_key_value_heads * self.head_dim,
439
+ )
440
+ value = packed_sequence.new_zeros(
441
+ packed_sequence.shape[0],
442
+ self.num_key_value_heads * self.head_dim,
443
+ )
444
+ und = packed_sequence[packed_und_token_indexes]
445
+ gen = packed_sequence[packed_gen_token_indexes]
446
+ query[packed_und_token_indexes] = self.q_proj(und)
447
+ query[packed_gen_token_indexes] = self.q_proj_moe_gen(gen)
448
+ key[packed_und_token_indexes] = self.k_proj(und)
449
+ key[packed_gen_token_indexes] = self.k_proj_moe_gen(gen)
450
+ value[packed_und_token_indexes] = self.v_proj(und)
451
+ value[packed_gen_token_indexes] = self.v_proj_moe_gen(gen)
452
+
453
+ query = query.view(-1, self.num_heads, self.head_dim)
454
+ key = key.view(-1, self.num_key_value_heads, self.head_dim)
455
+ value = value.view(-1, self.num_key_value_heads, self.head_dim)
456
+ normalized_query = torch.zeros_like(query)
457
+ normalized_key = torch.zeros_like(key)
458
+ normalized_query[packed_und_token_indexes], normalized_key[
459
+ packed_und_token_indexes
460
+ ] = self._apply_sensenova_qk_norm(
461
+ query[packed_und_token_indexes],
462
+ key[packed_und_token_indexes],
463
+ self.q_norm_t,
464
+ self.q_norm_hw,
465
+ self.k_norm_t,
466
+ self.k_norm_hw,
467
+ )
468
+ normalized_query[packed_gen_token_indexes], normalized_key[
469
+ packed_gen_token_indexes
470
+ ] = self._apply_sensenova_qk_norm(
471
+ query[packed_gen_token_indexes],
472
+ key[packed_gen_token_indexes],
473
+ self.q_norm_t_moe_gen,
474
+ self.q_norm_hw_moe_gen,
475
+ self.k_norm_t_moe_gen,
476
+ self.k_norm_hw_moe_gen,
477
+ )
478
+ query, key = self._apply_sensenova_mrope(
479
+ normalized_query,
480
+ normalized_key,
481
+ packed_position_embeddings,
482
+ )
483
+
484
+ if not hasattr(attention_mask, "shape"):
485
+ raise TypeError(
486
+ "PixelUMM training requires a FlexAttention BlockMask"
487
+ )
488
+ pad_size = attention_mask.shape[-2] - query.shape[0]
489
+ if pad_size < 0:
490
+ raise ValueError("attention mask is shorter than the packed sequence")
491
+ query = pad_sequence(query.permute(1, 0, 2), pad_size)
492
+ key = pad_sequence(key.permute(1, 0, 2), pad_size)
493
+ value = pad_sequence(value.permute(1, 0, 2), pad_size)
494
+ output = flex_attention(
495
+ query.unsqueeze(0),
496
+ key.unsqueeze(0),
497
+ value.unsqueeze(0),
498
+ enable_gqa=True,
499
+ block_mask=attention_mask,
500
+ )
501
+ end_index = output.shape[2] - pad_size if pad_size else output.shape[2]
502
+ output = output[0, :, :end_index, :]
503
+ output = output.transpose(0, 1).reshape(
504
+ -1, self.num_heads * self.head_dim
505
+ )
506
+
507
+ projected = output.new_zeros(output.shape[0], self.hidden_size)
508
+ projected[packed_und_token_indexes] = self.o_proj(
509
+ output[packed_und_token_indexes]
510
+ )
511
+ projected[packed_gen_token_indexes] = self.o_proj_moe_gen(
512
+ output[packed_gen_token_indexes]
513
+ )
514
+ return projected
515
+
516
+ def forward_inference(
517
+ self,
518
+ packed_query_sequence: torch.Tensor,
519
+ query_lens: torch.Tensor,
520
+ packed_query_position_embeddings: torch.Tensor,
521
+ packed_query_indexes: torch.Tensor,
522
+ past_key_values: Optional[NaiveCache] = None,
523
+ key_values_lens: Optional[torch.Tensor] = None,
524
+ packed_key_value_indexes: Optional[torch.Tensor] = None,
525
+ update_past_key_values: bool = True,
526
+ is_causal: bool = True,
527
+ mode: str = "und",
528
+ packed_gen_token_indexes=None,
529
+ packed_und_token_indexes=None,
530
+ ):
531
+ if mode not in {"und", "gen"}:
532
+ raise ValueError(f"Unsupported inference mode: {mode!r}")
533
+
534
+ if mode == "und":
535
+ query = self.q_proj(packed_query_sequence).view(
536
+ -1, self.num_heads, self.head_dim
537
+ )
538
+ key = self.k_proj(packed_query_sequence).view(
539
+ -1, self.num_key_value_heads, self.head_dim
540
+ )
541
+ value = self.v_proj(packed_query_sequence).view(
542
+ -1, self.num_key_value_heads, self.head_dim
543
+ )
544
+ query, key = self._apply_sensenova_qk_norm(
545
+ query,
546
+ key,
547
+ self.q_norm_t,
548
+ self.q_norm_hw,
549
+ self.k_norm_t,
550
+ self.k_norm_hw,
551
+ )
552
+ else:
553
+ if packed_gen_token_indexes is None or packed_und_token_indexes is None:
554
+ raise ValueError("GEN inference requires UND and GEN token indexes")
555
+ sequence = packed_query_sequence.to(torch.bfloat16)
556
+ query = sequence.new_zeros(
557
+ sequence.shape[0], self.num_heads * self.head_dim
558
+ )
559
+ key = sequence.new_zeros(
560
+ sequence.shape[0],
561
+ self.num_key_value_heads * self.head_dim,
562
+ )
563
+ value = sequence.new_zeros(
564
+ sequence.shape[0],
565
+ self.num_key_value_heads * self.head_dim,
566
+ )
567
+ und = sequence[packed_und_token_indexes]
568
+ gen = sequence[packed_gen_token_indexes]
569
+ query[packed_und_token_indexes] = self.q_proj(und)
570
+ query[packed_gen_token_indexes] = self.q_proj_moe_gen(gen)
571
+ key[packed_und_token_indexes] = self.k_proj(und)
572
+ key[packed_gen_token_indexes] = self.k_proj_moe_gen(gen)
573
+ value[packed_und_token_indexes] = self.v_proj(und)
574
+ value[packed_gen_token_indexes] = self.v_proj_moe_gen(gen)
575
+ query = query.view(-1, self.num_heads, self.head_dim).float()
576
+ key = key.view(-1, self.num_key_value_heads, self.head_dim).float()
577
+ value = value.view(-1, self.num_key_value_heads, self.head_dim)
578
+ query[packed_und_token_indexes], key[
579
+ packed_und_token_indexes
580
+ ] = self._apply_sensenova_qk_norm(
581
+ query[packed_und_token_indexes],
582
+ key[packed_und_token_indexes],
583
+ self.q_norm_t,
584
+ self.q_norm_hw,
585
+ self.k_norm_t,
586
+ self.k_norm_hw,
587
+ )
588
+ query[packed_gen_token_indexes], key[
589
+ packed_gen_token_indexes
590
+ ] = self._apply_sensenova_qk_norm(
591
+ query[packed_gen_token_indexes],
592
+ key[packed_gen_token_indexes],
593
+ self.q_norm_t_moe_gen,
594
+ self.q_norm_hw_moe_gen,
595
+ self.k_norm_t_moe_gen,
596
+ self.k_norm_hw_moe_gen,
597
+ )
598
+
599
+ query, key = self._apply_sensenova_mrope(
600
+ query,
601
+ key,
602
+ packed_query_position_embeddings,
603
+ )
604
+ query = query.to(torch.bfloat16)
605
+ key = key.to(torch.bfloat16)
606
+ value = value.to(torch.bfloat16)
607
+
608
+ if past_key_values is not None and past_key_values.key_cache[
609
+ self.layer_idx
610
+ ] is not None:
611
+ if key_values_lens is None or packed_key_value_indexes is None:
612
+ raise ValueError(
613
+ "cached inference requires key lengths and KV indexes"
614
+ )
615
+ past_key = past_key_values.key_cache[self.layer_idx]
616
+ past_value = past_key_values.value_cache[self.layer_idx]
617
+ sequence_length = sum(query_lens) + sum(key_values_lens)
618
+ merged_key = past_key.new_zeros(
619
+ sequence_length,
620
+ self.num_key_value_heads,
621
+ self.head_dim,
622
+ )
623
+ merged_value = past_value.new_zeros(
624
+ sequence_length,
625
+ self.num_key_value_heads,
626
+ self.head_dim,
627
+ )
628
+ merged_key[packed_query_indexes] = key
629
+ merged_key[packed_key_value_indexes] = past_key
630
+ merged_value[packed_query_indexes] = value
631
+ merged_value[packed_key_value_indexes] = past_value
632
+ effective_key_lens = key_values_lens + query_lens
633
+ else:
634
+ merged_key = key
635
+ merged_value = value
636
+ effective_key_lens = query_lens
637
+
638
+ output = _flash_varlen_inference_attention(
639
+ query,
640
+ merged_key,
641
+ merged_value,
642
+ query_lens,
643
+ effective_key_lens,
644
+ causal=is_causal,
645
+ ).reshape(-1, self.num_heads * self.head_dim)
646
+ if mode == "und":
647
+ output = self.o_proj(output)
648
+ else:
649
+ projected = output.new_zeros(output.shape[0], self.hidden_size)
650
+ projected[packed_und_token_indexes] = self.o_proj(
651
+ output[packed_und_token_indexes]
652
+ )
653
+ projected[packed_gen_token_indexes] = self.o_proj_moe_gen(
654
+ output[packed_gen_token_indexes]
655
+ )
656
+ output = projected
657
+
658
+ if update_past_key_values:
659
+ if past_key_values is None:
660
+ raise ValueError("update_past_key_values requires a NaiveCache")
661
+ past_key_values.key_cache[self.layer_idx] = merged_key
662
+ past_key_values.value_cache[self.layer_idx] = merged_value
663
+ return output, past_key_values
664
+
665
+ class Qwen3MoTDecoderLayer(nn.Module):
666
+ def __init__(self, config, layer_idx: Optional[int] = None):
667
+ super().__init__()
668
+ self.hidden_size = config.hidden_size
669
+ self.qwen_mlp_sequence_chunk_size = int(
670
+ getattr(config, "qwen_mlp_sequence_chunk_size", 0)
671
+ )
672
+ if self.qwen_mlp_sequence_chunk_size < 0:
673
+ raise ValueError("qwen_mlp_sequence_chunk_size must be >= 0")
674
+ self.self_attn = PackedAttentionMoT(config, layer_idx)
675
+ self.mlp = Qwen3MLP(config)
676
+ self.mlp_moe_gen = Qwen3MLP(config)
677
+ self.input_layernorm = Qwen3RMSNorm(
678
+ config.hidden_size, eps=config.rms_norm_eps
679
+ )
680
+ self.post_attention_layernorm = Qwen3RMSNorm(
681
+ config.hidden_size, eps=config.rms_norm_eps
682
+ )
683
+ self.input_layernorm_moe_gen = Qwen3RMSNorm(
684
+ config.hidden_size, eps=config.rms_norm_eps
685
+ )
686
+ self.post_attention_layernorm_moe_gen = Qwen3RMSNorm(
687
+ config.hidden_size, eps=config.rms_norm_eps
688
+ )
689
+
690
+ def forward(self, *args, **kwargs):
691
+ if self.training:
692
+ return self.forward_train(*args, **kwargs)
693
+ return self.forward_inference(*args, **kwargs)
694
+
695
+ def forward_train(
696
+ self,
697
+ packed_sequence: torch.Tensor,
698
+ sample_lens: Optional[List[int]],
699
+ attention_mask,
700
+ packed_position_embeddings: torch.Tensor,
701
+ packed_und_token_indexes: torch.LongTensor,
702
+ packed_gen_token_indexes: torch.LongTensor,
703
+ ) -> torch.Tensor:
704
+ if packed_und_token_indexes is None or packed_gen_token_indexes is None:
705
+ raise ValueError("MoT training requires UND and GEN token indexes")
706
+
707
+ residual = packed_sequence
708
+ normalized = torch.zeros_like(packed_sequence)
709
+ und_normalized = self.input_layernorm(
710
+ packed_sequence[packed_und_token_indexes]
711
+ )
712
+ gen_normalized = self.input_layernorm_moe_gen(
713
+ packed_sequence[packed_gen_token_indexes]
714
+ )
715
+ normalized[packed_und_token_indexes] = und_normalized
716
+ normalized[packed_gen_token_indexes] = gen_normalized
717
+ packed_sequence = residual + self.self_attn(
718
+ packed_sequence=normalized,
719
+ sample_lens=sample_lens,
720
+ attention_mask=attention_mask,
721
+ packed_position_embeddings=packed_position_embeddings,
722
+ packed_und_token_indexes=packed_und_token_indexes,
723
+ packed_gen_token_indexes=packed_gen_token_indexes,
724
+ )
725
+
726
+ residual = packed_sequence
727
+ mlp_output = torch.zeros_like(packed_sequence)
728
+ und_mlp = _apply_sequence_chunked(
729
+ self.mlp,
730
+ self.post_attention_layernorm(
731
+ packed_sequence[packed_und_token_indexes]
732
+ ),
733
+ self.qwen_mlp_sequence_chunk_size,
734
+ )
735
+ gen_mlp = _apply_sequence_chunked(
736
+ self.mlp_moe_gen,
737
+ self.post_attention_layernorm_moe_gen(
738
+ packed_sequence[packed_gen_token_indexes]
739
+ ),
740
+ self.qwen_mlp_sequence_chunk_size,
741
+ )
742
+ mlp_output[packed_und_token_indexes] = und_mlp
743
+ mlp_output[packed_gen_token_indexes] = gen_mlp
744
+ return residual + mlp_output
745
+
746
+ def forward_inference(
747
+ self,
748
+ packed_query_sequence: torch.Tensor,
749
+ query_lens: torch.Tensor,
750
+ packed_query_position_embeddings: torch.Tensor,
751
+ packed_query_indexes: torch.Tensor,
752
+ past_key_values: Optional[NaiveCache] = None,
753
+ key_values_lens: Optional[torch.Tensor] = None,
754
+ packed_key_value_indexes: Optional[torch.Tensor] = None,
755
+ update_past_key_values: bool = True,
756
+ is_causal: bool = True,
757
+ mode: str = "und",
758
+ packed_gen_token_indexes=None,
759
+ packed_und_token_indexes=None,
760
+ ):
761
+ if mode == "und":
762
+ normalized = self.input_layernorm(packed_query_sequence)
763
+ elif mode == "gen":
764
+ if packed_gen_token_indexes is None or packed_und_token_indexes is None:
765
+ raise ValueError("GEN inference requires UND and GEN token indexes")
766
+ normalized = torch.zeros_like(packed_query_sequence)
767
+ normalized[packed_und_token_indexes] = self.input_layernorm(
768
+ packed_query_sequence[packed_und_token_indexes]
769
+ )
770
+ normalized[packed_gen_token_indexes] = self.input_layernorm_moe_gen(
771
+ packed_query_sequence[packed_gen_token_indexes]
772
+ )
773
+ else:
774
+ raise ValueError(f"Unsupported inference mode: {mode!r}")
775
+
776
+ residual = packed_query_sequence
777
+ attention_output, past_key_values = self.self_attn(
778
+ packed_query_sequence=normalized,
779
+ query_lens=query_lens,
780
+ packed_query_position_embeddings=packed_query_position_embeddings,
781
+ packed_query_indexes=packed_query_indexes,
782
+ past_key_values=past_key_values,
783
+ key_values_lens=key_values_lens,
784
+ packed_key_value_indexes=packed_key_value_indexes,
785
+ update_past_key_values=update_past_key_values,
786
+ is_causal=is_causal,
787
+ mode=mode,
788
+ packed_gen_token_indexes=packed_gen_token_indexes,
789
+ packed_und_token_indexes=packed_und_token_indexes,
790
+ )
791
+ packed_query_sequence = residual + attention_output
792
+
793
+ residual = packed_query_sequence
794
+ if mode == "und":
795
+ mlp_output = self.mlp(
796
+ self.post_attention_layernorm(packed_query_sequence)
797
+ )
798
+ else:
799
+ mlp_output = torch.zeros_like(packed_query_sequence).to(
800
+ torch.bfloat16
801
+ )
802
+ mlp_output[packed_und_token_indexes] = self.mlp(
803
+ self.post_attention_layernorm(
804
+ packed_query_sequence[packed_und_token_indexes]
805
+ ).to(torch.bfloat16)
806
+ )
807
+ mlp_output[packed_gen_token_indexes] = self.mlp_moe_gen(
808
+ self.post_attention_layernorm_moe_gen(
809
+ packed_query_sequence[packed_gen_token_indexes]
810
+ ).to(torch.bfloat16)
811
+ )
812
+ return residual + mlp_output, past_key_values
813
+
814
+ class Qwen3PreTrainedModel(PreTrainedModel):
815
+ config_class = Qwen3Config
816
+ base_model_prefix = "model"
817
+ supports_gradient_checkpointing = True
818
+ _no_split_modules = ["Qwen3MoTDecoderLayer"]
819
+
820
+
821
+ class Qwen3Model(Qwen3PreTrainedModel):
822
+ def __init__(self, config):
823
+ super().__init__(config)
824
+ if config.layer_module != "Qwen3MoTDecoderLayer":
825
+ raise ValueError("PixelUMM requires Qwen3MoTDecoderLayer")
826
+ if config.qwen3_mrope_type != "sensenova":
827
+ raise ValueError("PixelUMM requires qwen3_mrope_type='sensenova'")
828
+ if config.freeze_und:
829
+ raise ValueError("PixelUMM release does not support freeze_und")
830
+ if config.conditional_branch_gradient_contract != "pixelumm_zero_v1":
831
+ raise ValueError(
832
+ "PixelUMM requires conditional_branch_gradient_contract="
833
+ "'pixelumm_zero_v1'"
834
+ )
835
+
836
+ self.padding_idx = config.pad_token_id
837
+ self.vocab_size = config.vocab_size
838
+ self.embed_tokens = nn.Embedding(
839
+ config.vocab_size, config.hidden_size, self.padding_idx
840
+ )
841
+ self.layers = nn.ModuleList(
842
+ [
843
+ Qwen3MoTDecoderLayer(config, layer_idx)
844
+ for layer_idx in range(config.num_hidden_layers)
845
+ ]
846
+ )
847
+ self.norm = Qwen3RMSNorm(
848
+ config.hidden_size, eps=config.rms_norm_eps
849
+ )
850
+ self.norm_moe_gen = Qwen3RMSNorm(
851
+ config.hidden_size, eps=config.rms_norm_eps
852
+ )
853
+ self.post_init()
854
+
855
+ def forward(self, *args, **kwargs):
856
+ if self.training:
857
+ return self.forward_train(*args, **kwargs)
858
+ return self.forward_inference(*args, **kwargs)
859
+
860
+ def forward_train(
861
+ self,
862
+ packed_sequence: torch.Tensor,
863
+ sample_lens: List[int],
864
+ attention_mask,
865
+ packed_position_ids: torch.Tensor,
866
+ packed_und_token_indexes: Optional[torch.LongTensor] = None,
867
+ packed_gen_token_indexes: Optional[torch.LongTensor] = None,
868
+ ) -> torch.Tensor:
869
+ if packed_position_ids.ndim != 2 or packed_position_ids.shape[0] != 3:
870
+ raise ValueError(
871
+ "PixelUMM requires T/H/W position IDs shaped (3, seq), "
872
+ f"got {tuple(packed_position_ids.shape)}"
873
+ )
874
+ if packed_und_token_indexes is None:
875
+ raise ValueError("packed_und_token_indexes is required")
876
+ if packed_gen_token_indexes is None:
877
+ packed_gen_token_indexes = packed_und_token_indexes.new_empty((0,))
878
+ position_ids = packed_position_ids[
879
+ :, : packed_sequence.shape[0]
880
+ ].contiguous()
881
+
882
+ for layer in self.layers:
883
+ packed_sequence = layer(
884
+ packed_sequence=packed_sequence,
885
+ sample_lens=None,
886
+ attention_mask=attention_mask,
887
+ packed_position_embeddings=position_ids,
888
+ packed_und_token_indexes=packed_und_token_indexes,
889
+ packed_gen_token_indexes=packed_gen_token_indexes,
890
+ )
891
+
892
+ output = torch.zeros_like(packed_sequence)
893
+ output[packed_und_token_indexes] = self.norm(
894
+ packed_sequence[packed_und_token_indexes]
895
+ )
896
+ output[packed_gen_token_indexes] = self.norm_moe_gen(
897
+ packed_sequence[packed_gen_token_indexes]
898
+ )
899
+ return output
900
+
901
+ def forward_inference(
902
+ self,
903
+ packed_query_sequence: torch.Tensor,
904
+ query_lens: torch.Tensor,
905
+ packed_query_position_ids: torch.Tensor,
906
+ packed_query_indexes: torch.Tensor,
907
+ past_key_values: Optional[NaiveCache] = None,
908
+ key_values_lens: Optional[torch.Tensor] = None,
909
+ packed_key_value_indexes: Optional[torch.Tensor] = None,
910
+ update_past_key_values: bool = True,
911
+ is_causal: bool = True,
912
+ mode: str = "und",
913
+ packed_gen_token_indexes=None,
914
+ packed_und_token_indexes=None,
915
+ ) -> BaseNavitOutputWithPast:
916
+ if (
917
+ packed_query_position_ids.ndim != 2
918
+ or packed_query_position_ids.shape[0] != 3
919
+ ):
920
+ raise ValueError(
921
+ "PixelUMM requires T/H/W query position IDs shaped (3, seq), "
922
+ f"got {tuple(packed_query_position_ids.shape)}"
923
+ )
924
+ position_ids = packed_query_position_ids[
925
+ :, : packed_query_sequence.shape[0]
926
+ ].contiguous()
927
+ extra_inputs = {"mode": mode}
928
+ if mode == "gen":
929
+ if packed_gen_token_indexes is None or packed_und_token_indexes is None:
930
+ raise ValueError("GEN inference requires UND and GEN token indexes")
931
+ extra_inputs.update(
932
+ packed_gen_token_indexes=packed_gen_token_indexes,
933
+ packed_und_token_indexes=packed_und_token_indexes,
934
+ )
935
+ elif mode != "und":
936
+ raise ValueError(f"Unsupported inference mode: {mode!r}")
937
+
938
+ for layer in self.layers:
939
+ packed_query_sequence, past_key_values = layer(
940
+ packed_query_sequence=packed_query_sequence,
941
+ query_lens=query_lens,
942
+ packed_query_position_embeddings=position_ids,
943
+ packed_query_indexes=packed_query_indexes,
944
+ past_key_values=past_key_values,
945
+ key_values_lens=key_values_lens,
946
+ packed_key_value_indexes=packed_key_value_indexes,
947
+ update_past_key_values=update_past_key_values,
948
+ is_causal=is_causal,
949
+ **extra_inputs,
950
+ )
951
+
952
+ if mode == "und":
953
+ packed_query_sequence = self.norm(packed_query_sequence)
954
+ else:
955
+ output = torch.zeros_like(packed_query_sequence)
956
+ output[packed_und_token_indexes] = self.norm(
957
+ packed_query_sequence[packed_und_token_indexes]
958
+ )
959
+ output[packed_gen_token_indexes] = self.norm_moe_gen(
960
+ packed_query_sequence[packed_gen_token_indexes]
961
+ )
962
+ packed_query_sequence = output
963
+ return BaseNavitOutputWithPast(
964
+ packed_query_sequence=packed_query_sequence,
965
+ past_key_values=past_key_values,
966
+ )
967
+
968
+
969
+ class Qwen3ForCausalLM(Qwen3PreTrainedModel):
970
+ _tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
971
+
972
+ def __init__(self, config):
973
+ super().__init__(config)
974
+ self.model = Qwen3Model(config)
975
+ self.vocab_size = config.vocab_size
976
+ self.lm_head = nn.Linear(
977
+ config.hidden_size, config.vocab_size, bias=False
978
+ )
979
+ self.post_init()
980
+
981
+ def remove_unused_sensenova_base_qk_norms(self):
982
+ num_removed = 0
983
+ for layer in self.model.layers:
984
+ num_removed += layer.self_attn._remove_unused_base_qk_norms()
985
+ return num_removed
986
+
987
+ def get_input_embeddings(self):
988
+ return self.model.embed_tokens
989
+
990
+ def set_input_embeddings(self, value):
991
+ self.model.embed_tokens = value
992
+
993
+ def get_output_embeddings(self):
994
+ return self.lm_head
995
+
996
+ def set_output_embeddings(self, new_embeddings):
997
+ self.lm_head = new_embeddings
998
+
999
+ def set_decoder(self, decoder):
1000
+ self.model = decoder
1001
+
1002
+ def get_decoder(self):
1003
+ return self.model
1004
+
1005
+ def forward(self, *args, **kwargs):
1006
+ if self.training:
1007
+ return self.forward_train(*args, **kwargs)
1008
+ return self.forward_inference(*args, **kwargs)
1009
+
1010
+ def forward_train(
1011
+ self,
1012
+ packed_sequence: torch.Tensor,
1013
+ sample_lens: List[int],
1014
+ attention_mask,
1015
+ packed_position_ids: torch.Tensor,
1016
+ packed_und_token_indexes: Optional[torch.LongTensor] = None,
1017
+ packed_gen_token_indexes: Optional[torch.LongTensor] = None,
1018
+ ) -> torch.Tensor:
1019
+ return self.model(
1020
+ packed_sequence=packed_sequence,
1021
+ sample_lens=sample_lens,
1022
+ packed_position_ids=packed_position_ids,
1023
+ attention_mask=attention_mask,
1024
+ packed_und_token_indexes=packed_und_token_indexes,
1025
+ packed_gen_token_indexes=packed_gen_token_indexes,
1026
+ )
1027
+
1028
+ def forward_inference(
1029
+ self,
1030
+ packed_query_sequence: torch.Tensor,
1031
+ query_lens: torch.Tensor,
1032
+ packed_query_position_ids: torch.Tensor,
1033
+ packed_query_indexes: torch.Tensor,
1034
+ past_key_values: Optional[NaiveCache] = None,
1035
+ key_values_lens: Optional[torch.Tensor] = None,
1036
+ packed_key_value_indexes: Optional[torch.Tensor] = None,
1037
+ update_past_key_values: bool = True,
1038
+ is_causal: bool = True,
1039
+ mode: str = "und",
1040
+ packed_gen_token_indexes=None,
1041
+ packed_und_token_indexes=None,
1042
+ ) -> BaseNavitOutputWithPast:
1043
+ return self.model(
1044
+ packed_query_sequence=packed_query_sequence,
1045
+ query_lens=query_lens,
1046
+ packed_query_position_ids=packed_query_position_ids,
1047
+ packed_query_indexes=packed_query_indexes,
1048
+ past_key_values=past_key_values,
1049
+ key_values_lens=key_values_lens,
1050
+ packed_key_value_indexes=packed_key_value_indexes,
1051
+ update_past_key_values=update_past_key_values,
1052
+ is_causal=is_causal,
1053
+ mode=mode,
1054
+ packed_gen_token_indexes=packed_gen_token_indexes,
1055
+ packed_und_token_indexes=packed_und_token_indexes,
1056
+ )
modeling/pixelumm/time_schedule.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Released PixelUMM noise-time sampling and spatial shift."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import torch
9
+
10
+
11
+ def sample_logit_normal_noise_time(
12
+ raw: torch.Tensor,
13
+ *,
14
+ logit_mean: float,
15
+ logit_std: float,
16
+ ) -> torch.Tensor:
17
+ """Map N(0,1) variates to PixelUMM noise time; -inf means clean."""
18
+
19
+ if logit_std <= 0:
20
+ raise ValueError(f"logit_std must be positive, got {logit_std}")
21
+ noise_time = torch.sigmoid(raw * float(logit_std) + float(logit_mean))
22
+ return torch.where(torch.isneginf(raw), torch.zeros_like(noise_time), noise_time)
23
+
24
+
25
+ def pixelumm_resolution_shift(height: int, width: int) -> float:
26
+ """Select the released spatial shift from the target short edge."""
27
+
28
+ height = int(height)
29
+ width = int(width)
30
+ if height <= 0 or width <= 0:
31
+ raise ValueError(f"Spatial dimensions must be positive, got {(height, width)}")
32
+ short_edge = min(height, width)
33
+ if short_edge <= 256:
34
+ return 1.0
35
+ if short_edge <= 640:
36
+ return 2.0
37
+ if short_edge <= 960:
38
+ return 3.0
39
+ raise ValueError(
40
+ "PixelUMM resolution shift supports short edges up to 960 px; "
41
+ f"got shape {(height, width)}"
42
+ )
43
+
44
+
45
+ def flow_shift_noise_timestep(t: torch.Tensor, shift: float) -> torch.Tensor:
46
+ """Shift PixelUMM noise time toward noise for shifts greater than one."""
47
+
48
+ if shift <= 0:
49
+ raise ValueError(f"shift must be positive, got {shift}")
50
+ return shift * t / (1 + (shift - 1) * t)
51
+
52
+
53
+ __all__ = [
54
+ "flow_shift_noise_timestep",
55
+ "pixelumm_resolution_shift",
56
+ "sample_logit_normal_noise_time",
57
+ ]
modeling/qwen3/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .configuration_qwen3 import Qwen3Config
5
+
6
+ __all__ = ["Qwen3Config"]
modeling/qwen3/configuration_qwen3.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Qwen3 configuration for PixelUMM — standalone, no HF transformers dependency."""
5
+
6
+ from transformers.configuration_utils import PretrainedConfig
7
+
8
+
9
+ class Qwen3Config(PretrainedConfig):
10
+ """
11
+ Qwen3-0.6B config with MoT extensions.
12
+
13
+ Qwen3-0.6B reference values:
14
+ hidden_size=1024, intermediate_size=3072, num_hidden_layers=28,
15
+ num_attention_heads=16, num_key_value_heads=8, head_dim=128,
16
+ attention_bias=False, rope_theta=1e6, vocab_size=151936
17
+ """
18
+
19
+ model_type = "qwen3"
20
+
21
+ def __init__(
22
+ self,
23
+ vocab_size=151936,
24
+ hidden_size=1024,
25
+ intermediate_size=3072,
26
+ num_hidden_layers=28,
27
+ num_attention_heads=16,
28
+ num_key_value_heads=8,
29
+ head_dim=128,
30
+ hidden_act="silu",
31
+ max_position_embeddings=40960,
32
+ initializer_range=0.02,
33
+ rms_norm_eps=1e-6,
34
+ use_cache=True,
35
+ tie_word_embeddings=True,
36
+ rope_theta=1000000.0,
37
+ rope_parameters=None, # dict with rope_type etc; None → default
38
+ attention_bias=False,
39
+ attention_dropout=0.0,
40
+ # Hugging Face Qwen compatibility fields.
41
+ # use_sliding_window,sliding_window,max_window_layers,layer_types
42
+ # PixelUMM's qwen3_navit path currently keeps full attention and does not consume
43
+ # sliding-window attention, but newer transformer configs may define them.
44
+ use_sliding_window=False,
45
+ sliding_window=4096,
46
+ max_window_layers=28,
47
+ layer_types=None,
48
+ pad_token_id=None,
49
+ bos_token_id=151643,
50
+ eos_token_id=151645,
51
+ # MoT extensions (same as Qwen2 version)
52
+ layer_module="Qwen3MoTDecoderLayer",
53
+ freeze_und=False,
54
+ qk_norm=True, # always True for Qwen3
55
+ # PixelUMM's released SenseNova T/H/W MRoPE topology.
56
+ qwen3_mrope_type="sensenova",
57
+ mrope_section=None,
58
+ rope_theta_hw=10000.0,
59
+ max_position_embeddings_hw=10000,
60
+ inference_attention_backend="flash_attn_varlen",
61
+ **kwargs,
62
+ ):
63
+ super().__init__(**kwargs)
64
+ self.vocab_size = vocab_size
65
+ self.hidden_size = hidden_size
66
+ self.intermediate_size = intermediate_size
67
+ self.num_hidden_layers = num_hidden_layers
68
+ self.num_attention_heads = num_attention_heads
69
+ self.num_key_value_heads = num_key_value_heads
70
+ self.head_dim = head_dim
71
+ self.hidden_act = hidden_act
72
+ self.max_position_embeddings = max_position_embeddings
73
+ self.initializer_range = initializer_range
74
+ self.rms_norm_eps = rms_norm_eps
75
+ self.use_cache = use_cache
76
+ self.tie_word_embeddings = tie_word_embeddings
77
+ self.rope_theta = rope_theta
78
+ # MRoPE section (set before rope_parameters which needs it)
79
+ if mrope_section is None:
80
+ self.mrope_section = [24, 20, 20] # default for head_dim=128
81
+ else:
82
+ self.mrope_section = mrope_section
83
+ self.qwen3_mrope_type = qwen3_mrope_type
84
+ self.rope_theta_hw = rope_theta_hw
85
+ self.max_position_embeddings_hw = max_position_embeddings_hw
86
+ # rope_parameters: ensure all fields needed by Qwen3 rotary and local interleaved MRoPE.
87
+ if rope_parameters is None:
88
+ rope_parameters = {"rope_type": "default"}
89
+ rope_parameters.setdefault("rope_theta", rope_theta)
90
+ rope_parameters.setdefault("mrope_section", self.mrope_section)
91
+ self.rope_parameters = rope_parameters
92
+ self.attention_bias = attention_bias
93
+ self.attention_dropout = attention_dropout
94
+ self.use_sliding_window = use_sliding_window
95
+ self.sliding_window = sliding_window if self.use_sliding_window else None
96
+ self.max_window_layers = max_window_layers
97
+ if layer_types is None:
98
+ layer_types = [
99
+ "sliding_attention"
100
+ if self.sliding_window is not None and i >= self.max_window_layers
101
+ else "full_attention"
102
+ for i in range(self.num_hidden_layers)
103
+ ]
104
+ self.layer_types = layer_types
105
+ self.pad_token_id = pad_token_id
106
+ self.bos_token_id = bos_token_id
107
+ self.eos_token_id = eos_token_id
108
+ self.layer_module = layer_module
109
+ self.freeze_und = freeze_und
110
+ self.qk_norm = qk_norm
111
+ self.inference_attention_backend = str(inference_attention_backend)
112
+ if self.inference_attention_backend != "flash_attn_varlen":
113
+ raise ValueError(
114
+ "inference_attention_backend must be 'flash_attn_varlen'; "
115
+ f"got {self.inference_attention_backend!r}"
116
+ )
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torchvision==0.28.0
2
+ torchcodec==0.16.0
3
+ transformers==4.57.1
4
+ huggingface_hub>=0.34.0,<1.0
5
+ accelerate
6
+ einops==0.8.1
7
+ numpy>=2.0,<2.3
8
+ Pillow
9
+ PyYAML
10
+ safetensors
11
+ tqdm
12
+ imageio-ffmpeg==0.6.0
train/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
3
+ # SPDX-License-Identifier: Apache-2.0
train/checkpoint_export.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Integrity receipts for static DCP exports without a training completion marker.
5
+
6
+ This verifies locally supplied model files, not upstream training completion,
7
+ publisher authenticity, optimizer resumability, or model/config compatibility.
8
+ Only prepare receipts for a trusted, fully downloaded, immutable export.
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ from pathlib import Path, PurePosixPath
14
+
15
+ EXPORT_MANIFEST = "checkpoint_export.json"
16
+ EXPORT_SCHEMA = "pixelumm_dcp_export_v1"
17
+
18
+
19
+ def _model_file(root: Path, name: str) -> Path:
20
+ if not isinstance(name, str):
21
+ raise ValueError("Export file path must be a string")
22
+ parts = PurePosixPath(name).parts
23
+ if (
24
+ len(parts) != 2 or parts[0] != "model"
25
+ or name != "/".join(parts) or "\\" in name
26
+ or (parts[1] != ".metadata" and not parts[1].endswith(".distcp"))
27
+ ):
28
+ raise ValueError(f"Invalid export model path: {name!r}")
29
+ path = root / name
30
+ if path.resolve().parent != (root / "model").resolve() or path.is_symlink():
31
+ raise ValueError(f"Export model path must stay inside model/: {name!r}")
32
+ return path
33
+
34
+
35
+ def _digest(path: Path) -> str:
36
+ before = path.stat()
37
+ digest = hashlib.sha256()
38
+ with path.open("rb") as stream:
39
+ for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""):
40
+ digest.update(chunk)
41
+ after = path.stat()
42
+ if (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
43
+ raise RuntimeError(f"Export changed during verification: {path}")
44
+ return digest.hexdigest()
45
+
46
+
47
+ def validate_export_manifest(root: Path) -> set[str]:
48
+ root = Path(root)
49
+ payload = json.loads((root / EXPORT_MANIFEST).read_text(encoding="utf-8"))
50
+ if not isinstance(payload, dict) or payload.get("schema") != EXPORT_SCHEMA:
51
+ raise ValueError("Unsupported checkpoint export manifest schema")
52
+ entries = payload.get("files")
53
+ if not isinstance(entries, list) or not entries:
54
+ raise ValueError("Checkpoint export manifest has no files")
55
+ names = set()
56
+ for entry in entries:
57
+ if not isinstance(entry, dict) or set(entry) != {"path", "bytes", "sha256"}:
58
+ raise ValueError("Malformed checkpoint export file entry")
59
+ name = entry["path"]
60
+ path = _model_file(root, name)
61
+ if name in names:
62
+ raise ValueError(f"Duplicate export file: {name}")
63
+ names.add(name)
64
+ if type(entry["bytes"]) is not int or entry["bytes"] <= 0:
65
+ raise ValueError(f"Invalid export file size: {name}")
66
+ expected = entry["sha256"]
67
+ if not isinstance(expected, str) or len(expected) != 64 or any(c not in "0123456789abcdef" for c in expected):
68
+ raise ValueError(f"Invalid export SHA256: {name}")
69
+ if not path.is_file() or path.stat().st_size != entry["bytes"]:
70
+ raise RuntimeError(f"Missing or wrong-size export file: {name}")
71
+ if _digest(path) != expected:
72
+ raise RuntimeError(f"Export SHA256 mismatch: {name}")
73
+ if "model/.metadata" not in names:
74
+ raise ValueError("Export manifest must include model/.metadata")
75
+ return names
76
+
77
+
78
+ def prepare_export_manifest(root: Path) -> Path:
79
+ from train.release_checkpoint import _validate_dcp_model
80
+
81
+ root = Path(root).expanduser().resolve()
82
+ output = root / EXPORT_MANIFEST
83
+ if output.exists():
84
+ raise FileExistsError(f"Preserving existing export manifest: {output}")
85
+ required = _validate_dcp_model(root)
86
+ if (root / "model.safetensors").exists():
87
+ raise ValueError("Prepare a DCP-only export, without model.safetensors")
88
+ files = []
89
+ for name in sorted(required):
90
+ path = _model_file(root, name)
91
+ files.append({"path": name, "bytes": path.stat().st_size, "sha256": _digest(path)})
92
+ payload = {
93
+ "schema": EXPORT_SCHEMA,
94
+ "verification": "local DCP file integrity; not a training completion assertion",
95
+ "files": files,
96
+ }
97
+ with output.open("x", encoding="utf-8") as stream:
98
+ stream.write(json.dumps(payload, indent=2) + "\n")
99
+ return output
train/config.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Typed configuration for the released F18 and F22 PixelUMM profiles."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, fields, replace
9
+ from pathlib import Path
10
+ from typing import Optional, TypeVar
11
+
12
+ from train.scientific_config import load_scientific_config
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ModelArguments:
17
+ llm_path: str = "Qwen/Qwen3-8B"
18
+ llm_qk_norm: bool = True
19
+ llm_backend: str = "qwen3"
20
+ qwen3_mrope_type: str = "sensenova"
21
+ qwen3_mrope_section: str = ""
22
+ qwen3_rope_theta: float = 1_000_000.0
23
+ tie_word_embeddings: bool = False
24
+ layer_module: str = "Qwen3MoTDecoderLayer"
25
+ pixel_token_patch_size: int = 16
26
+ pixel_embedder_type: str = "image_raw_patch_linear"
27
+ pixel_image_additive_pos_embed_type: str = "none"
28
+ pixel_video_additive_pos_embed_type: str = "none"
29
+ sensenova_vision_rope_theta: float = 10_000.0
30
+ sensenova_vision_max_position_embeddings: int = 10_000
31
+ add_timestep_embedding: bool = False
32
+ add_noise_scale_embedding: bool = False
33
+ noise_scale: float = 1.0
34
+ noise_scale_mode: str = "constant"
35
+ enable_pixel_video: bool = True
36
+ pixel_video_temporal_patch_size: int = 4
37
+ pixel_video_embedder_type: str = "video_raw_tube_linear"
38
+ pixel_separate_und_gen_embedder: bool = True
39
+ pixel_video_separate_und_gen_embedder: bool = True
40
+ packed_attention_impl: str = "flex"
41
+ inference_attention_backend: str = "flash_attn_varlen"
42
+ packed_expert_routing: str = "pixelumm_mot"
43
+ pixel_head_type: str = "minit2i_linear"
44
+ pixel_video_head_type: str = "jit_style"
45
+ qwen_mlp_sequence_chunk_size: int = 32_768
46
+ ce_loss_checkpoint_chunk_size: int = 4096
47
+ text_cond_dropout_prob: float = 0.1
48
+ pixel_gen_cond_dropout_prob: float = 0.1
49
+ pixel_und_cond_dropout_prob: float = 0.0
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class DataArguments:
54
+ packed_sequence_layout: str = "pixelumm_mot"
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class TrainingArguments:
59
+ gen_space: str = "pixel"
60
+ und_space: str = "pixel"
61
+ prediction_type: str = "x"
62
+ loss_type: str = "v_loss"
63
+ x_pred_t_min: float = 0.05
64
+ visual_gen: bool = True
65
+ visual_und: bool = True
66
+ freeze_und: bool = False
67
+ conditional_branch_gradient_contract: str = "pixelumm_zero_v1"
68
+ noise_time_logit_mean: float = 0.0
69
+ noise_time_logit_std: float = 1.0
70
+ time_schedule: str = "pixelumm_resolution"
71
+ mse_loss_reduction: str = "sample"
72
+ eval_num_timesteps: int = 50
73
+ eval_sampler: str = "dpm-solver"
74
+ eval_timestep_shift: float = 3.0
75
+ eval_cfg_text_scale: float = 3.5
76
+ eval_cfg_renorm_type: str = "none"
77
+ eval_video_sampler: str = "unipc"
78
+ eval_video_timestep_shift: float = 10.0
79
+ eval_video_cfg_text_scale: float = 6.0
80
+
81
+
82
+ T = TypeVar("T")
83
+
84
+
85
+ def _instantiate(cls: type[T], values: dict, *, section: str) -> T:
86
+ allowed = {item.name for item in fields(cls)}
87
+ unknown = sorted(set(values) - allowed)
88
+ if unknown:
89
+ raise RuntimeError(
90
+ f"release config has unsupported {section} fields: {unknown}"
91
+ )
92
+ return cls(**values)
93
+
94
+
95
+ def load_release_config(
96
+ path: str | Path,
97
+ *,
98
+ llm_path: str | None = None,
99
+ ) -> tuple[ModelArguments, DataArguments, TrainingArguments]:
100
+ sections = load_scientific_config(Path(path))
101
+ model_args = _instantiate(ModelArguments, sections["model"], section="model")
102
+ data_args = _instantiate(DataArguments, sections["data"], section="data")
103
+ training_args = _instantiate(
104
+ TrainingArguments, sections["training"], section="training"
105
+ )
106
+ if llm_path is not None:
107
+ model_args = replace(model_args, llm_path=str(llm_path))
108
+ return model_args, data_args, training_args
109
+
110
+
111
+ def parse_mrope_section(value: Optional[str]) -> Optional[list[int]]:
112
+ if value is None or not value.strip():
113
+ return None
114
+ section = [int(item.strip()) for item in value.strip("[]()").split(",") if item.strip()]
115
+ if len(section) != 3:
116
+ raise ValueError(f"Expected qwen3_mrope_section T,H,W, got {value!r}")
117
+ return section
118
+
119
+
120
+ def configure_qwen3_mrope_config(llm_config, model_args: ModelArguments) -> None:
121
+ llm_config.qwen3_mrope_type = model_args.qwen3_mrope_type
122
+ if not getattr(llm_config, "rope_parameters", None):
123
+ llm_config.rope_parameters = {}
124
+ if model_args.qwen3_rope_theta > 0:
125
+ theta = float(model_args.qwen3_rope_theta)
126
+ llm_config.rope_theta = theta
127
+ llm_config.rope_parameters["rope_theta"] = theta
128
+ section = parse_mrope_section(model_args.qwen3_mrope_section)
129
+ if section is None:
130
+ section = getattr(llm_config, "mrope_section", None)
131
+ if section is None:
132
+ section = llm_config.rope_parameters.get("mrope_section")
133
+ if section is not None:
134
+ llm_config.mrope_section = section
135
+ llm_config.rope_parameters["mrope_section"] = section
136
+ head_dim = getattr(
137
+ llm_config,
138
+ "head_dim",
139
+ llm_config.hidden_size // llm_config.num_attention_heads,
140
+ )
141
+ rotary_dim = 2 * sum(section)
142
+ if rotary_dim > head_dim:
143
+ raise ValueError(
144
+ f"mrope section {section} gives rotary_dim={rotary_dim} > head_dim={head_dim}"
145
+ )
146
+ llm_config.rope_parameters["partial_rotary_factor"] = rotary_dim / head_dim
train/eval_utils.py ADDED
@@ -0,0 +1,875 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """PixelUMM image, video, and multimodal inference helpers."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from copy import deepcopy
10
+ from pathlib import Path
11
+
12
+ import imageio_ffmpeg
13
+ import numpy as np
14
+ import torch
15
+ from PIL import Image
16
+
17
+ from data.data_utils import untubeify
18
+
19
+
20
+ def save_video_mp4(frames, output_dir, fps=16, filename="t2v.mp4"):
21
+ if not frames:
22
+ return None
23
+ path = str(Path(output_dir) / filename)
24
+ width, height = frames[0].size
25
+ fps = max(float(fps), 1.0)
26
+ writer = imageio_ffmpeg.write_frames(
27
+ path,
28
+ (width, height),
29
+ fps=fps,
30
+ codec="libx264",
31
+ pix_fmt_in="rgb24",
32
+ pix_fmt_out="yuv420p",
33
+ output_params=["-movflags", "+faststart"],
34
+ ffmpeg_log_level="error",
35
+ )
36
+ writer.send(None)
37
+ try:
38
+ for frame in frames:
39
+ writer.send(np.asarray(frame.convert("RGB")))
40
+ finally:
41
+ writer.close()
42
+ return path
43
+
44
+
45
+ def _video_tubes_to_pil_frames(video_tubes, image_size, num_frames, patch_size):
46
+ if not torch.isfinite(video_tubes).all().item():
47
+ raise RuntimeError("Video generation produced non-finite pixels")
48
+ frames = untubeify(video_tubes, image_size, num_frames, patch_size)
49
+ frames = ((frames.clamp(-1, 1) + 1) / 2 * 255).to(torch.uint8).numpy()
50
+ return [Image.fromarray(frames[i]) for i in range(num_frames)]
51
+
52
+
53
+ def _prefill_text_to_visual_chatml_context(
54
+ model, tokenizer, new_token_ids, prompt, device, kv, kv_lens, ropes,
55
+ ):
56
+ """Prefill the exact T2I/T2V training ChatML context into a KV cache."""
57
+ text_input, kv_lens, ropes = model.prepare_text_to_visual_chatml_prompts(
58
+ curr_kvlens=kv_lens,
59
+ curr_rope=ropes,
60
+ prompts=[prompt],
61
+ tokenizer=tokenizer,
62
+ new_token_ids=new_token_ids,
63
+ )
64
+ text_input = {
65
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
66
+ for key, value in text_input.items()
67
+ }
68
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
69
+ kv = model.forward_cache_update_text(kv, **text_input)
70
+ return kv, kv_lens, ropes
71
+
72
+
73
+ def _prefill_generation_visual_start(
74
+ model, new_token_ids, device, kv, kv_lens, ropes,
75
+ ):
76
+ """Cache the causal ``<vision_start>`` used by PixelUMM generation."""
77
+ start_input, kv_lens, ropes = model.prepare_visual_delimiter_tokens(
78
+ kv_lens, ropes, new_token_ids, delimiter="start",
79
+ )
80
+ start_input = {
81
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
82
+ for key, value in start_input.items()
83
+ }
84
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
85
+ kv = model.forward_cache_update_text(kv, **start_input)
86
+ return kv, kv_lens, ropes, True
87
+
88
+
89
+ def _generate_t2i_one(
90
+ model, tokenizer, new_token_ids, prompt, image_size, device,
91
+ num_timesteps, timestep_shift, sampler,
92
+ cfg_text_scale, cfg_img_scale, cfg_interval, cfg_renorm_type,
93
+ ):
94
+ """Run one T2I sample through the FSDP-wrapped model. Mirrors
95
+ test_inference_pixel.py:generate_t2i. Returns a PIL image."""
96
+ from modeling.pixelumm.qwen3_navit import NaiveCache
97
+
98
+ n_layers = model.config.llm_config.num_hidden_layers
99
+ kv = NaiveCache(n_layers)
100
+ kv_lens, ropes = [0], [0]
101
+
102
+ cfg_text_kv = deepcopy(kv) if cfg_text_scale > 1.0 else None
103
+ cfg_text_kv_lens = list(kv_lens) if cfg_text_scale > 1.0 else None
104
+ cfg_text_ropes = list(ropes) if cfg_text_scale > 1.0 else None
105
+ cfg_img_kv = deepcopy(kv) if cfg_img_scale > 1.0 else None
106
+ cfg_img_kv_lens = list(kv_lens) if cfg_img_scale > 1.0 else None
107
+ cfg_img_ropes = list(ropes) if cfg_img_scale > 1.0 else None
108
+
109
+ kv, kv_lens, ropes = _prefill_text_to_visual_chatml_context(
110
+ model, tokenizer, new_token_ids, prompt, device, kv, kv_lens, ropes,
111
+ )
112
+
113
+ kv, kv_lens, ropes, visual_start_prefilled = _prefill_generation_visual_start(
114
+ model, new_token_ids, device, kv, kv_lens, ropes,
115
+ )
116
+
117
+ generation_input = model.prepare_pixel_noise(
118
+ curr_kvlens=kv_lens, curr_rope=ropes,
119
+ image_sizes=[image_size],
120
+ visual_start_prefilled=visual_start_prefilled,
121
+ )
122
+ generation_input = {k: v.to(device) if isinstance(v, torch.Tensor) else v
123
+ for k, v in generation_input.items()}
124
+ # Preserve the full-precision RNG sample. Model forward remains under BF16
125
+ # autocast, while every sampler starts from an FP32 numerical state.
126
+ generation_input['packed_init_noises'] = generation_input['packed_init_noises'].float()
127
+
128
+ cfg_kwargs = {}
129
+ if cfg_text_scale > 1.0 and cfg_text_kv is not None:
130
+ # CFG text-uncond reference must match training's ChatML shell dropout, NOT an
131
+ # empty prefill. Training drops the caption to cfg_dropout_input_ids =
132
+ # <|im_start|>user\n<|im_end|>\n<|im_start|>assistant\n[EOS] (an empty caption
133
+ # through the same ChatML builder). Feed prompts=[""] so the uncond context the
134
+ # model denoises from equals what it saw as the dropout target.
135
+ cfg_text_kv, cfg_text_kv_lens, cfg_text_ropes = _prefill_text_to_visual_chatml_context(
136
+ model, tokenizer, new_token_ids, "", device,
137
+ cfg_text_kv, cfg_text_kv_lens, cfg_text_ropes,
138
+ )
139
+ (
140
+ cfg_text_kv,
141
+ cfg_text_kv_lens,
142
+ cfg_text_ropes,
143
+ cfg_text_start_prefilled,
144
+ ) = _prefill_generation_visual_start(
145
+ model, new_token_ids, device,
146
+ cfg_text_kv, cfg_text_kv_lens, cfg_text_ropes,
147
+ )
148
+ cfg_text_input = model.prepare_pixel_noise(
149
+ curr_kvlens=cfg_text_kv_lens, curr_rope=cfg_text_ropes,
150
+ image_sizes=[image_size],
151
+ visual_start_prefilled=cfg_text_start_prefilled,
152
+ )
153
+ cfg_text_input = {k: v.to(device) if isinstance(v, torch.Tensor) else v
154
+ for k, v in cfg_text_input.items()}
155
+ cfg_kwargs.update(
156
+ cfg_text_past_key_values=cfg_text_kv,
157
+ cfg_text_packed_position_ids=cfg_text_input['packed_position_ids'],
158
+ cfg_text_packed_query_indexes=cfg_text_input['packed_indexes'],
159
+ cfg_text_key_values_lens=cfg_text_input['key_values_lens'],
160
+ cfg_text_packed_key_value_indexes=cfg_text_input['packed_key_value_indexes'],
161
+ )
162
+ if cfg_img_scale > 1.0 and cfg_img_kv is not None:
163
+ (
164
+ cfg_img_kv,
165
+ cfg_img_kv_lens,
166
+ cfg_img_ropes,
167
+ cfg_img_start_prefilled,
168
+ ) = _prefill_generation_visual_start(
169
+ model, new_token_ids, device,
170
+ cfg_img_kv, cfg_img_kv_lens, cfg_img_ropes,
171
+ )
172
+ cfg_img_input = model.prepare_pixel_noise(
173
+ curr_kvlens=cfg_img_kv_lens, curr_rope=cfg_img_ropes,
174
+ image_sizes=[image_size],
175
+ visual_start_prefilled=cfg_img_start_prefilled,
176
+ )
177
+ cfg_img_input = {k: v.to(device) if isinstance(v, torch.Tensor) else v
178
+ for k, v in cfg_img_input.items()}
179
+ cfg_kwargs.update(
180
+ cfg_img_past_key_values=cfg_img_kv,
181
+ cfg_img_packed_position_ids=cfg_img_input['packed_position_ids'],
182
+ cfg_img_packed_query_indexes=cfg_img_input['packed_indexes'],
183
+ cfg_img_key_values_lens=cfg_img_input['key_values_lens'],
184
+ cfg_img_packed_key_value_indexes=cfg_img_input['packed_key_value_indexes'],
185
+ )
186
+
187
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
188
+ unpacked_pixels = model.generate_image(
189
+ past_key_values=kv,
190
+ num_timesteps=num_timesteps,
191
+ timestep_shift=timestep_shift,
192
+ sampler=sampler,
193
+ cfg_text_scale=cfg_text_scale,
194
+ cfg_img_scale=cfg_img_scale,
195
+ cfg_interval=cfg_interval,
196
+ cfg_renorm_min=0.0,
197
+ cfg_renorm_type=cfg_renorm_type,
198
+ **cfg_kwargs,
199
+ **generation_input,
200
+ )
201
+
202
+ H, W = image_size
203
+ ps = model.config.patch_size
204
+ h_patches, w_patches = H // ps, W // ps
205
+ pixels = unpacked_pixels[0]
206
+ img = pixels.float().cpu()
207
+ img = img.reshape(h_patches, w_patches, ps, ps, 3)
208
+ img = img.permute(0, 2, 1, 3, 4).reshape(H, W, 3)
209
+ img = ((img.clamp(-1, 1) + 1) / 2 * 255).to(torch.uint8).numpy()
210
+ return Image.fromarray(img)
211
+
212
+
213
+ def _generate_t2v_one(
214
+ model, tokenizer, new_token_ids, prompt, video_size, device,
215
+ num_timesteps, timestep_shift, sampler,
216
+ cfg_text_scale, cfg_img_scale, cfg_interval, cfg_renorm_type,
217
+ video_fps=16,
218
+ negative_prompt="",
219
+ ):
220
+ from modeling.pixelumm.qwen3_navit import NaiveCache
221
+
222
+ n_layers = model.config.llm_config.num_hidden_layers
223
+ kv = NaiveCache(n_layers)
224
+ kv_lens, ropes = [0], [0]
225
+
226
+ cfg_text_kv = deepcopy(kv) if cfg_text_scale > 1.0 else None
227
+ cfg_text_kv_lens = list(kv_lens) if cfg_text_scale > 1.0 else None
228
+ cfg_text_ropes = list(ropes) if cfg_text_scale > 1.0 else None
229
+ cfg_img_kv = deepcopy(kv) if cfg_img_scale > 1.0 else None
230
+ cfg_img_kv_lens = list(kv_lens) if cfg_img_scale > 1.0 else None
231
+ cfg_img_ropes = list(ropes) if cfg_img_scale > 1.0 else None
232
+
233
+ kv, kv_lens, ropes = _prefill_text_to_visual_chatml_context(
234
+ model, tokenizer, new_token_ids, prompt, device, kv, kv_lens, ropes,
235
+ )
236
+
237
+ kv, kv_lens, ropes, visual_start_prefilled = _prefill_generation_visual_start(
238
+ model, new_token_ids, device, kv, kv_lens, ropes,
239
+ )
240
+
241
+ generation_input = model.prepare_pixel_video_noise(
242
+ curr_kvlens=kv_lens, curr_rope=ropes,
243
+ video_sizes=[video_size],
244
+ fps=video_fps,
245
+ visual_start_prefilled=visual_start_prefilled,
246
+ )
247
+ generation_input = {k: v.to(device) if isinstance(v, torch.Tensor) else v
248
+ for k, v in generation_input.items()}
249
+ # Preserve the full-precision RNG sample. Model forward remains under BF16
250
+ # autocast, while every sampler starts from an FP32 numerical state.
251
+ generation_input['packed_init_noises'] = generation_input['packed_init_noises'].float()
252
+
253
+ cfg_kwargs = {}
254
+ if cfg_text_scale > 1.0 and cfg_text_kv is not None:
255
+ # Empty preserves the training-dropout shell. An explicit negative
256
+ # prompt reproduces the published video's CFG reference context.
257
+ cfg_text_kv, cfg_text_kv_lens, cfg_text_ropes = _prefill_text_to_visual_chatml_context(
258
+ model, tokenizer, new_token_ids, negative_prompt, device,
259
+ cfg_text_kv, cfg_text_kv_lens, cfg_text_ropes,
260
+ )
261
+ (
262
+ cfg_text_kv,
263
+ cfg_text_kv_lens,
264
+ cfg_text_ropes,
265
+ cfg_text_start_prefilled,
266
+ ) = _prefill_generation_visual_start(
267
+ model, new_token_ids, device,
268
+ cfg_text_kv, cfg_text_kv_lens, cfg_text_ropes,
269
+ )
270
+ cfg_text_input = model.prepare_pixel_video_noise(
271
+ curr_kvlens=cfg_text_kv_lens, curr_rope=cfg_text_ropes,
272
+ video_sizes=[video_size],
273
+ fps=video_fps,
274
+ visual_start_prefilled=cfg_text_start_prefilled,
275
+ )
276
+ cfg_text_input = {k: v.to(device) if isinstance(v, torch.Tensor) else v
277
+ for k, v in cfg_text_input.items()}
278
+ cfg_kwargs.update(
279
+ cfg_text_past_key_values=cfg_text_kv,
280
+ cfg_text_packed_position_ids=cfg_text_input['packed_position_ids'],
281
+ cfg_text_packed_query_indexes=cfg_text_input['packed_indexes'],
282
+ cfg_text_key_values_lens=cfg_text_input['key_values_lens'],
283
+ cfg_text_packed_key_value_indexes=cfg_text_input['packed_key_value_indexes'],
284
+ )
285
+ if cfg_img_scale > 1.0 and cfg_img_kv is not None:
286
+ (
287
+ cfg_img_kv,
288
+ cfg_img_kv_lens,
289
+ cfg_img_ropes,
290
+ cfg_img_start_prefilled,
291
+ ) = _prefill_generation_visual_start(
292
+ model, new_token_ids, device,
293
+ cfg_img_kv, cfg_img_kv_lens, cfg_img_ropes,
294
+ )
295
+ cfg_img_input = model.prepare_pixel_video_noise(
296
+ curr_kvlens=cfg_img_kv_lens, curr_rope=cfg_img_ropes,
297
+ video_sizes=[video_size],
298
+ fps=video_fps,
299
+ visual_start_prefilled=cfg_img_start_prefilled,
300
+ )
301
+ cfg_img_input = {k: v.to(device) if isinstance(v, torch.Tensor) else v
302
+ for k, v in cfg_img_input.items()}
303
+ cfg_kwargs.update(
304
+ cfg_img_past_key_values=cfg_img_kv,
305
+ cfg_img_packed_position_ids=cfg_img_input['packed_position_ids'],
306
+ cfg_img_packed_query_indexes=cfg_img_input['packed_indexes'],
307
+ cfg_img_key_values_lens=cfg_img_input['key_values_lens'],
308
+ cfg_img_packed_key_value_indexes=cfg_img_input['packed_key_value_indexes'],
309
+ )
310
+
311
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
312
+ unpacked_video = model.generate_video(
313
+ past_key_values=kv,
314
+ num_timesteps=num_timesteps,
315
+ timestep_shift=timestep_shift,
316
+ sampler=sampler,
317
+ cfg_text_scale=cfg_text_scale,
318
+ cfg_img_scale=cfg_img_scale,
319
+ cfg_interval=cfg_interval,
320
+ cfg_renorm_min=0.0,
321
+ cfg_renorm_type=cfg_renorm_type,
322
+ **cfg_kwargs,
323
+ **generation_input,
324
+ )
325
+
326
+ num_frames, H, W = video_size
327
+ return _video_tubes_to_pil_frames(
328
+ unpacked_video[0],
329
+ (H, W),
330
+ num_frames,
331
+ model.config.patch_size,
332
+ )
333
+
334
+
335
+ def generate_vlm_interleaved(
336
+ model,
337
+ tokenizer,
338
+ new_token_ids,
339
+ images,
340
+ instruction,
341
+ device,
342
+ max_length,
343
+ temperature=0.0,
344
+ fixed_length=False,
345
+ image_placeholder_policy="strict",
346
+ reasoning_mode="standard_bare",
347
+ exact_max_new_tokens=False,
348
+ return_generation_metadata=False,
349
+ ):
350
+ """Generate an I2T answer with train-aligned interleaved media prefill.
351
+
352
+ Training represents every image as four consecutive attention segments:
353
+ causal user text, causal ``<vision_start>``, a full-attention patch block,
354
+ then causal ``<vision_end>``. This helper applies the same sequence to one
355
+ or more images and preserves ``<image>`` / ``<image N>`` locations in the
356
+ benchmark question. When a question has no placeholders, all images are
357
+ placed before the question, matching ordinary F14 I2T samples.
358
+
359
+ ``fixed_length=True`` is reserved for in-training FSDP eval, where every
360
+ rank must execute the same number of decode iterations. Standalone
361
+ benchmark replicas can stop at EOS and should leave it false.
362
+
363
+ ``reasoning_mode="standard_bare"`` preserves the train-aligned bare
364
+ assistant prefix. Source responses may begin directly, with an empty
365
+ shell, or with a filled trace. ``exact_max_new_tokens=True`` makes the standalone benchmark's
366
+ output budget match Hugging Face/lmms-eval semantics without changing the
367
+ fixed-iteration contract used by in-training FSDP eval. The defaults
368
+ preserve all existing callers.
369
+ """
370
+ from modeling.pixelumm.qwen3_navit import NaiveCache
371
+
372
+ images = list(images)
373
+ if not images:
374
+ raise ValueError("VLM generation requires at least one image")
375
+
376
+ if image_placeholder_policy not in {"strict", "numbered_references"}:
377
+ raise ValueError(
378
+ f"Unsupported image placeholder policy: {image_placeholder_policy}"
379
+ )
380
+ if reasoning_mode != "standard_bare":
381
+ raise ValueError(f"Unsupported VLM reasoning mode: {reasoning_mode}")
382
+
383
+ placeholder = re.compile(r"<image(?:\s+(\d+))?>", flags=re.IGNORECASE)
384
+ matches = list(placeholder.finditer(instruction))
385
+
386
+ parts = []
387
+ if matches:
388
+ numbered = [match.group(1) is not None for match in matches]
389
+ if any(numbered) and not all(numbered):
390
+ raise ValueError(
391
+ "VLM prompts cannot mix <image> and numbered <image N> placeholders"
392
+ )
393
+
394
+ if image_placeholder_policy == "numbered_references" and not all(numbered):
395
+ raise ValueError(
396
+ "numbered_references requires numbered <image N> placeholders"
397
+ )
398
+
399
+ if image_placeholder_policy == "numbered_references":
400
+ labels = [int(match.group(1)) for match in matches]
401
+ unique_labels = sorted(set(labels))
402
+ if len(images) == 1 and len(unique_labels) > 1:
403
+ # Some benchmarks provide one pre-composed panel while their
404
+ # question uses <image N> as textual sub-panel references.
405
+ normalized = placeholder.sub(
406
+ lambda match: f"Panel {int(match.group(1))}",
407
+ instruction,
408
+ )
409
+ parts.extend((
410
+ ("image", 0),
411
+ ("text", normalized),
412
+ ))
413
+ else:
414
+ expected_labels = list(range(1, len(images) + 1))
415
+ if unique_labels != expected_labels:
416
+ raise ValueError(
417
+ "Numbered image references must cover each benchmark visual: "
418
+ f"labels={unique_labels} expected={expected_labels}"
419
+ )
420
+ cursor = 0
421
+ seen_labels = set()
422
+ for label, match in zip(labels, matches):
423
+ parts.append(("text", instruction[cursor : match.start()]))
424
+ if label not in seen_labels:
425
+ parts.append(("image", label - 1))
426
+ seen_labels.add(label)
427
+ else:
428
+ parts.append(("text", f"Picture {label}"))
429
+ cursor = match.end()
430
+ parts.append(("text", instruction[cursor:]))
431
+ else:
432
+ if len(matches) != len(images):
433
+ raise ValueError(
434
+ "Image placeholder count must match the number of benchmark visuals: "
435
+ f"placeholders={len(matches)} images={len(images)}"
436
+ )
437
+ image_indices = (
438
+ [int(match.group(1)) - 1 for match in matches]
439
+ if all(numbered)
440
+ else list(range(len(matches)))
441
+ )
442
+ if sorted(image_indices) != list(range(len(images))):
443
+ raise ValueError(
444
+ "Numbered image placeholders must reference every visual exactly once: "
445
+ f"indices={image_indices} images={len(images)}"
446
+ )
447
+ cursor = 0
448
+ for image_index, match in zip(image_indices, matches):
449
+ parts.append(("text", instruction[cursor : match.start()]))
450
+ parts.append(("image", image_index))
451
+ cursor = match.end()
452
+ parts.append(("text", instruction[cursor:]))
453
+ else:
454
+ parts.extend(("image", image_index) for image_index in range(len(images)))
455
+ parts.append(("text", instruction))
456
+
457
+ n_layers = model.config.llm_config.num_hidden_layers
458
+ kv = NaiveCache(n_layers)
459
+ kv_lens, ropes = [0], [0]
460
+
461
+ def to_device(inputs):
462
+ return {
463
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
464
+ for key, value in inputs.items()
465
+ }
466
+
467
+ def update_text_ids(token_ids):
468
+ nonlocal kv, kv_lens, ropes
469
+ if not token_ids:
470
+ return
471
+ text_input, kv_lens, ropes = model._prepare_token_ids_as_text_cache_input(
472
+ kv_lens,
473
+ ropes,
474
+ [list(token_ids)],
475
+ )
476
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
477
+ kv = model.forward_cache_update_text(kv, **to_device(text_input))
478
+
479
+ prefix_input, kv_lens, ropes = model.prepare_image_to_text_chatml_prefixes(
480
+ curr_kvlens=kv_lens,
481
+ curr_rope=ropes,
482
+ tokenizer=tokenizer,
483
+ )
484
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
485
+ kv = model.forward_cache_update_text(kv, **to_device(prefix_input))
486
+
487
+ if not parts or parts[-1][0] != "text":
488
+ raise AssertionError("Interleaved VLM prompt must end in a text segment")
489
+ trailing_instruction = parts[-1][1]
490
+
491
+ for part_type, value in parts[:-1]:
492
+ if part_type == "text":
493
+ update_text_ids(model._encode_prompt_text(tokenizer, value))
494
+ continue
495
+
496
+ image_index = int(value)
497
+ if len(images) > 1:
498
+ update_text_ids(
499
+ model._encode_prompt_text(tokenizer, f"Picture {image_index + 1}: ")
500
+ )
501
+
502
+ start_input, kv_lens, ropes = model.prepare_visual_delimiter_tokens(
503
+ kv_lens,
504
+ ropes,
505
+ new_token_ids,
506
+ delimiter="start",
507
+ )
508
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
509
+ kv = model.forward_cache_update_text(kv, **to_device(start_input))
510
+
511
+ image_input, kv_lens, ropes = model.prepare_pixel_images(
512
+ curr_kvlens=kv_lens,
513
+ curr_rope=ropes,
514
+ images=[images[image_index]],
515
+ )
516
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
517
+ kv = model.forward_cache_update_pixel(kv, **to_device(image_input))
518
+
519
+ end_input, kv_lens, ropes = model.prepare_visual_delimiter_tokens(
520
+ kv_lens,
521
+ ropes,
522
+ new_token_ids,
523
+ delimiter="end",
524
+ )
525
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
526
+ kv = model.forward_cache_update_text(kv, **to_device(end_input))
527
+
528
+ # Tokenize the trailing question text and ChatML turn boundary together,
529
+ # in one encode call to preserve the boundary before <|im_end|>.
530
+ suffix_ids = model._image_to_text_chatml_suffix_ids(
531
+ tokenizer,
532
+ trailing_instruction,
533
+ )
534
+ assistant_prefill = ""
535
+ if not suffix_ids:
536
+ raise ValueError("Image-to-text ChatML suffix cannot be empty")
537
+ update_text_ids(suffix_ids[:-1])
538
+ start_token_ids = [suffix_ids[-1]]
539
+ start_input = to_device(
540
+ model.prepare_text_start_tokens(kv_lens, ropes, start_token_ids)
541
+ )
542
+
543
+ # ``generate_text`` counts ``start_token_ids`` in its returned length.
544
+ # Add one iteration when exact_max_new_tokens is requested; the start
545
+ # token is removed below.
546
+ decode_max_length = int(max_length) + (1 if exact_max_new_tokens else 0)
547
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
548
+ token_ids, generation_metadata = model.generate_text(
549
+ past_key_values=kv,
550
+ max_length=decode_max_length,
551
+ do_sample=(temperature > 0.0),
552
+ temperature=max(temperature, 1e-5),
553
+ end_token_id=None if fixed_length else new_token_ids["eos_token_id"],
554
+ return_generation_metadata=True,
555
+ **start_input,
556
+ )
557
+
558
+ token_ids = token_ids.squeeze(-1).cpu().tolist()
559
+ eos_id = new_token_ids["eos_token_id"]
560
+ bos_id = new_token_ids["bos_token_id"]
561
+ text_ids = []
562
+ hit_eos = bool(generation_metadata["hit_end_token"])
563
+ for token_id in token_ids:
564
+ if token_id == eos_id:
565
+ break
566
+ text_ids.append(token_id)
567
+ if text_ids and text_ids[0] == start_token_ids[0]:
568
+ text_ids = text_ids[1:]
569
+ if text_ids and text_ids[0] == bos_id:
570
+ text_ids = text_ids[1:]
571
+ text = tokenizer.decode(text_ids, skip_special_tokens=True)
572
+ if not return_generation_metadata:
573
+ return text
574
+ return text, {
575
+ "assistant_prefill": assistant_prefill,
576
+ "generated_tokens": len(text_ids),
577
+ "hit_eos": hit_eos,
578
+ "max_new_tokens": int(max_length),
579
+ "reasoning_mode": reasoning_mode,
580
+ }
581
+
582
+
583
+ def _v2t_frames_to_und_tubes(model, frames, timestamps, *, timestamp_mode="tube_start"):
584
+ """Group processed PIL frames into the released video-UND tube contract."""
585
+ import numpy as np
586
+
587
+ temporal_patch_size = int(model.config.pixel_video_temporal_patch_size)
588
+ mrope_type = str(model.config.llm_config.qwen3_mrope_type)
589
+ if mrope_type != "sensenova":
590
+ raise ValueError(f"PixelUMM V2T requires qwen3_mrope_type='sensenova', got {mrope_type!r}")
591
+ if timestamp_mode != "tube_start":
592
+ raise ValueError(f"PixelUMM V2T requires timestamp_mode='tube_start', got {timestamp_mode!r}")
593
+ if temporal_patch_size <= 0:
594
+ raise ValueError(
595
+ f"pixel_video_temporal_patch_size must be positive, got {temporal_patch_size}"
596
+ )
597
+ if not frames:
598
+ raise ValueError("V2T tube eval requires at least one processed frame")
599
+
600
+ timeline_origin = float(timestamps[0])
601
+ local_timestamps = [float(value) - timeline_origin for value in timestamps]
602
+ pad = (-len(frames)) % temporal_patch_size
603
+ if pad:
604
+ frames = list(frames) + [frames[-1]] * pad
605
+ local_timestamps = local_timestamps + [local_timestamps[-1]] * pad
606
+
607
+ def frame_to_tensor(frame):
608
+ array = np.asarray(frame.convert("RGB"), dtype=np.float32)
609
+ tensor = torch.from_numpy(array).permute(2, 0, 1)
610
+ return tensor / 255.0 * 2.0 - 1.0
611
+
612
+ tubes = []
613
+ for start in range(0, len(frames), temporal_patch_size):
614
+ tube_frames = frames[start : start + temporal_patch_size]
615
+ tube_start_seconds = float(local_timestamps[start])
616
+ tube_tensor = torch.stack([frame_to_tensor(frame) for frame in tube_frames], dim=0)
617
+ tubes.append(
618
+ {
619
+ "tensor": tube_tensor,
620
+ "timestamp_seconds": tube_start_seconds,
621
+ }
622
+ )
623
+ return tubes
624
+
625
+
626
+ def _prefill_v2t_und_tubes(
627
+ model,
628
+ tokenizer,
629
+ new_token_ids,
630
+ kv,
631
+ kv_lens,
632
+ ropes,
633
+ frames,
634
+ timestamps,
635
+ device,
636
+ *,
637
+ timestamp_mode="tube_start",
638
+ ):
639
+ """Prefill the model-aligned tube V2T media context, split-by-split.
640
+
641
+ Training packs, per tube: a causal ``<t.t seconds>`` text span, a causal
642
+ ``<vision_start>``, one full-attention tube, then a causal ``<vision_end>``.
643
+ """
644
+
645
+ def to_device(prepared):
646
+ return {
647
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
648
+ for key, value in prepared.items()
649
+ }
650
+
651
+ tubes = _v2t_frames_to_und_tubes(
652
+ model, frames, timestamps, timestamp_mode=timestamp_mode
653
+ )
654
+ for tube in tubes:
655
+ text_input, kv_lens, ropes = model.prepare_text_spans_for_cache(
656
+ kv_lens,
657
+ ropes,
658
+ [f"<{tube['timestamp_seconds']:.1f} seconds>"],
659
+ tokenizer,
660
+ )
661
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
662
+ kv = model.forward_cache_update_text(kv, **to_device(text_input))
663
+
664
+ start_input, kv_lens, ropes = model.prepare_visual_delimiter_tokens(
665
+ kv_lens, ropes, new_token_ids, delimiter="start"
666
+ )
667
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
668
+ kv = model.forward_cache_update_text(kv, **to_device(start_input))
669
+
670
+ tube_input, kv_lens, ropes = model.prepare_pixel_video_und_tubes_for_text(
671
+ kv_lens,
672
+ ropes,
673
+ [tube["tensor"]],
674
+ )
675
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
676
+ kv = model.forward_cache_update_pixel(kv, **to_device(tube_input))
677
+
678
+ end_input, kv_lens, ropes = model.prepare_visual_delimiter_tokens(
679
+ kv_lens, ropes, new_token_ids, delimiter="end"
680
+ )
681
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
682
+ kv = model.forward_cache_update_text(kv, **to_device(end_input))
683
+
684
+ return kv, kv_lens, ropes
685
+
686
+
687
+ def _prefill_v2t_und_frames(
688
+ model,
689
+ tokenizer,
690
+ new_token_ids,
691
+ kv,
692
+ kv_lens,
693
+ ropes,
694
+ frames,
695
+ timestamps,
696
+ device,
697
+ ):
698
+ """Prefill sparse long-video frames through the image UND embedder.
699
+
700
+ This is the released Video4 sparse-frame contract: each sampled frame gets
701
+ its own lexical clip-relative
702
+ timestamp and visual island. No temporal tube projection or physical-time
703
+ MRoPE is used.
704
+ """
705
+
706
+ def to_device(prepared):
707
+ return {
708
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
709
+ for key, value in prepared.items()
710
+ }
711
+
712
+ timeline_origin = float(timestamps[0])
713
+ local_timestamps = [float(value) - timeline_origin for value in timestamps]
714
+ for frame, timestamp_seconds in zip(frames, local_timestamps, strict=True):
715
+ text_input, kv_lens, ropes = model.prepare_text_spans_for_cache(
716
+ kv_lens,
717
+ ropes,
718
+ [f"<{timestamp_seconds:.1f} seconds>"],
719
+ tokenizer,
720
+ )
721
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
722
+ kv = model.forward_cache_update_text(kv, **to_device(text_input))
723
+
724
+ start_input, kv_lens, ropes = model.prepare_visual_delimiter_tokens(
725
+ kv_lens, ropes, new_token_ids, delimiter="start"
726
+ )
727
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
728
+ kv = model.forward_cache_update_text(kv, **to_device(start_input))
729
+
730
+ image_input, kv_lens, ropes = model.prepare_pixel_images(
731
+ curr_kvlens=kv_lens,
732
+ curr_rope=ropes,
733
+ images=[frame],
734
+ )
735
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
736
+ kv = model.forward_cache_update_pixel(kv, **to_device(image_input))
737
+
738
+ end_input, kv_lens, ropes = model.prepare_visual_delimiter_tokens(
739
+ kv_lens, ropes, new_token_ids, delimiter="end"
740
+ )
741
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
742
+ kv = model.forward_cache_update_text(kv, **to_device(end_input))
743
+
744
+ return kv, kv_lens, ropes
745
+
746
+
747
+ def _generate_v2t_one(
748
+ model,
749
+ tokenizer,
750
+ new_token_ids,
751
+ frames,
752
+ timestamps,
753
+ instruction,
754
+ device,
755
+ max_length,
756
+ temperature,
757
+ *,
758
+ temporal_patch_size=None,
759
+ timestamp_mode="tube_start",
760
+ ):
761
+ """Decode one V2T video context (frame-as-image or video-UND tubes)."""
762
+ from modeling.pixelumm.qwen3_navit import NaiveCache
763
+
764
+ if len(frames) != len(timestamps):
765
+ raise ValueError(
766
+ f"V2T frame/timestamp length mismatch: {len(frames)} != {len(timestamps)}"
767
+ )
768
+ n_layers = model.config.llm_config.num_hidden_layers
769
+ kv = NaiveCache(n_layers)
770
+ kv_lens, ropes = [0], [0]
771
+
772
+ prefix_input, kv_lens, ropes = model.prepare_image_to_text_chatml_prefixes(
773
+ curr_kvlens=kv_lens,
774
+ curr_rope=ropes,
775
+ tokenizer=tokenizer,
776
+ )
777
+ prefix_input = {
778
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
779
+ for key, value in prefix_input.items()
780
+ }
781
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
782
+ kv = model.forward_cache_update_text(kv, **prefix_input)
783
+
784
+ actual_temporal_patch_size = int(
785
+ temporal_patch_size
786
+ if temporal_patch_size is not None
787
+ else model.config.pixel_video_temporal_patch_size
788
+ )
789
+ if actual_temporal_patch_size == 1:
790
+ kv, kv_lens, ropes = _prefill_v2t_und_frames(
791
+ model, tokenizer, new_token_ids, kv, kv_lens, ropes,
792
+ frames, timestamps, device,
793
+ )
794
+ elif actual_temporal_patch_size == int(model.config.pixel_video_temporal_patch_size):
795
+ kv, kv_lens, ropes = _prefill_v2t_und_tubes(
796
+ model, tokenizer, new_token_ids, kv, kv_lens, ropes,
797
+ frames, timestamps, device, timestamp_mode=timestamp_mode,
798
+ )
799
+ else:
800
+ raise ValueError(
801
+ "Unsupported V2T temporal patch override: "
802
+ f"{actual_temporal_patch_size}; expected 1 or "
803
+ f"{model.config.pixel_video_temporal_patch_size}"
804
+ )
805
+
806
+ return _decode_v2t_suffix(
807
+ model,
808
+ tokenizer,
809
+ new_token_ids,
810
+ kv,
811
+ kv_lens,
812
+ ropes,
813
+ instruction,
814
+ device,
815
+ max_length,
816
+ temperature,
817
+ )
818
+
819
+
820
+ def _decode_v2t_suffix(
821
+ model,
822
+ tokenizer,
823
+ new_token_ids,
824
+ kv,
825
+ kv_lens,
826
+ ropes,
827
+ instruction,
828
+ device,
829
+ max_length,
830
+ temperature,
831
+ ):
832
+ """Shared V2T ChatML suffix prefill and answer decoding."""
833
+ suffix_input, kv_lens, ropes, start_token_ids = (
834
+ model.prepare_image_to_text_chatml_suffix_contexts_for_decode(
835
+ curr_kvlens=kv_lens,
836
+ curr_rope=ropes,
837
+ instructions=[instruction],
838
+ tokenizer=tokenizer,
839
+ )
840
+ )
841
+ suffix_input = {
842
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
843
+ for key, value in suffix_input.items()
844
+ }
845
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
846
+ kv = model.forward_cache_update_text(kv, **suffix_input)
847
+
848
+ start_input = model.prepare_text_start_tokens(kv_lens, ropes, start_token_ids)
849
+ start_input = {
850
+ key: value.to(device) if isinstance(value, torch.Tensor) else value
851
+ for key, value in start_input.items()
852
+ }
853
+ with torch.amp.autocast("cuda", dtype=torch.bfloat16):
854
+ token_ids = model.generate_text(
855
+ past_key_values=kv,
856
+ max_length=max_length,
857
+ do_sample=(temperature > 0.0),
858
+ temperature=max(temperature, 1e-5),
859
+ end_token_id=None,
860
+ **start_input,
861
+ )
862
+
863
+ token_ids = token_ids.squeeze(-1).cpu().tolist()
864
+ eos_id = new_token_ids["eos_token_id"]
865
+ bos_id = new_token_ids["bos_token_id"]
866
+ text_ids = []
867
+ for token_id in token_ids:
868
+ if token_id == eos_id:
869
+ break
870
+ text_ids.append(token_id)
871
+ if text_ids and text_ids[0] == start_token_ids[0]:
872
+ text_ids = text_ids[1:]
873
+ if text_ids and text_ids[0] == bos_id:
874
+ text_ids = text_ids[1:]
875
+ return tokenizer.decode(text_ids, skip_special_tokens=True)
train/model_factory.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Construct a PixelUMM model from the selected architecture profile."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from transformers import AutoTokenizer
9
+ from transformers.modeling_utils import no_init_weights
10
+
11
+ from data.data_utils import add_special_tokens
12
+ from modeling.pixelumm.pixelumm import PixelUMM, PixelUMMConfig
13
+ from modeling.pixelumm.qwen3_navit import Qwen3ForCausalLM
14
+ from modeling.qwen3.configuration_qwen3 import Qwen3Config
15
+ from train.config import (
16
+ DataArguments,
17
+ ModelArguments,
18
+ TrainingArguments,
19
+ configure_qwen3_mrope_config,
20
+ )
21
+
22
+
23
+ def _count_parameters(module) -> int:
24
+ return sum(parameter.numel() for parameter in module.parameters())
25
+
26
+
27
+ def build_pixelumm_model(
28
+ model_args: ModelArguments,
29
+ data_args: DataArguments,
30
+ training_args: TrainingArguments,
31
+ logger,
32
+ ):
33
+ """Build the selected topology and tokenizer without loading checkpoint weights."""
34
+ if model_args.llm_backend != "qwen3":
35
+ raise ValueError(
36
+ "PixelUMM-release supports only the R07 Qwen3 backend; "
37
+ f"got {model_args.llm_backend!r}"
38
+ )
39
+
40
+ llm_config = Qwen3Config.from_pretrained(model_args.llm_path)
41
+ llm_config.layer_module = model_args.layer_module
42
+ llm_config.qk_norm = model_args.llm_qk_norm
43
+ llm_config.tie_word_embeddings = model_args.tie_word_embeddings
44
+ llm_config.freeze_und = training_args.freeze_und
45
+ llm_config.qwen_mlp_sequence_chunk_size = int(
46
+ model_args.qwen_mlp_sequence_chunk_size
47
+ )
48
+ llm_config.inference_attention_backend = model_args.inference_attention_backend
49
+ llm_config.conditional_branch_gradient_contract = (
50
+ training_args.conditional_branch_gradient_contract
51
+ )
52
+
53
+ configure_qwen3_mrope_config(llm_config, model_args)
54
+ llm_config.rope_theta_hw = model_args.sensenova_vision_rope_theta
55
+ llm_config.max_position_embeddings_hw = (
56
+ model_args.sensenova_vision_max_position_embeddings
57
+ )
58
+
59
+ with no_init_weights():
60
+ language_model = Qwen3ForCausalLM(llm_config)
61
+ removed = language_model.remove_unused_sensenova_base_qk_norms()
62
+ logger.info(
63
+ "Removed %d unused inherited Q/K norm tensors.",
64
+ removed,
65
+ )
66
+
67
+ config = PixelUMMConfig(
68
+ visual_gen=training_args.visual_gen,
69
+ visual_und=training_args.visual_und,
70
+ llm_config=llm_config,
71
+ llm_backend=model_args.llm_backend,
72
+ gen_space=training_args.gen_space,
73
+ und_space=training_args.und_space,
74
+ patch_size=model_args.pixel_token_patch_size,
75
+ pixel_embedder_type=model_args.pixel_embedder_type,
76
+ pixel_image_additive_pos_embed_type=model_args.pixel_image_additive_pos_embed_type,
77
+ pixel_video_additive_pos_embed_type=model_args.pixel_video_additive_pos_embed_type,
78
+ add_timestep_embedding=model_args.add_timestep_embedding,
79
+ add_noise_scale_embedding=model_args.add_noise_scale_embedding,
80
+ noise_scale=model_args.noise_scale,
81
+ noise_scale_mode=model_args.noise_scale_mode,
82
+ enable_pixel_video=model_args.enable_pixel_video,
83
+ pixel_video_embedder_type=model_args.pixel_video_embedder_type,
84
+ pixel_separate_und_gen_embedder=model_args.pixel_separate_und_gen_embedder,
85
+ pixel_video_separate_und_gen_embedder=model_args.pixel_video_separate_und_gen_embedder,
86
+ pixel_video_temporal_patch_size=model_args.pixel_video_temporal_patch_size,
87
+ noise_time_logit_mean=training_args.noise_time_logit_mean,
88
+ noise_time_logit_std=training_args.noise_time_logit_std,
89
+ time_schedule=training_args.time_schedule,
90
+ prediction_type=training_args.prediction_type,
91
+ loss_type=training_args.loss_type,
92
+ x_pred_t_min=training_args.x_pred_t_min,
93
+ pixel_head_type=model_args.pixel_head_type,
94
+ pixel_video_head_type=model_args.pixel_video_head_type,
95
+ ce_loss_checkpoint_chunk_size=model_args.ce_loss_checkpoint_chunk_size,
96
+ mse_loss_reduction=training_args.mse_loss_reduction,
97
+ packed_attention_impl=model_args.packed_attention_impl,
98
+ packed_expert_routing=model_args.packed_expert_routing,
99
+ packed_sequence_layout=data_args.packed_sequence_layout,
100
+ conditional_branch_gradient_contract=(
101
+ training_args.conditional_branch_gradient_contract
102
+ ),
103
+ )
104
+ model = PixelUMM(language_model, config)
105
+
106
+ tokenizer = AutoTokenizer.from_pretrained(model_args.llm_path)
107
+ tokenizer, new_token_ids, num_new_tokens = add_special_tokens(tokenizer)
108
+ current_vocab_size = model.language_model.model.embed_tokens.num_embeddings
109
+ if num_new_tokens > 0 or current_vocab_size != len(tokenizer):
110
+ logger.info(
111
+ "Resizing token embeddings: %d -> %d", current_vocab_size, len(tokenizer)
112
+ )
113
+ model.language_model.resize_token_embeddings(len(tokenizer))
114
+ model.config.llm_config.vocab_size = len(tokenizer)
115
+ model.language_model.config.vocab_size = len(tokenizer)
116
+
117
+ total_param_count = _count_parameters(model)
118
+ lm_param_count = _count_parameters(model.language_model)
119
+ logger.info(
120
+ "Model parameter count: %.2fB (LM-only: %.2fB)",
121
+ total_param_count / 1e9,
122
+ lm_param_count / 1e9,
123
+ )
124
+ return model, tokenizer, new_token_ids
train/release_checkpoint.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Load complete PixelUMM checkpoints using a model configuration."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import os
10
+ from dataclasses import dataclass
11
+ from datetime import timedelta
12
+ from pathlib import Path
13
+ from typing import Sequence
14
+
15
+ import torch
16
+ import torch.distributed as dist
17
+ from safetensors import safe_open
18
+ from safetensors.torch import load_file
19
+
20
+ from train.config import DataArguments, ModelArguments, TrainingArguments, load_release_config
21
+ from train.model_factory import build_pixelumm_model
22
+ from train.checkpoint_export import EXPORT_MANIFEST, validate_export_manifest
23
+
24
+
25
+ ROOT = Path(__file__).resolve().parents[1]
26
+ DEFAULT_RELEASE_CONFIG = ROOT / "experiments" / "s8_f22_r07" / "release.yaml"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ReleasedModel:
31
+ model: torch.nn.Module
32
+ tokenizer: object
33
+ new_token_ids: dict
34
+ model_args: ModelArguments
35
+ data_args: DataArguments
36
+ training_args: TrainingArguments
37
+ checkpoint_path: Path
38
+ config_path: Path
39
+
40
+
41
+ def dtype_from_name(name: str) -> torch.dtype:
42
+ try:
43
+ return {
44
+ "bf16": torch.bfloat16,
45
+ "bfloat16": torch.bfloat16,
46
+ "fp16": torch.float16,
47
+ "float16": torch.float16,
48
+ "half": torch.float16,
49
+ "fp32": torch.float32,
50
+ "float32": torch.float32,
51
+ "float": torch.float32,
52
+ }[str(name).lower()]
53
+ except KeyError as exc:
54
+ raise ValueError(f"Unsupported PixelUMM dtype: {name}") from exc
55
+
56
+
57
+ def parse_release_arguments(
58
+ config_path: str | os.PathLike[str] = DEFAULT_RELEASE_CONFIG,
59
+ *,
60
+ extra_args: Sequence[str] = (),
61
+ ) -> tuple[ModelArguments, DataArguments, TrainingArguments]:
62
+ """Parse the checked-in release YAML without rebuilding a training CLI."""
63
+
64
+ config_path = Path(config_path).expanduser().resolve()
65
+ extra_args = tuple(map(str, extra_args))
66
+ if extra_args:
67
+ if len(extra_args) != 2 or extra_args[0] != "--llm_path":
68
+ raise ValueError(
69
+ "Released config accepts only the explicit --llm_path override"
70
+ )
71
+ llm_path = extra_args[1]
72
+ else:
73
+ llm_path = None
74
+ model_args, data_args, training_args = load_release_config(
75
+ config_path,
76
+ llm_path=llm_path,
77
+ )
78
+ _validate_release_contract(model_args, data_args, training_args)
79
+ return model_args, data_args, training_args
80
+
81
+
82
+ def _validate_release_contract(model_args, data_args, training_args) -> None:
83
+ checks = {
84
+ "llm_backend=qwen3": model_args.llm_backend == "qwen3",
85
+ "mrope=sensenova": model_args.qwen3_mrope_type == "sensenova",
86
+ "image_patch=16": int(model_args.pixel_token_patch_size) == 16,
87
+ "video_enabled": bool(model_args.enable_pixel_video),
88
+ "video_und_embedder=boolean": isinstance(model_args.pixel_video_separate_und_gen_embedder, bool),
89
+ "video_temporal_patch=4": int(model_args.pixel_video_temporal_patch_size) == 4,
90
+ "image_embedder=image_raw_patch_linear": model_args.pixel_embedder_type
91
+ == "image_raw_patch_linear",
92
+ "video_embedder=video_raw_tube_linear": model_args.pixel_video_embedder_type
93
+ == "video_raw_tube_linear",
94
+ "inference_attention=flash_attn_varlen": model_args.inference_attention_backend
95
+ == "flash_attn_varlen",
96
+ "layout=pixelumm_mot": data_args.packed_sequence_layout == "pixelumm_mot",
97
+ "visual_gen": bool(training_args.visual_gen),
98
+ "visual_und": bool(training_args.visual_und),
99
+ "gen_space=pixel": training_args.gen_space == "pixel",
100
+ "und_space=pixel": training_args.und_space == "pixel",
101
+ "prediction=x": training_args.prediction_type == "x",
102
+ "loss=v_loss": training_args.loss_type == "v_loss",
103
+ }
104
+ failed = [name for name, passed in checks.items() if not passed]
105
+ if failed:
106
+ raise RuntimeError(
107
+ "Released checkpoint config is not a supported PixelUMM contract: "
108
+ + ", ".join(failed)
109
+ )
110
+
111
+
112
+ def validate_released_checkpoint(checkpoint_path: str | os.PathLike[str]) -> Path:
113
+ checkpoint_path = Path(checkpoint_path).expanduser().resolve()
114
+ if not checkpoint_path.is_dir():
115
+ raise FileNotFoundError(f"Released checkpoint directory is missing: {checkpoint_path}")
116
+ export_manifest = checkpoint_path / EXPORT_MANIFEST
117
+ exported_files = validate_export_manifest(checkpoint_path) if export_manifest.is_file() else None
118
+ if not (checkpoint_path / "__SAVE_COMPLETE").is_file() and exported_files is None:
119
+ raise FileNotFoundError(
120
+ f"Released checkpoint lacks __SAVE_COMPLETE or verified {EXPORT_MANIFEST}: {checkpoint_path}"
121
+ )
122
+ if (checkpoint_path / "model" / ".metadata").is_file():
123
+ required_files = _validate_dcp_model(checkpoint_path)
124
+ if exported_files is not None and exported_files != required_files:
125
+ raise RuntimeError("Export manifest must cover exactly DCP metadata and all referenced shards")
126
+ if exported_files is not None and (checkpoint_path / "model.safetensors").exists():
127
+ raise RuntimeError("DCP export must not contain an unverified model.safetensors")
128
+ else:
129
+ safetensors_path = checkpoint_path / "model.safetensors"
130
+ if not safetensors_path.is_file() or safetensors_path.stat().st_size <= 0:
131
+ raise FileNotFoundError(
132
+ "Released checkpoint has neither complete DCP model/.metadata nor "
133
+ f"a non-empty model.safetensors: {checkpoint_path}"
134
+ )
135
+ return checkpoint_path
136
+
137
+
138
+ def _validate_dcp_model(checkpoint_path: Path) -> set[str]:
139
+ """Fail closed on missing or truncated DCP model shards."""
140
+
141
+ from torch.distributed.checkpoint import FileSystemReader
142
+
143
+ model_dir = checkpoint_path / "model"
144
+ metadata_path = model_dir / ".metadata"
145
+ metadata = FileSystemReader(str(model_dir)).read_metadata()
146
+ if not metadata.storage_data:
147
+ raise RuntimeError(f"Released checkpoint DCP metadata is empty: {metadata_path}")
148
+
149
+ required_sizes: dict[str, int] = {}
150
+ for storage_info in metadata.storage_data.values():
151
+ relative_path = str(storage_info.relative_path)
152
+ required_sizes[relative_path] = max(
153
+ required_sizes.get(relative_path, 0),
154
+ int(storage_info.offset) + int(storage_info.length),
155
+ )
156
+ missing = []
157
+ truncated = []
158
+ for relative_path, required_size in sorted(required_sizes.items()):
159
+ shard = model_dir / relative_path
160
+ if shard.resolve().parent != model_dir.resolve() or not relative_path.endswith('.distcp'):
161
+ raise RuntimeError(f"Invalid DCP shard path: {relative_path}")
162
+ if not shard.is_file():
163
+ missing.append(relative_path)
164
+ elif shard.stat().st_size < required_size:
165
+ truncated.append((relative_path, shard.stat().st_size, required_size))
166
+ if missing or truncated:
167
+ raise RuntimeError(
168
+ f"Incomplete released DCP model at {checkpoint_path}: "
169
+ f"missing={missing[:8]} truncated={truncated[:8]}"
170
+ )
171
+ return {"model/.metadata", *(f"model/{name}" for name in required_sizes)}
172
+
173
+
174
+ def validate_checkpoint_model_schema(
175
+ checkpoint_path: str | os.PathLike[str], model: torch.nn.Module
176
+ ) -> None:
177
+ """Compare stored keys/shapes, including DCP keys a load plan would ignore.
178
+
179
+ A model on the meta device is sufficient: this check reads only checkpoint
180
+ metadata, without allocating model weights or touching a CUDA device.
181
+ """
182
+ checkpoint_path = Path(checkpoint_path)
183
+ safetensors_path = checkpoint_path / "model.safetensors"
184
+ if safetensors_path.is_file():
185
+ with safe_open(str(safetensors_path), framework="pt", device="cpu") as reader:
186
+ shapes = {key: tuple(reader.get_slice(key).get_shape()) for key in reader.keys()}
187
+ else:
188
+ from torch.distributed.checkpoint import FileSystemReader
189
+ from torch.distributed.checkpoint.metadata import TensorStorageMetadata
190
+
191
+ metadata = FileSystemReader(str(checkpoint_path / "model")).read_metadata()
192
+ non_tensors = [
193
+ key for key, value in metadata.state_dict_metadata.items()
194
+ if not isinstance(value, TensorStorageMetadata)
195
+ ]
196
+ if non_tensors:
197
+ raise RuntimeError(f"Non-tensor entries in released model: {non_tensors[:8]}")
198
+ shapes = {key: tuple(value.size) for key, value in metadata.state_dict_metadata.items()}
199
+
200
+ state = model.state_dict()
201
+ missing = sorted(set(state) - set(shapes))
202
+ unexpected = sorted(set(shapes) - set(state))
203
+ mismatched = [
204
+ (key, tuple(state[key].shape), shapes[key])
205
+ for key in sorted(set(state) & set(shapes))
206
+ if tuple(state[key].shape) != shapes[key]
207
+ ]
208
+ if missing or unexpected or mismatched:
209
+ raise RuntimeError(
210
+ f"Checkpoint/model schema mismatch at {checkpoint_path}: "
211
+ f"missing={missing[:8]} unexpected={unexpected[:8]} "
212
+ f"shape_mismatches(model, checkpoint)={mismatched[:8]}"
213
+ )
214
+
215
+
216
+ def load_checkpoint_weights(
217
+ checkpoint_path: str | os.PathLike[str],
218
+ model: torch.nn.Module,
219
+ *,
220
+ logger: logging.Logger | None = None,
221
+ ) -> torch.nn.Module:
222
+ """Strictly load only learned model tensors from a released checkpoint."""
223
+
224
+ checkpoint_path = validate_released_checkpoint(checkpoint_path)
225
+ validate_checkpoint_model_schema(checkpoint_path, model)
226
+ logger = logger or logging.getLogger("pixelumm.release")
227
+ safetensors_path = checkpoint_path / "model.safetensors"
228
+ if safetensors_path.is_file():
229
+ logger.info("Loading released safetensors weights from %s", safetensors_path)
230
+ state_dict = load_file(str(safetensors_path), device="cpu")
231
+ else:
232
+ from torch.distributed.checkpoint import FileSystemReader, load
233
+
234
+ model_dir = checkpoint_path / "model"
235
+ logger.info("Loading released DCP weights from %s", model_dir)
236
+ state_dict = model.state_dict()
237
+ load(state_dict, storage_reader=FileSystemReader(str(model_dir)))
238
+ incompatible = model.load_state_dict(state_dict, strict=True)
239
+ if incompatible.missing_keys or incompatible.unexpected_keys:
240
+ raise RuntimeError(
241
+ "Strict released checkpoint load returned incompatible keys: "
242
+ f"missing={incompatible.missing_keys[:8]} "
243
+ f"unexpected={incompatible.unexpected_keys[:8]}"
244
+ )
245
+ return model
246
+
247
+
248
+ def initialize_single_process(device: str | torch.device = "cuda") -> torch.device:
249
+ """Initialize the world-size-one process group required by DCP loading."""
250
+
251
+ device = torch.device(device)
252
+ if device.type == "cuda":
253
+ if not torch.cuda.is_available():
254
+ raise RuntimeError("Released PixelUMM inference requires a CUDA GPU")
255
+ index = 0 if device.index is None else int(device.index)
256
+ torch.cuda.set_device(index)
257
+ device = torch.device("cuda", index)
258
+ if not dist.is_initialized():
259
+ os.environ.setdefault("RANK", "0")
260
+ os.environ.setdefault("WORLD_SIZE", "1")
261
+ os.environ.setdefault("LOCAL_RANK", "0")
262
+ os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
263
+ os.environ.setdefault("MASTER_PORT", "29501")
264
+ dist.init_process_group("gloo", timeout=timedelta(seconds=1800))
265
+ if dist.get_world_size() != 1:
266
+ raise RuntimeError(
267
+ "release inference.py is a one-process entrypoint; use the benchmark "
268
+ "launchers for distributed evaluation"
269
+ )
270
+ return device
271
+
272
+
273
+ def load_released_model(
274
+ checkpoint_path: str | os.PathLike[str],
275
+ *,
276
+ config_path: str | os.PathLike[str] = DEFAULT_RELEASE_CONFIG,
277
+ device: str | torch.device = "cuda",
278
+ dtype: str = "bf16",
279
+ llm_path: str | None = None,
280
+ logger: logging.Logger | None = None,
281
+ ) -> ReleasedModel:
282
+ """Load a released DCP/safetensors checkpoint without training lineage state."""
283
+
284
+ checkpoint_path = validate_released_checkpoint(checkpoint_path)
285
+ config_path = Path(config_path).expanduser().resolve()
286
+ extra_args = [] if llm_path is None else ["--llm_path", str(llm_path)]
287
+ model_args, data_args, training_args = parse_release_arguments(
288
+ config_path, extra_args=extra_args
289
+ )
290
+ device = initialize_single_process(device)
291
+ parameter_dtype = dtype_from_name(dtype)
292
+ logger = logger or logging.getLogger("pixelumm.release")
293
+ if not logger.handlers:
294
+ logger.addHandler(logging.StreamHandler())
295
+ logger.setLevel(logging.INFO)
296
+
297
+ # The released checkpoint owns every learned tensor. The Qwen path is a
298
+ # config/tokenizer dependency only; loading its five weight shards here
299
+ # would waste tens of GB and then be immediately overwritten by DCP.
300
+ previous_default_dtype = torch.get_default_dtype()
301
+ try:
302
+ torch.set_default_dtype(parameter_dtype)
303
+ model, tokenizer, new_token_ids = build_pixelumm_model(
304
+ model_args,
305
+ data_args,
306
+ training_args,
307
+ logger,
308
+ )
309
+ finally:
310
+ torch.set_default_dtype(previous_default_dtype)
311
+ model = load_checkpoint_weights(checkpoint_path, model, logger=logger)
312
+ model = model.to(device=device, dtype=parameter_dtype).eval()
313
+ for parameter in model.parameters():
314
+ parameter.requires_grad = False
315
+ return ReleasedModel(
316
+ model=model,
317
+ tokenizer=tokenizer,
318
+ new_token_ids=new_token_ids,
319
+ model_args=model_args,
320
+ data_args=data_args,
321
+ training_args=training_args,
322
+ checkpoint_path=checkpoint_path,
323
+ config_path=config_path,
324
+ )
train/scientific_config.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Strict loader for immutable PixelUMM scientific YAML configurations.
5
+
6
+ The release launcher owns runtime state such as checkpoint paths, output
7
+ directories, topology, and tracking identity. This module deliberately loads
8
+ only model/data/training science knobs into the typed release configuration.
9
+ Keeping that boundary explicit prevents a portable
10
+ scientific config from silently capturing one cluster's filesystem state.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import yaml
20
+
21
+
22
+ SCHEMA = "pixelumm_scientific_config_v1"
23
+ SECTIONS = ("model", "data", "training")
24
+
25
+
26
+ class _UniqueKeyLoader(yaml.SafeLoader):
27
+ """Safe YAML loader that rejects duplicate mapping keys."""
28
+
29
+
30
+ def _construct_unique_mapping(
31
+ loader: _UniqueKeyLoader,
32
+ node: yaml.MappingNode,
33
+ deep: bool = False,
34
+ ) -> dict[Any, Any]:
35
+ # PyYAML normally keeps the last duplicate key. A reviewed scientific
36
+ # contract must reject that ambiguity instead of silently changing it.
37
+ loader.flatten_mapping(node)
38
+ mapping: dict[Any, Any] = {}
39
+ for key_node, value_node in node.value:
40
+ key = loader.construct_object(key_node, deep=deep)
41
+ try:
42
+ duplicated = key in mapping
43
+ except TypeError as exc:
44
+ raise yaml.constructor.ConstructorError(
45
+ "while constructing a mapping",
46
+ node.start_mark,
47
+ "found an unhashable mapping key",
48
+ key_node.start_mark,
49
+ ) from exc
50
+ if duplicated:
51
+ raise yaml.constructor.ConstructorError(
52
+ "while constructing a mapping",
53
+ node.start_mark,
54
+ f"found duplicate key {key!r}",
55
+ key_node.start_mark,
56
+ )
57
+ mapping[key] = loader.construct_object(value_node, deep=deep)
58
+ return mapping
59
+
60
+
61
+ _UniqueKeyLoader.add_constructor(
62
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
63
+ _construct_unique_mapping,
64
+ )
65
+
66
+
67
+ def sha256_file(path: Path) -> str:
68
+ digest = hashlib.sha256()
69
+ with path.open("rb") as stream:
70
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
71
+ digest.update(chunk)
72
+ return digest.hexdigest()
73
+
74
+
75
+ def _cli_scalar(value: Any, *, location: str) -> str:
76
+ if isinstance(value, bool):
77
+ return "True" if value else "False"
78
+ if value is None:
79
+ return "None"
80
+ if isinstance(value, (str, int, float)):
81
+ return str(value)
82
+ raise RuntimeError(
83
+ f"scientific config value must be a scalar at {location}; "
84
+ f"got {type(value).__name__}"
85
+ )
86
+
87
+
88
+ def load_scientific_config(
89
+ path: Path,
90
+ *,
91
+ expected_sha256: str | None = None,
92
+ ) -> dict[str, dict[str, Any]]:
93
+ """Load one fail-closed, sectioned scientific configuration."""
94
+
95
+ path = Path(path).resolve()
96
+ if not path.is_file() or path.stat().st_size == 0:
97
+ raise RuntimeError(f"scientific config is absent or empty: {path}")
98
+ if expected_sha256 is not None and sha256_file(path) != expected_sha256:
99
+ raise RuntimeError(f"scientific config identity mismatch: {path}")
100
+ try:
101
+ payload = yaml.load(
102
+ path.read_text(encoding="utf-8"),
103
+ Loader=_UniqueKeyLoader,
104
+ )
105
+ except (OSError, yaml.YAMLError) as exc:
106
+ raise RuntimeError(f"scientific config is invalid: {path}") from exc
107
+ if not isinstance(payload, dict):
108
+ raise RuntimeError("scientific config must be a YAML mapping")
109
+ expected_top_level = {"schema", *SECTIONS}
110
+ if set(payload) != expected_top_level:
111
+ raise RuntimeError(
112
+ "scientific config top-level keys changed: "
113
+ f"expected={sorted(expected_top_level)} actual={sorted(payload)}"
114
+ )
115
+ if payload["schema"] != SCHEMA:
116
+ raise RuntimeError(
117
+ f"unexpected scientific config schema: {payload['schema']!r}"
118
+ )
119
+
120
+ sections: dict[str, dict[str, Any]] = {}
121
+ owners: dict[str, str] = {}
122
+ for section in SECTIONS:
123
+ values = payload[section]
124
+ if not isinstance(values, dict) or not values:
125
+ raise RuntimeError(
126
+ f"scientific config section {section!r} must be a non-empty mapping"
127
+ )
128
+ normalized: dict[str, Any] = {}
129
+ for raw_name, value in values.items():
130
+ if not isinstance(raw_name, str) or not raw_name:
131
+ raise RuntimeError(
132
+ f"scientific config contains an invalid key in {section!r}"
133
+ )
134
+ if raw_name.startswith("--") or not raw_name.replace("_", "a").isalnum():
135
+ raise RuntimeError(
136
+ f"scientific config key must be a dataclass field name: {raw_name!r}"
137
+ )
138
+ if raw_name in owners:
139
+ raise RuntimeError(
140
+ f"scientific config key {raw_name!r} appears in both "
141
+ f"{owners[raw_name]!r} and {section!r}"
142
+ )
143
+ _cli_scalar(value, location=f"{section}.{raw_name}")
144
+ owners[raw_name] = section
145
+ normalized[raw_name] = value
146
+ sections[section] = normalized
147
+ return sections