Spaces:
Running on Zero
Running on Zero
Initial Space demo
Browse files- .gitattributes +2 -0
- .gitignore +2 -0
- LICENSE +201 -0
- README.md +12 -6
- app.py +431 -0
- data/__init__.py +3 -0
- data/data_utils.py +187 -0
- data/pixelumm_image_resize.py +76 -0
- data/pixelumm_smart_resize.py +55 -0
- data/pixelumm_video_decoder.py +686 -0
- eval/__init__.py +3 -0
- eval/vlm/__init__.py +3 -0
- eval/vlm/release_preprocess_contract.py +44 -0
- examples/cats.jpg +3 -0
- examples/street.jpg +3 -0
- experiments/s8_f22_r07/release.yaml +54 -0
- experiments/s8_f22_r07/t2v_negative_prompt.txt +1 -0
- modeling/__init__.py +7 -0
- modeling/pixelumm/__init__.py +17 -0
- modeling/pixelumm/dpm_solver.py +253 -0
- modeling/pixelumm/flow_unipc.py +69 -0
- modeling/pixelumm/flow_unipc_scheduler.py +249 -0
- modeling/pixelumm/loss_reduction.py +72 -0
- modeling/pixelumm/modeling_utils.py +165 -0
- modeling/pixelumm/pixelumm.py +1043 -0
- modeling/pixelumm/qwen3_navit.py +1056 -0
- modeling/pixelumm/time_schedule.py +57 -0
- modeling/qwen3/__init__.py +6 -0
- modeling/qwen3/configuration_qwen3.py +116 -0
- packages.txt +1 -0
- requirements.txt +12 -0
- train/__init__.py +3 -0
- train/checkpoint_export.py +99 -0
- train/config.py +146 -0
- train/eval_utils.py +875 -0
- train/model_factory.py +124 -0
- train/release_checkpoint.py +324 -0
- train/scientific_config.py +147 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
| 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
|
examples/street.jpg
ADDED
|
|
Git LFS Details
|
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
|