Skip to content

Commit 7fd7a96

Browse files
justinchubyCopilot
andcommitted
feat(models): add Cosmos3-Edge vision-language model (3-model split)
Extend the cosmos3_edge support from a text-only reasoner to the full vision-language model (nvidia/Cosmos3-Edge, Cosmos3EdgeForConditionalGeneration) as a LLaVA-style 3-model onnxruntime-genai split (decoder + vision_encoder + embedding): - decoder: squared-ReLU GQA text reasoner taking inputs_embeds, built with 3D multimodal RoPE (mrope_section=[24,20,20]). - vision_encoder: SigLIP vision tower + new Cosmos3EdgeMultiModalProjector (pre-shuffle LayerNorm -> 2x2 pixel-shuffle -> linear_fc1 -> GELU -> linear_fc2). - embedding: token embedding + image-feature fusion at image_token_id=19. preprocess_weights routes the single HF checkpoint to the three sub-models: model.visual.* / model.projector.* -> vision (SigLIP mlp.fc1/fc2 -> up_proj/down_proj), embed_tokens -> embedding, the top-level text tower -> decoder (self_attn.to_{q,k,v,out} -> {q,k,v,o}_proj), and drops the generator-tower k_norm_und_for_gen key-norm. Built via a new Cosmos3EdgeVLTask ("cosmos3-edge-vl"). A vision config hook reconstructs image_size from num_patches and pulls the projector's merger_intermediate_size from projector_config. The decoder-only text reasoner remains available as cosmos3_edge_text. L1 graph-build tested only: NVIDIA publishes no modeling code for cosmos3_edge (not in transformers, no remote-code module), so exact pixel-shuffle ordering and numerical parity are unverifiable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
1 parent 390852b commit 7fd7a96

13 files changed

Lines changed: 543 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
### NVIDIA Cosmos 3 Edge text reasoner (`cosmos3_edge`)
10+
### NVIDIA Cosmos 3 Edge vision-language model (`cosmos3_edge`)
1111

1212
#### Added
1313

14-
- Support for the **text reasoner backbone** of the `cosmos3_edge`
15-
vision-language checkpoint (`nvidia/Cosmos3-Edge`). The language tower is a
16-
grouped-query-attention decoder with a **non-gated squared-ReLU FFN**
17-
(`hidden_act="relu2"`, `up_proj → relu2 → down_proj`) and 3D multimodal RoPE
18-
(`mrope_section=[24, 20, 20]`, equivalent to 1D RoPE for text-only). Mapped
19-
onto the standard `CausalLMModel` backbone + `FCMLP`; `preprocess_weights`
20-
renames the `self_attn.to_{q,k,v,out}` projections, nests the top-level text
21-
tower under `model.`, and drops the vision encoder, projector, and the
22-
generator-tower `k_norm_und_for_gen` key-norm. Registered as `cosmos3_edge`
23-
/ `cosmos3_edge_text`; L1 graph-build tested. The `cosmos3_omni` variants
24-
(`Cosmos3-Nano`/`-Super`) are two-tower diffusion world models and remain out
25-
of scope for this decoder-only path.
14+
- Support for the **full `cosmos3_edge` vision-language model**
15+
(`nvidia/Cosmos3-Edge`, `Cosmos3EdgeForConditionalGeneration`) as a 3-model
16+
onnxruntime-genai split (`decoder` + `vision_encoder` + `embedding`):
17+
- **decoder**: grouped-query-attention text reasoner with a **non-gated
18+
squared-ReLU FFN** (`hidden_act="relu2"`, `up_proj → relu2 → down_proj`)
19+
and 3D multimodal RoPE (`mrope_section=[24, 20, 20]`); takes
20+
`inputs_embeds`.
21+
- **vision_encoder**: SigLIP vision tower + a new
22+
`Cosmos3EdgeMultiModalProjector` (pre-shuffle `LayerNorm` → 2×2
23+
pixel-shuffle → `linear_fc1` → GELU → `linear_fc2`).
24+
- **embedding**: token embedding + image-feature fusion at
25+
`image_token_id=19`.
26+
`preprocess_weights` routes the single HF checkpoint to the three
27+
sub-models: `model.visual.*` / `model.projector.*` → vision (with SigLIP
28+
`mlp.fc1/fc2``up_proj/down_proj`), `embed_tokens` → embedding, the
29+
top-level text tower (`layers.*` / `norm` / `lm_head`) → decoder (renaming
30+
`self_attn.to_{q,k,v,out}``{q,k,v,o}_proj`), and drops the
31+
generator-tower `k_norm_und_for_gen` key-norm. Built via a new
32+
`Cosmos3EdgeVLTask` (`cosmos3-edge-vl`). The decoder-only text reasoner
33+
remains available as `cosmos3_edge_text`.
34+
- **L1 graph-build tested only.** NVIDIA does not publish modeling code for
35+
`cosmos3_edge` (not in `transformers`, no remote-code module), so the exact
36+
pixel-shuffle ordering and numerical parity are unverifiable; L4/L5 parity
37+
is deferred. The `cosmos3_omni` variants (`Cosmos3-Nano`/`-Super`) are
38+
two-tower diffusion world models tracked separately.
2639

2740
### Text-only export for multimodal Gemma 4 (`--text-only`)
2841

src/mobius/_configs/_sub_configs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ class VisionConfig:
7474
pooling_kernel_size: int | None = None
7575
# MLP activation for vision encoder layers (e.g. "gelu_pytorch_tanh" for Gemma4 SigLIP)
7676
hidden_act: str | None = None
77+
# Cosmos3-Edge pixel-shuffle projector intermediate size
78+
# (HF: projector_config.merger_intermediate_size). ``None`` means the model
79+
# does not use a Cosmos-style merger projector.
80+
projector_intermediate_size: int | None = None
7781
# CLIP-style feature extraction: which ``hidden_states`` index to output
7882
# (HuggingFace convention, e.g. -2 for Phi-3.5-Vision). ``None`` means use
7983
# the final hidden state (all layers + post_layernorm).

src/mobius/_configs/per_model/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
# defaults run separately as an explicit first pass, so `ruff` / `isort`
2424
# may freely re-sort this block.
2525
from mobius._configs.per_model import ( # noqa: F401
26+
_cosmos3_edge_vision,
2627
_gemma4_audio,
2728
_gemma4_unified_audio,
2829
_gemma4_unified_vision,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Vision extractor hook for NVIDIA Cosmos3-Edge.
5+
6+
Cosmos3-Edge (``cosmos3_edge`` / ``Cosmos3EdgeForConditionalGeneration``) pairs
7+
a SigLIP-style vision encoder (``cosmos3_edge_vision``) with a pixel-shuffle
8+
merger projector (``cosmos3_edge_projector``). Two config quirks need bridging
9+
into :class:`~mobius._configs.VisionConfig`:
10+
11+
- The vision config declares ``num_patches`` (256) instead of ``image_size``.
12+
The standard :class:`~mobius.components.PatchEmbedding` derives the patch
13+
count from ``image_size // patch_size``, so we reconstruct
14+
``image_size = sqrt(num_patches) * patch_size`` (16 * 16 = 256).
15+
- The projector's intermediate width lives in a sibling ``projector_config``
16+
(``merger_intermediate_size``), not in ``vision_config``.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import math
22+
23+
from mobius._configs._extractors import register_vision_hook
24+
25+
26+
@register_vision_hook("cosmos3_edge", "cosmos3_edge_vision")
27+
def _cosmos3_edge_vision(config, parent_config, model_type: str, fields: dict):
28+
vision_source = parent_config or config
29+
hf_vision = getattr(vision_source, "vision_config", None) or getattr(
30+
config, "vision_config", None
31+
)
32+
33+
# Reconstruct image_size from num_patches (256 -> 16x16 grid -> 256 px).
34+
if fields.get("image_size") is None and hf_vision is not None:
35+
num_patches = getattr(hf_vision, "num_patches", None)
36+
patch_size = getattr(hf_vision, "patch_size", None) or fields.get("patch_size")
37+
if num_patches is not None and patch_size is not None:
38+
grid = round(math.sqrt(num_patches))
39+
fields["image_size"] = grid * patch_size
40+
41+
# Pixel-shuffle projector intermediate size from projector_config.
42+
projector_cfg = getattr(vision_source, "projector_config", None) or getattr(
43+
config, "projector_config", None
44+
)
45+
if projector_cfg is not None:
46+
get = (
47+
projector_cfg.get
48+
if isinstance(projector_cfg, dict)
49+
else lambda k, d=None: getattr(projector_cfg, k, d)
50+
)
51+
fields["projector_intermediate_size"] = get("merger_intermediate_size")
52+
# out_hidden_size drives the projector output (text hidden size).
53+
if fields.get("out_hidden_size") is None:
54+
fields["out_hidden_size"] = get("out_hidden_size")
55+
merge = get("spatial_merge_size")
56+
if merge is not None:
57+
fields["spatial_merge_size"] = merge
58+
59+
# Cosmos3-Edge places image_token_id at the top level (default 19).
60+
if fields.get("image_token_id") is None:
61+
fields["image_token_id"] = getattr(vision_source, "image_token_id", 19)
62+
return None

src/mobius/_registry.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
CausalLMModel,
3737
ChatGLMCausalLMModel,
3838
Cosmos3EdgeTextModel,
39+
Cosmos3EdgeVLModel,
3940
DeepSeekOCR2CausalLMModel,
4041
DeepSeekV3CausalLMModel,
4142
DeepSeekV4CausalLMModel,
@@ -398,7 +399,7 @@ def _detect_fallback_registration(hf_config) -> ModelRegistration | None:
398399
"codegen": ModelRegistration(CodeGenCausalLMModel),
399400
"cohere": ModelRegistration(CohereCausalLMModel),
400401
"cohere2": ModelRegistration(CohereCausalLMModel),
401-
"cosmos3_edge": ModelRegistration(Cosmos3EdgeTextModel),
402+
"cosmos3_edge": ModelRegistration(Cosmos3EdgeVLModel),
402403
"cosmos3_edge_text": ModelRegistration(Cosmos3EdgeTextModel),
403404
"diffllama": ModelRegistration(DiffLlamaCausalLMModel),
404405
"doge": ModelRegistration(DogeCausalLMModel),

src/mobius/components/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"LoRALinear",
4848
"MLP",
4949
"MLPMultiModalProjector",
50+
"Cosmos3EdgeMultiModalProjector",
5051
"MoELayer",
5152
"OffsetRMSNorm",
5253
"PatchEmbed",
@@ -169,6 +170,9 @@
169170
SparseMixerGate,
170171
TopKGate,
171172
)
173+
from mobius.components._multimodal import (
174+
Cosmos3EdgeMultiModalProjector as Cosmos3EdgeMultiModalProjector,
175+
)
172176
from mobius.components._multimodal import (
173177
Gemma3MultiModalProjector as Gemma3MultiModalProjector,
174178
)

src/mobius/components/_multimodal.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from mobius.components._common import Linear
2525
from mobius.components._rms_norm import RMSNorm
26+
from mobius.components._vision import VisionLayerNorm
2627

2728
if TYPE_CHECKING:
2829
import onnx_ir as ir
@@ -117,6 +118,81 @@ def forward(self, op: OpBuilder, vision_features: ir.Value):
117118
return hidden
118119

119120

121+
class Cosmos3EdgeMultiModalProjector(nn.Module):
122+
"""Cosmos3-Edge pixel-shuffle merger projector.
123+
124+
``LayerNorm → spatial 2x2 pixel-shuffle → Linear(fc1) → GELU → Linear(fc2)``
125+
126+
The SigLIP vision encoder emits a fixed ``grid x grid`` patch grid
127+
(``num_patches`` patches, e.g. 16x16 = 256). ``use_postshuffle_norm=false``
128+
means the ``LayerNorm`` is applied on the raw ``vision_hidden_size`` (1152)
129+
features **before** the spatial merge. The merge concatenates each
130+
``spatial_merge_size x spatial_merge_size`` block of adjacent patches into a
131+
single ``spatial_merge_size**2 * vision_hidden_size`` (4608) vector, which
132+
``linear_fc1`` maps to ``intermediate_size`` (11520) and ``linear_fc2`` maps
133+
to ``text_hidden_size`` (2048).
134+
135+
HF weights (``model.projector.*``):
136+
- ``norm.{weight,bias}`` (pre-shuffle LayerNorm)
137+
- ``linear_fc1.{weight,bias}``
138+
- ``linear_fc2.{weight,bias}``
139+
"""
140+
141+
def __init__(
142+
self,
143+
vision_hidden_size: int,
144+
text_hidden_size: int,
145+
intermediate_size: int,
146+
grid_size: int,
147+
spatial_merge_size: int = 2,
148+
norm_eps: float = 1e-6,
149+
):
150+
super().__init__()
151+
self._grid = grid_size
152+
self._ms = spatial_merge_size
153+
self._vision_hidden = vision_hidden_size
154+
merged_dim = vision_hidden_size * spatial_merge_size * spatial_merge_size
155+
# Pre-shuffle LayerNorm over the raw vision hidden size.
156+
self.norm = VisionLayerNorm(vision_hidden_size, eps=norm_eps)
157+
self.linear_fc1 = Linear(merged_dim, intermediate_size, bias=True)
158+
self.linear_fc2 = Linear(intermediate_size, text_hidden_size, bias=True)
159+
160+
def forward(self, op: OpBuilder, vision_features: ir.Value):
161+
# vision_features: [batch, grid*grid, vision_hidden]
162+
ms = self._ms
163+
g = self._grid
164+
gm = g // ms
165+
d = self._vision_hidden
166+
167+
# Pre-shuffle LayerNorm (use_postshuffle_norm=false).
168+
x = self.norm(op, vision_features)
169+
170+
batch = op.Shape(vision_features, start=0, end=1) # dynamic [1]
171+
172+
# [B, g*g, D] -> [B, g/ms, ms, g/ms, ms, D]
173+
shape_6d = op.Concat(
174+
batch,
175+
op.Constant(value_ints=[gm, ms, gm, ms, d]),
176+
axis=0,
177+
)
178+
x = op.Reshape(x, shape_6d)
179+
# Group hidden dim outermost per merged block (HF F.unfold ordering):
180+
# [B, g/ms, ms, g/ms, ms, D] -> [B, g/ms, g/ms, D, ms, ms]
181+
x = op.Transpose(x, perm=[0, 1, 3, 5, 2, 4])
182+
# Flatten to [B, (g/ms)^2, D*ms*ms]
183+
shape_3d = op.Concat(
184+
batch,
185+
op.Constant(value_ints=[gm * gm, d * ms * ms]),
186+
axis=0,
187+
)
188+
x = op.Reshape(x, shape_3d)
189+
190+
x = self.linear_fc1(op, x)
191+
x = op.Gelu(x)
192+
x = self.linear_fc2(op, x)
193+
return x
194+
195+
120196
class LinearMultiModalProjector(nn.Module):
121197
"""Single linear projection (PaliGemma, Qwen2-Audio).
122198

src/mobius/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"CohereCausalLMModel",
2525
"ControlNetModel",
2626
"Cosmos3EdgeTextModel",
27+
"Cosmos3EdgeVLModel",
2728
"DeepSeekOCR2CausalLMModel",
2829
"DeepSeekV3CausalLMModel",
2930
"DeepSeekV4CausalLMModel",
@@ -165,7 +166,7 @@
165166
from mobius.models.cogvideox import CogVideoXTransformer3DModel
166167
from mobius.models.cohere import CohereCausalLMModel
167168
from mobius.models.controlnet import ControlNetModel
168-
from mobius.models.cosmos import Cosmos3EdgeTextModel
169+
from mobius.models.cosmos import Cosmos3EdgeTextModel, Cosmos3EdgeVLModel
169170
from mobius.models.ctrl import CTRLCausalLMModel
170171
from mobius.models.deepseek import DeepSeekV3CausalLMModel
171172
from mobius.models.deepseek_ocr2 import DeepSeekOCR2CausalLMModel

0 commit comments

Comments
 (0)