Support DROID policy server for Cosmos3 OpenPI (#4282)

Signed-off-by: yuzhud <yuzhudong@gmail.com>
This commit is contained in:
Yuzhu Dong authored and GitHub committed 2026-06-15 21:22:59 +08:00
1 parent ae937320ff
commit e2e79177db
16 files changed
+1397 -154

No files matched your search

+44
View File
@@ -47,6 +47,10 @@ mode is selected per request:
**`nvidia/Cosmos3-Nano-Policy-DROID`** is served the same way
(`domain_name=droid_lerobot`).
- **DROID OpenPI policy server** — serve `nvidia/Cosmos3-Nano-Policy-DROID` and
connect an OpenPI-compatible websocket client to `/v1/realtime/robot/openpi`.
This path returns action chunks directly instead of an mp4.
Action requests can use `input_reference` or `video_reference` for video input.
`policy` and `forward_dynamics` can also use an image reference; `inverse_dynamics`
requires a video reference.
@@ -248,6 +252,39 @@ VIDEO_ID=$(curl -sS -X POST http://localhost:8000/v1/videos \
# poll until status == completed, then:
curl -sS "http://localhost:8000/v1/videos/$VIDEO_ID" | jq '.action | {shape, dtype, raw_action_dim, domain_id}'
curl -sS -L "http://localhost:8000/v1/videos/$VIDEO_ID/content" -o cosmos3_inverse_dynamics.mp4
# DROID OpenPI policy server (websocket action serving).
# Requires cosmos_framework on PYTHONPATH because the pipeline reuses the
# reference RoboLab action transforms. If your checkpoint config already
# includes policy_server_config, omit the stage_overrides file and flag.
cat > cosmos3_droid_openpi_stage_overrides.json <<'JSON'
{
"0": {
"model_config": {
"policy_server_config": {
"image_resolution": [540, 640],
"n_external_cameras": 2,
"needs_wrist_camera": true,
"needs_stereo_camera": false,
"needs_session_id": true,
"action_space": "joint_position"
}
}
}
}
JSON
vllm serve nvidia/Cosmos3-Nano-Policy-DROID \
--omni \
--host 0.0.0.0 --port 8000 \
--model-class-name Cosmos3OmniDiffusersPipeline \
--no-guardrails \
--stage-overrides "$(cat cosmos3_droid_openpi_stage_overrides.json)"
# Point an OpenPI websocket client at:
# ws://localhost:8000/v1/realtime/robot/openpi
# The first server message is policy_server_config. Each infer request sends a
# msgpack-numpy observation dict and receives a writable float32 action array.
```
#### Notes
@@ -275,6 +312,13 @@ curl -sS -L "http://localhost:8000/v1/videos/$VIDEO_ID/content" -o cosmos3_inver
For V2V, `condition_frame_indexes_vision` selects the clean conditioned latent
frame indexes (default `[0, 1]`), and `condition_video_keep` selects whether the
API decodes the first or last needed reference frames (`"first"` by default).
- **DROID OpenPI observations:** include a string `prompt`, either
`observation/image` or the three-view DROID camera keys
(`observation/wrist_image_left`, `observation/exterior_image_1_left`,
`observation/exterior_image_2_left`), plus `observation/gripper_position` and
`observation/joint_position`. Optional extra params include `history_length`,
`conditioning_fps`, `action_chunk_size`, `raw_action_dim`, `deterministic_seed`,
and `session_id`.
- **Known limitations:**
- Guardrails-on requires `cosmos-guardrail` **and** access to the gated
`nvidia/Cosmos-1.0-Guardrail` repo (accept license + `HF_TOKEN`); otherwise
+1
View File
@@ -27,3 +27,4 @@ prettytable>=3.8.0
aenum==3.1.16
pyzmq>=25.0.0
janus>=1.0.0
msgpack>=1.0.0
@@ -5,6 +5,7 @@ from __future__ import annotations
import sys
import types
from dataclasses import dataclass
from types import SimpleNamespace
from typing import Any
@@ -230,6 +231,8 @@ def test_pipeline_registered_and_exported() -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
from vllm_omni.diffusion.registry import (
_DIFFUSION_ACTION_POST_PROCESS_FUNCS,
_DIFFUSION_IR_OP_PRIORITY_FUNCS,
_DIFFUSION_MODELS,
_DIFFUSION_POST_PROCESS_FUNCS,
_DIFFUSION_PRE_PROCESS_FUNCS,
@@ -245,6 +248,10 @@ def test_pipeline_registered_and_exported() -> None:
)
assert _DIFFUSION_PRE_PROCESS_FUNCS["Cosmos3OmniDiffusersPipeline"] == "get_cosmos3_pre_process_func"
assert _DIFFUSION_POST_PROCESS_FUNCS["Cosmos3OmniDiffusersPipeline"] == "get_cosmos3_post_process_func"
assert (
_DIFFUSION_ACTION_POST_PROCESS_FUNCS["Cosmos3OmniDiffusersPipeline"] == "get_cosmos3_action_post_process_func"
)
assert _DIFFUSION_IR_OP_PRIORITY_FUNCS["Cosmos3OmniDiffusersPipeline"] == "get_cosmos3_ir_op_priority_func"
assert "Cosmos3OmniDiffusersPipeline" in CUSTOM_DIT_ENABLERS
assert "Cosmos3OmniDiffusersPipeline" in cosmos3.__all__
@@ -413,6 +420,73 @@ def test_postprocess_handles_image_video_audio_and_validation() -> None:
func({"image": video, "video": video})
def test_action_postprocess_handles_robolab_policy_outputs() -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import (
RoboLabPolicyInputs,
get_cosmos3_action_post_process_func,
make_robolab_action_postprocess_inputs,
)
func = get_cosmos3_action_post_process_func(SimpleNamespace())
inputs = RoboLabPolicyInputs(
prompt="Pick the cube.",
video_tensor=torch.zeros(1, 3, 3, 16, 16),
action_tensor=torch.zeros(2, 2),
action_condition_indexes=[0],
action_start_frame_offset=1,
raw_action_dim=2,
domain_id=7,
fps=15.0,
height=16,
width=16,
image_size=None,
num_frames=3,
num_inference_steps=4,
guidance_scale=3.0,
flow_shift=5.0,
seed=11,
history_length=1,
action_space="joint_pos",
observation={},
)
action = torch.tensor([[[0.0, 0.25], [1.0, 0.75]]])
custom_output = {"robolab_action_postprocess": make_robolab_action_postprocess_inputs(inputs)}
processed = func(action, custom_output=custom_output)
assert processed.shape == (1, 2)
assert processed.dtype == torch.zeros((), dtype=torch.float32).numpy().dtype
torch.testing.assert_close(torch.from_numpy(processed), torch.tensor([[1.0, 0.25]]))
assert "robolab_action_postprocess" not in custom_output
def test_ir_op_priority_hook_preserves_platform_fields(monkeypatch: pytest.MonkeyPatch) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import get_cosmos3_ir_op_priority_func
@dataclass
class FakeIrOpPriorityConfig:
rms_norm: list[str]
fused_add_rms_norm: list[str]
custom_op: list[str]
fake_kernel = types.ModuleType("vllm.config.kernel")
fake_kernel.IrOpPriorityConfig = FakeIrOpPriorityConfig
monkeypatch.setitem(sys.modules, fake_kernel.__name__, fake_kernel)
func = get_cosmos3_ir_op_priority_func(SimpleNamespace())
default_priority = FakeIrOpPriorityConfig(
rms_norm=["vllm_c", "native"],
fused_add_rms_norm=["vllm_c", "native"],
custom_op=["platform_kernel", "native"],
)
merged = func(default_priority, vllm_config=SimpleNamespace())
assert merged.rms_norm == ["native"]
assert merged.fused_add_rms_norm == ["native"]
assert merged.custom_op == ["platform_kernel", "native"]
def test_prompt_formatting_and_checkpoint_key_remap(make_cosmos3_pipeline) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
@@ -762,6 +836,95 @@ class TestForwardRouting:
)
assert captured["diffuse_calls"][-1]["shared_kwargs"]["action_domain_ids"].tolist() == [7]
assert output.custom_output["action"].shape == (1, 2, 2)
assert "action_only_output" not in output.custom_output
def test_forward_dispatches_robolab_policy_flow(
self,
make_cosmos3_pipeline,
monkeypatch: pytest.MonkeyPatch,
) -> None:
from vllm_omni.diffusion.models.cosmos3 import pipeline_cosmos3
pipeline = make_cosmos3_pipeline()
pipeline.transformer = pipeline.transformer.__class__(latent_channel_size=2, action_gen=True, action_dim=4)
captured = self._install_forward_stubs(pipeline)
video_latents = torch.zeros(1, 2, 1, 1, 1)
velocity_mask = torch.ones(1, 1, 1, 1, 1)
condition_latents = torch.zeros_like(video_latents)
inputs = pipeline_cosmos3.RoboLabPolicyInputs(
prompt="Pick the cube.",
video_tensor=torch.zeros(1, 3, 3, 16, 16),
action_tensor=torch.zeros(2, 2),
action_condition_indexes=[0],
action_start_frame_offset=1,
raw_action_dim=2,
domain_id=7,
fps=15.0,
height=16,
width=16,
image_size=None,
num_frames=3,
num_inference_steps=4,
guidance_scale=3.0,
flow_shift=5.0,
seed=11,
history_length=1,
action_space="joint_pos",
observation={},
)
def fake_prepare_action_latents(**kwargs):
captured["prepare_action"] = kwargs
action_chunk_size = kwargs["action_chunk_size"]
raw_action_dim = int(kwargs["raw_action_dim"])
return (
torch.zeros(1, action_chunk_size, 4),
torch.ones(1, action_chunk_size, 1),
torch.zeros(1, action_chunk_size, 4),
raw_action_dim,
)
def fake_prepare_action_video(*args, **kwargs):
captured["prepare_action_video"] = {"args": args, "kwargs": kwargs}
return video_latents, velocity_mask, condition_latents
monkeypatch.setattr(
pipeline_cosmos3,
"build_robolab_unipc_scheduler",
lambda num_steps, shift, device: StubScheduler(list(range(num_steps, 0, -1)), flow_shift=shift),
)
pipeline._build_robolab_policy_inputs = lambda sp, prompt_data, request_id=None: inputs
pipeline._prepare_action_latents = fake_prepare_action_latents
pipeline._prepare_latents_action_video = fake_prepare_action_video
pipeline._decode_latents = lambda latents: (_ for _ in ()).throw(
AssertionError("RoboLab should not decode video")
)
output = pipeline.forward(SimpleNamespace(prompts=["ignored"], sampling_params=make_sampling_params()))
assert captured["format"] == {
"prompt": "Pick the cube.",
"negative_prompt": "",
"num_frames": 3,
"frame_rate": 15.0,
"height": 16,
"width": 16,
"is_t2i": False,
}
assert "flow_shifts" not in captured
assert pipeline.scheduler.set_timesteps_calls == []
assert captured["prepare_action"]["clean_action"] is inputs.action_tensor
assert captured["prepare_action"]["condition_indexes"] == [0]
assert captured["prepare_action_video"]["kwargs"] == {"image_size": None}
assert captured["diffuse_calls"][-1]["shared_kwargs"]["action_domain_ids"].tolist() == [7]
assert captured["diffuse_calls"][-1]["timesteps"].tolist() == [4, 3, 2, 1]
assert output.output == {}
assert output.custom_output["action_only_output"] is True
assert output.custom_output["action"].shape == (1, 2, 2)
assert "actions" not in output.custom_output
assert "robolab_action_postprocess" in output.custom_output
assert "robolab_policy_inputs" not in output.custom_output
@pytest.mark.parametrize(
("prompt", "sampling_params", "message"),
+33 -1
View File
@@ -10,11 +10,14 @@ This module tests:
- Worker integration: model runner resolved via platform hook
"""
from unittest.mock import patch
from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
from vllm_omni.diffusion.registry import (
_DIFFUSION_ACTION_POST_PROCESS_FUNCS,
_DIFFUSION_IR_OP_PRIORITY_FUNCS,
_DIFFUSION_MODELS,
_DIFFUSION_POST_PROCESS_FUNCS,
_DIFFUSION_PRE_PROCESS_FUNCS,
@@ -59,6 +62,8 @@ class TestRegisterDiffusionModel:
original_models = _DIFFUSION_MODELS.copy()
original_pre = _DIFFUSION_PRE_PROCESS_FUNCS.copy()
original_post = _DIFFUSION_POST_PROCESS_FUNCS.copy()
original_action_post = _DIFFUSION_ACTION_POST_PROCESS_FUNCS.copy()
original_ir_op_priority = _DIFFUSION_IR_OP_PRIORITY_FUNCS.copy()
yield
_DIFFUSION_MODELS.clear()
_DIFFUSION_MODELS.update(original_models)
@@ -66,6 +71,10 @@ class TestRegisterDiffusionModel:
_DIFFUSION_PRE_PROCESS_FUNCS.update(original_pre)
_DIFFUSION_POST_PROCESS_FUNCS.clear()
_DIFFUSION_POST_PROCESS_FUNCS.update(original_post)
_DIFFUSION_ACTION_POST_PROCESS_FUNCS.clear()
_DIFFUSION_ACTION_POST_PROCESS_FUNCS.update(original_action_post)
_DIFFUSION_IR_OP_PRIORITY_FUNCS.clear()
_DIFFUSION_IR_OP_PRIORITY_FUNCS.update(original_ir_op_priority)
def test_register_new_model(self):
"""Test registering a new diffusion model with pre/post process functions."""
@@ -75,6 +84,8 @@ class TestRegisterDiffusionModel:
class_name="TestPipeline",
pre_process_func_name="test_pre_process",
post_process_func_name="test_post_process",
action_post_process_func_name="test_action_post_process",
ir_op_priority_func_name="test_ir_op_priority",
)
assert "TestPipeline" in _DIFFUSION_MODELS
assert _DIFFUSION_MODELS["TestPipeline"] == (
@@ -84,6 +95,8 @@ class TestRegisterDiffusionModel:
)
assert _DIFFUSION_PRE_PROCESS_FUNCS["TestPipeline"] == "test_pre_process"
assert _DIFFUSION_POST_PROCESS_FUNCS["TestPipeline"] == "test_post_process"
assert _DIFFUSION_ACTION_POST_PROCESS_FUNCS["TestPipeline"] == "test_action_post_process"
assert _DIFFUSION_IR_OP_PRIORITY_FUNCS["TestPipeline"] == "test_ir_op_priority"
class TestWorkerUsesHook:
@@ -108,3 +121,22 @@ class TestWorkerUsesHook:
assert worker.model_runner is mock_runner_instance
mock_platform.get_diffusion_model_runner_cls.assert_called_once()
mock_resolve.assert_called_once_with("custom.path")
@patch("vllm_omni.diffusion.worker.diffusion_worker.get_diffusion_ir_op_priority_func")
@patch("vllm_omni.diffusion.worker.diffusion_worker.current_omni_platform")
def test_ir_op_priority_hook_receives_platform_default(self, mock_platform, mock_get_hook):
"""Test model IR priority hook merges from the platform default."""
from vllm_omni.diffusion.worker.diffusion_worker import _resolve_ir_op_priority
od_config = SimpleNamespace(model_class_name="TestPipeline")
vllm_config = SimpleNamespace()
default_priority = object()
merged_priority = object()
hook = Mock(return_value=merged_priority)
mock_platform.get_default_ir_op_priority.return_value = default_priority
mock_get_hook.return_value = hook
assert _resolve_ir_op_priority(od_config, vllm_config) is merged_priority
mock_platform.get_default_ir_op_priority.assert_called_once_with(vllm_config)
mock_get_hook.assert_called_once_with(od_config)
hook.assert_called_once_with(default_priority, vllm_config=vllm_config)
@@ -567,6 +567,78 @@ class TestDiffusionEngine:
torch.tensor([3.0, 4.0]),
)
@pytest.mark.asyncio
async def test_step_empty_dict_output_still_runs_postprocess(self, mocker: MockerFixture) -> None:
engine = DiffusionEngine.__new__(DiffusionEngine)
engine.od_config = SimpleNamespace(
model_class_name="mock_model",
enable_cpu_offload=False,
)
engine.pre_process_func = None
engine.post_process_func = mocker.Mock(return_value={"video": ["processed"]})
engine._post_process_accepts_sampling_params = False
engine._check_and_start_background_loop = mocker.AsyncMock()
engine.async_add_req_and_wait_for_response = mocker.AsyncMock(
return_value=DiffusionOutput(
output={},
custom_output={"actions": torch.tensor([[1.0, 2.0]])},
)
)
request = OmniDiffusionRequest(
prompts=["prompt"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_id="req-action",
)
mocker.patch("vllm_omni.diffusion.diffusion_engine.supports_audio_output", return_value=False)
outputs = await engine.step(request)
engine.post_process_func.assert_called_once_with({})
assert outputs[0].images == ["processed"]
torch.testing.assert_close(outputs[0].multimodal_output["actions"], torch.tensor([[1.0, 2.0]]))
@pytest.mark.asyncio
async def test_step_action_only_flag_skips_postprocess(self, mocker: MockerFixture) -> None:
engine = DiffusionEngine.__new__(DiffusionEngine)
engine.od_config = SimpleNamespace(
model_class_name="mock_model",
enable_cpu_offload=False,
)
engine.pre_process_func = None
engine.post_process_func = mocker.Mock(side_effect=AssertionError("postprocess should be skipped"))
engine.action_post_process_func = mocker.Mock(return_value=torch.tensor([[3.0, 4.0]]))
engine._post_process_accepts_sampling_params = False
engine._action_post_process_accepts_custom_output = True
engine._action_post_process_accepts_sampling_params = False
engine._check_and_start_background_loop = mocker.AsyncMock()
raw_action = torch.tensor([[1.0, 2.0]])
engine.async_add_req_and_wait_for_response = mocker.AsyncMock(
return_value=DiffusionOutput(
output={},
custom_output={
"action": raw_action,
"action_only_output": True,
},
)
)
request = OmniDiffusionRequest(
prompts=["prompt"],
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=1),
request_id="req-action",
)
mocker.patch("vllm_omni.diffusion.diffusion_engine.supports_audio_output", return_value=False)
outputs = await engine.step(request)
engine.post_process_func.assert_not_called()
engine.action_post_process_func.assert_called_once()
assert engine.action_post_process_func.call_args.args[0] is raw_action
assert "custom_output" in engine.action_post_process_func.call_args.kwargs
assert outputs[0].images == []
torch.testing.assert_close(outputs[0].multimodal_output["actions"], torch.tensor([[3.0, 4.0]]))
class TestStepScheduler:
def setup_method(self) -> None:
@@ -1,9 +1,7 @@
import asyncio
import builtins
import sys
import types
from unittest.mock import AsyncMock, MagicMock
import numpy as np
import pytest
from vllm_omni.entrypoints.openpi import connection as openpi_connection
@@ -52,48 +50,56 @@ def _serving_mock():
return serving
def test_pack_reports_clear_error_when_openpi_client_is_missing(monkeypatch):
real_import = builtins.__import__
def test_pack_and_unpack_round_trip_numpy_values():
payload = {
"image": np.arange(6, dtype=np.uint8).reshape(2, 3),
"action": np.asarray([[1.0, 2.0]], dtype=np.float32),
"scalar": np.float32(3.5),
"nested": [{"done": np.bool_(True)}],
}
def import_without_openpi_client(name, globals=None, locals=None, fromlist=(), level=0):
if name == "openpi_client":
raise ModuleNotFoundError("No module named 'openpi_client'", name="openpi_client")
return real_import(name, globals, locals, fromlist, level)
decoded = openpi_connection._unpack(openpi_connection._pack(payload))
monkeypatch.setattr(builtins, "__import__", import_without_openpi_client)
with pytest.raises(ImportError) as exc_info:
openpi_connection._pack({"prompt": "pick up the object"})
message = str(exc_info.value)
assert "/v1/realtime/robot/openpi" in message
assert "pip install openpi-client" in message
np.testing.assert_array_equal(decoded["image"], payload["image"])
np.testing.assert_allclose(decoded["action"], payload["action"])
assert decoded["image"].dtype == np.uint8
assert decoded["action"].dtype == np.float32
assert decoded["scalar"] == np.float32(3.5)
assert decoded["nested"][0]["done"] == np.bool_(True)
def test_pack_and_unpack_delegate_to_openpi_msgpack_numpy(monkeypatch):
calls = []
def test_unpack_accepts_msgpack_numpy_marker_dicts():
action = np.asarray([[1.0, 2.0]], dtype=np.float32)
payload = {
b"actions": {
b"nd": True,
b"type": action.dtype.str,
b"kind": action.dtype.kind,
b"shape": action.shape,
b"data": action.tobytes(),
}
}
class FakeMsgpackNumpy:
@staticmethod
def packb(obj):
calls.append(("packb", obj))
return b"packed"
decoded = openpi_connection._unpack_numpy(payload)
@staticmethod
def unpackb(data):
calls.append(("unpackb", data))
return {"unpacked": data}
np.testing.assert_allclose(decoded[b"actions"], action)
assert decoded[b"actions"].flags.writeable is True
decoded[b"actions"][:, -1] = 0.0
np.testing.assert_allclose(decoded[b"actions"], np.asarray([[1.0, 0.0]], dtype=np.float32))
fake_openpi_client = types.ModuleType("openpi_client")
fake_openpi_client.msgpack_numpy = FakeMsgpackNumpy
monkeypatch.setitem(sys.modules, "openpi_client", fake_openpi_client)
assert openpi_connection._pack({"x": 1}) == b"packed"
assert openpi_connection._unpack(b"payload") == {"unpacked": b"payload"}
assert calls == [
("packb", {"x": 1}),
("unpackb", b"payload"),
]
def test_unpack_leaves_user_dict_without_numpy_kind_marker_unchanged():
payload = {
"metadata": {
"nd": True,
"type": "<f4",
"data": b"user payload",
}
}
decoded = openpi_connection._unpack_numpy(payload)
assert decoded == payload
def test_handle_connection_returns_structured_error_for_invalid_payload(monkeypatch):
@@ -40,7 +40,9 @@ def _json_unpack(data):
def _engine_with_policy_config(policy_config=None):
od_config = SimpleNamespace(model_config={"policy_server_config": policy_config or TEST_POLICY_SERVER_CONFIG})
if policy_config is None:
policy_config = TEST_POLICY_SERVER_CONFIG
od_config = SimpleNamespace(model_config={"policy_server_config": policy_config})
return SimpleNamespace(get_diffusion_od_config=lambda: od_config)
@@ -172,6 +174,15 @@ def test_create_policy_server_returns_none_without_policy_config():
assert serving is None
def test_policy_server_config_allows_explicit_empty_config():
serving = openpi_serving.ServingRealtimeRobotOpenPI(
engine_client=_engine_with_policy_config(policy_config={}),
model_name="nvidia/Cosmos3-Nano-Policy-DROID",
)
assert serving.policy_server_config.to_dict() == {}
def test_policy_server_config_reads_engine_model_config():
policy_config = {"custom_model_key": "custom-value"}
engine_client = SimpleNamespace(model_config=SimpleNamespace(policy_server_config=policy_config))
+37 -6
View File
@@ -27,6 +27,7 @@ from vllm_omni.diffusion.data import (
from vllm_omni.diffusion.executor.abstract import DiffusionExecutor
from vllm_omni.diffusion.registry import (
DiffusionModelRegistry,
get_diffusion_action_post_process_func,
get_diffusion_post_process_func,
get_diffusion_pre_process_func,
)
@@ -84,6 +85,15 @@ def supports_audio_output(model_class_name: str) -> bool:
return bool(getattr(model_cls, "support_audio_output", False))
def _func_accepts_parameter(func: object | None, parameter_name: str) -> bool:
if func is None:
return False
parameters = inspect.signature(func).parameters
return parameter_name in parameters or any(
parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in parameters.values()
)
def _move_tensor_tree_to_cpu(value: object) -> object:
if isinstance(value, torch.Tensor):
return value.cpu() if value.device.type != "cpu" else value
@@ -149,12 +159,16 @@ class DiffusionEngine:
self.od_config = od_config
self.post_process_func = get_diffusion_post_process_func(od_config)
self.action_post_process_func = get_diffusion_action_post_process_func(od_config)
self.pre_process_func = get_diffusion_pre_process_func(od_config)
# Cache whether the model-specific postprocess accepts request-level
# sampling params so step() can support both legacy and extended hooks.
self._post_process_accepts_sampling_params = bool(
self.post_process_func is not None
and "sampling_params" in inspect.signature(self.post_process_func).parameters
self._post_process_accepts_sampling_params = _func_accepts_parameter(self.post_process_func, "sampling_params")
self._action_post_process_accepts_sampling_params = _func_accepts_parameter(
self.action_post_process_func, "sampling_params"
)
self._action_post_process_accepts_custom_output = _func_accepts_parameter(
self.action_post_process_func, "custom_output"
)
executor_class = DiffusionExecutor.get_class(od_config)
@@ -261,8 +275,14 @@ class DiffusionEngine:
if self.od_config.enable_cpu_offload:
output_data = _move_tensor_tree_to_cpu(output_data)
custom_output = output.custom_output or {}
action_payload = None
action_only_output = bool(custom_output.get("action_only_output"))
postprocess_start_time = time.perf_counter()
if self.post_process_func is not None:
if action_only_output:
outputs = []
elif self.post_process_func is not None:
# Some video pipelines need request-level controls during
# postprocess (for example worker-side frame interpolation).
if self._post_process_accepts_sampling_params:
@@ -272,10 +292,8 @@ class DiffusionEngine:
else:
outputs = output_data
audio_payload = None
custom_output = output.custom_output or {}
model_audio_sample_rate = None
model_fps = None
action_payload = None
if isinstance(outputs, dict):
audio_payload = outputs.get("audio")
action_payload = outputs.get("actions")
@@ -283,6 +301,19 @@ class DiffusionEngine:
model_audio_sample_rate = outputs.get("audio_sample_rate")
model_fps = outputs.get("fps")
outputs = outputs.get("video", outputs)
if action_payload is None:
action_payload = custom_output.get("actions")
action_post_process_func = getattr(self, "action_post_process_func", None)
if action_payload is None and action_post_process_func is not None:
raw_action_payload = custom_output.get("action", action_payload)
if raw_action_payload is not None:
action_kwargs: dict[str, Any] = {}
if getattr(self, "_action_post_process_accepts_custom_output", False):
action_kwargs["custom_output"] = custom_output
if getattr(self, "_action_post_process_accepts_sampling_params", False):
action_kwargs["sampling_params"] = request.sampling_params
action_payload = action_post_process_func(raw_action_payload, **action_kwargs)
custom_output["actions"] = action_payload
postprocess_time = time.perf_counter() - postprocess_start_time
logger.debug("Post-processing completed in %.4f seconds", postprocess_time)
@@ -3,6 +3,7 @@
from .pipeline_cosmos3 import (
Cosmos3OmniDiffusersPipeline,
get_cosmos3_action_post_process_func,
get_cosmos3_post_process_func,
get_cosmos3_pre_process_func,
)
@@ -10,6 +11,7 @@ from .transformer_cosmos3 import Cosmos3VFMTransformer
__all__ = [
"Cosmos3OmniDiffusersPipeline",
"get_cosmos3_action_post_process_func",
"get_cosmos3_post_process_func",
"get_cosmos3_pre_process_func",
"Cosmos3VFMTransformer",
@@ -21,6 +21,7 @@ import math
import os
import time
from collections.abc import Iterable
from dataclasses import fields
from typing import Any, ClassVar
import numpy as np
@@ -65,16 +66,45 @@ from .action import (
resolve_domain_id,
)
from .transformer_cosmos3 import Cosmos3VFMTransformer, resolve_sound_gen
from .utils import (
COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION,
COSMOS3_VAE_TEMPORAL_COMPRESSION,
ROBOLAB_CONCAT_VIEW_DESCRIPTION,
ROBOLAB_DEFAULT_ACTION_CHUNK_SIZE,
ROBOLAB_DEFAULT_ACTION_SPACE,
ROBOLAB_DEFAULT_CONDITIONING_FPS,
ROBOLAB_DEFAULT_DOMAIN_NAME,
ROBOLAB_DEFAULT_FLOW_SHIFT,
ROBOLAB_DEFAULT_GUIDANCE_SCALE,
ROBOLAB_DEFAULT_IMAGE_HEIGHT,
ROBOLAB_DEFAULT_IMAGE_WIDTH,
ROBOLAB_DEFAULT_NUM_INFERENCE_STEPS,
ROBOLAB_DEFAULT_RAW_ACTION_DIM,
ROBOLAB_DEFAULT_RESOLUTION,
ROBOLAB_MIDTRAIN_RAW_ACTION_DIM,
RoboLabActionPostprocessInputs,
RoboLabPolicyInputs,
build_abs_pose_from_components,
build_robolab_unipc_scheduler,
condition_pixel_frame_count,
convert_midtrain_rotation,
ensure_2d_float_array,
ensure_gripper_array,
extract_robolab_image,
extract_robolab_prompt_image,
lazy_action_transform_pipeline,
make_robolab_action_postprocess_inputs,
next_robolab_seed,
normalize_condition_frame_indexes_vision,
normalize_condition_video_keep,
normalize_robolab_action_space,
pose_abs_to_rel,
postprocess_robolab_action,
resize_rgb_uint8,
)
logger = init_logger(__name__)
COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION = (0, 1)
COSMOS3_DEFAULT_CONDITION_VIDEO_KEEP = "first"
# Mirrors the WAN VAE's temporal compression. Authoritative value is
# ``self.vae.config.scale_factor_temporal`` at runtime; this constant exists so
# off-line / API code that runs before the pipeline is constructed can compute
# pixel-frame budgets without instantiating the VAE.
COSMOS3_VAE_TEMPORAL_COMPRESSION = 4
COSMOS3_DEFAULT_CONDITION_PIXEL_FRAMES = (
max(COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION) * COSMOS3_VAE_TEMPORAL_COMPRESSION + 1
)
@@ -107,43 +137,6 @@ COSMOS3_T2I_DEFAULT_GUIDANCE_INTERVAL: tuple[float, float] = (400.0, 1000.0)
COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH = 4096
def _normalize_condition_frame_indexes_vision(value: Any) -> tuple[int, ...]:
"""Normalize Cosmos3 vision-conditioning latent frame indexes."""
if value is None:
return COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION
if isinstance(value, str):
value = [item.strip() for item in value.split(",") if item.strip()]
elif isinstance(value, int):
value = [value]
if not isinstance(value, Iterable):
raise TypeError(
"Cosmos3 condition_frame_indexes_vision must be an int, comma-separated string, "
f"or iterable of ints; got {type(value)!r}."
)
indexes = tuple(sorted({int(index) for index in value}))
if not indexes:
raise ValueError("Cosmos3 condition_frame_indexes_vision must contain at least one index.")
if any(index < 0 for index in indexes):
raise ValueError(f"Cosmos3 condition_frame_indexes_vision must be non-negative, got {indexes}.")
return indexes
def _condition_pixel_frame_count(
condition_frame_indexes_vision: Iterable[int],
temporal_compression: int = COSMOS3_VAE_TEMPORAL_COMPRESSION,
) -> int:
return max(condition_frame_indexes_vision) * int(temporal_compression) + 1
def _normalize_condition_video_keep(value: Any) -> str:
keep = str(value or COSMOS3_DEFAULT_CONDITION_VIDEO_KEEP).strip().lower()
if keep not in {"first", "last"}:
raise ValueError("Cosmos3 condition_video_keep must be either 'first' or 'last'.")
return keep
# ---------------------------------------------------------------------------
# Post-process function (registered in registry.py)
# ---------------------------------------------------------------------------
@@ -365,16 +358,16 @@ def get_cosmos3_pre_process_func(od_config: OmniDiffusionConfig):
else:
assert raw_video_frames is not None
extra = _extra_args(request)
condition_frame_indexes_vision = _normalize_condition_frame_indexes_vision(
condition_frame_indexes_vision = normalize_condition_frame_indexes_vision(
extra.get(
"condition_frame_indexes_vision",
prompt.get("condition_frame_indexes_vision"),
)
)
keep = _normalize_condition_video_keep(
keep = normalize_condition_video_keep(
extra.get("condition_video_keep", prompt.get("condition_video_keep"))
)
max_frames = _condition_pixel_frame_count(condition_frame_indexes_vision)
max_frames = condition_pixel_frame_count(condition_frame_indexes_vision)
prompt["additional_information"]["preprocessed_video"] = _preprocess_condition_video(
raw_video_frames,
int(target_h),
@@ -491,6 +484,36 @@ def get_cosmos3_post_process_func(od_config: OmniDiffusionConfig):
return post_process_func
def get_cosmos3_action_post_process_func(od_config: OmniDiffusionConfig):
del od_config
def action_post_process_func(action: Any, custom_output: dict[str, Any] | None = None, sampling_params=None):
del sampling_params
inputs = custom_output.get("robolab_action_postprocess") if isinstance(custom_output, dict) else None
if isinstance(inputs, RoboLabActionPostprocessInputs):
processed_action = postprocess_robolab_action(action, inputs)
custom_output.pop("robolab_action_postprocess", None)
return processed_action
return action
return action_post_process_func
def get_cosmos3_ir_op_priority_func(od_config: OmniDiffusionConfig):
del od_config
def ir_op_priority_func(ir_op_priority, vllm_config=None):
del vllm_config
from vllm.config.kernel import IrOpPriorityConfig
priority_kwargs = {field.name: list(getattr(ir_op_priority, field.name)) for field in fields(ir_op_priority)}
priority_kwargs["rms_norm"] = ["native"]
priority_kwargs["fused_add_rms_norm"] = ["native"]
return IrOpPriorityConfig(**priority_kwargs)
return ir_op_priority_func
# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------
@@ -549,11 +572,11 @@ class Cosmos3OmniDiffusersPipeline(
return ReferenceVideoDecodeSpec(max_frames=max_frames, keep="first")
return ReferenceVideoDecodeSpec(max_frames=None, keep="first")
condition_indexes = _normalize_condition_frame_indexes_vision(extra_args.get("condition_frame_indexes_vision"))
max_frames = _condition_pixel_frame_count(condition_indexes)
condition_indexes = normalize_condition_frame_indexes_vision(extra_args.get("condition_frame_indexes_vision"))
max_frames = condition_pixel_frame_count(condition_indexes)
if num_frames is not None:
max_frames = min(max_frames, int(num_frames))
keep = _normalize_condition_video_keep(extra_args.get("condition_video_keep"))
keep = normalize_condition_video_keep(extra_args.get("condition_video_keep"))
return ReferenceVideoDecodeSpec(max_frames=max_frames, keep=keep)
def __init__(
@@ -654,6 +677,7 @@ class Cosmos3OmniDiffusersPipeline(
self._guidance_scale = None
self._num_timesteps = None
self._robolab_transform = None
# Set True by ``enable_cache_for_cosmos3`` when cache-dit is enabled on
# this pipeline. Tells the sequential-CFG loop to keep paired
@@ -875,6 +899,306 @@ class Cosmos3OmniDiffusersPipeline(
return val
return default
def _get_robolab_transform(self):
if self._robolab_transform is None:
action_dim = int(getattr(self.transformer, "action_dim", 64))
self._robolab_transform = lazy_action_transform_pipeline(action_dim)
return self._robolab_transform
def _build_robolab_policy_inputs(
self,
sp: OmniDiffusionSamplingParams,
prompt_data: Any | None = None,
request_id: str | None = None,
) -> RoboLabPolicyInputs | None:
extra = sp.extra_args if isinstance(sp.extra_args, dict) else {}
obs = extra.get("robot_obs")
if obs is None:
obs = extra.get("observation")
if obs is None:
return None
if not isinstance(obs, dict):
raise TypeError(f"Cosmos3 RoboLab observation must be a dict, got {type(obs)!r}.")
prompt = obs.get("prompt")
if not isinstance(prompt, str):
raise ValueError("RoboLab observation must contain string key 'prompt'.")
def extra_param(key: str, default: Any) -> Any:
value = extra.get(key)
return default if value is None else value
def extra_param_alias(primary_key: str, alias_key: str, default: Any) -> Any:
value = extra.get(primary_key)
if value is not None:
return value
value = extra.get(alias_key)
return default if value is None else value
action_space = normalize_robolab_action_space(extra_param("action_space", ROBOLAB_DEFAULT_ACTION_SPACE))
action_chunk_size = int(extra_param("action_chunk_size", ROBOLAB_DEFAULT_ACTION_CHUNK_SIZE))
raw_action_dim_default = (
ROBOLAB_DEFAULT_RAW_ACTION_DIM if action_space == "joint_pos" else ROBOLAB_MIDTRAIN_RAW_ACTION_DIM
)
raw_action_dim = int(extra_param("raw_action_dim", raw_action_dim_default))
image_h = int(extra_param("image_height", ROBOLAB_DEFAULT_IMAGE_HEIGHT))
image_w = int(extra_param("image_width", ROBOLAB_DEFAULT_IMAGE_WIDTH))
history_length = int(extra_param("history_length", 1))
use_state = self._truthy(extra_param("use_state", True))
resolution = str(extra_param("resolution", ROBOLAB_DEFAULT_RESOLUTION))
fps = float(extra_param("conditioning_fps", ROBOLAB_DEFAULT_CONDITIONING_FPS))
domain_name = str(extra_param("domain_name", ROBOLAB_DEFAULT_DOMAIN_NAME))
domain_id = resolve_domain_id(domain_name=domain_name, require_explicit=True)
if use_state and history_length < 1:
raise ValueError("RoboLab history_length must be >= 1 when use_state is true.")
if action_chunk_size <= 0:
raise ValueError(f"RoboLab action_chunk_size must be positive, got {action_chunk_size}.")
if raw_action_dim <= 0:
raise ValueError(f"RoboLab raw_action_dim must be positive, got {raw_action_dim}.")
try:
image = extract_robolab_image(obs)
except ValueError as exc:
image = extract_robolab_prompt_image(prompt_data)
if image is None:
raise exc
if image.shape[:2] != (image_h, image_w):
image = resize_rgb_uint8(image, (image_h, image_w))
t_frames = action_chunk_size + 1
video = torch.zeros((3, t_frames, image_h, image_w), dtype=torch.uint8)
video[:, 0] = torch.from_numpy(image.copy()).permute(2, 0, 1)
use_state_rows = 1 if use_state else 0
action = torch.zeros((action_chunk_size + use_state_rows, raw_action_dim), dtype=torch.float32)
history_action = None
num_history_rows = history_length - use_state_rows
gripper_position = 1.0 - ensure_gripper_array(obs["observation/gripper_position"])
if action_space == "joint_pos":
joint_position = ensure_2d_float_array(obs["observation/joint_position"], "observation/joint_position", 7)
if use_state:
action[0] = torch.from_numpy(np.concatenate((joint_position[-1], gripper_position[-1])))
if num_history_rows > 0:
if len(joint_position) < num_history_rows + 1:
raise ValueError("Not enough joint_position rows for requested history_length.")
history_np = np.concatenate(
(joint_position[-num_history_rows - 1 : -1], gripper_position[-num_history_rows - 1 : -1]),
axis=-1,
)
history_action = torch.from_numpy(history_np).float()
else:
eef_pos = ensure_2d_float_array(obs["observation/eef_pos"], "observation/eef_pos", 3)
eef_quat = ensure_2d_float_array(obs["observation/eef_quat"], "observation/eef_quat", 4)
if use_state:
rot6d = convert_midtrain_rotation(eef_quat[-1], "quat_xyzw", "rot6d")
action[0] = torch.from_numpy(np.concatenate((eef_pos[-1], rot6d, gripper_position[-1])))
if num_history_rows > 0:
if len(eef_pos) < num_history_rows + 1 or len(eef_quat) < num_history_rows + 1:
raise ValueError("Not enough eef_pos/eef_quat rows for requested history_length.")
poses_abs = build_abs_pose_from_components(eef_pos, eef_quat, "quat_xyzw")
poses_rel = pose_abs_to_rel(poses_abs, rotation_format="rot6d", pose_convention="backward_framewise")
history_np = np.concatenate(
[poses_rel[-num_history_rows:], gripper_position[-num_history_rows:]],
axis=-1,
)
history_action = torch.from_numpy(history_np).float()
sample: dict[str, Any] = {
"ai_caption": prompt,
"video": video,
"action": action,
# Cosmos Framework consumes this as an integer conditioning bucket.
"conditioning_fps": torch.tensor(fps, dtype=torch.long),
"mode": ACTION_MODE_POLICY,
"domain_id": torch.tensor(domain_id, dtype=torch.long),
"viewpoint": "concat_view",
"additional_view_description": ROBOLAB_CONCAT_VIEW_DESCRIPTION,
}
if history_action is not None:
sample["history_action"] = history_action
sample = self._get_robolab_transform()(sample, resolution)
sequence_plan = sample["sequence_plan"]
video_tensor = sample["video"].float() / 127.5 - 1.0
raw_action_dim_tensor = sample.get("raw_action_dim")
if isinstance(raw_action_dim_tensor, torch.Tensor):
transformed_raw_action_dim = int(raw_action_dim_tensor.item())
else:
transformed_raw_action_dim = raw_action_dim
return RoboLabPolicyInputs(
prompt=sample["ai_caption"],
video_tensor=video_tensor.unsqueeze(0),
action_tensor=sample["action"].float(),
action_condition_indexes=list(getattr(sequence_plan, "condition_frame_indexes_action", []) or []),
action_start_frame_offset=int(getattr(sequence_plan, "action_start_frame_offset", 1)),
raw_action_dim=transformed_raw_action_dim,
domain_id=domain_id,
fps=fps,
height=int(video_tensor.shape[-2]),
width=int(video_tensor.shape[-1]),
image_size=sample.get("image_size"),
num_frames=int(video_tensor.shape[1]),
num_inference_steps=int(
extra_param_alias("num_inference_steps", "num_steps", ROBOLAB_DEFAULT_NUM_INFERENCE_STEPS)
),
guidance_scale=float(extra_param_alias("guidance_scale", "guidance", ROBOLAB_DEFAULT_GUIDANCE_SCALE)),
flow_shift=float(extra_param_alias("flow_shift", "shift", ROBOLAB_DEFAULT_FLOW_SHIFT)),
seed=next_robolab_seed(extra, obs, request_id),
history_length=history_length,
action_space=action_space,
observation=obs,
)
@staticmethod
def _build_action_condition_mask_from_indexes(
indexes: list[int],
action_length: int,
*,
device: torch.device,
dtype: torch.dtype,
) -> torch.Tensor:
mask = torch.zeros(1, action_length, 1, device=device, dtype=dtype)
for idx in indexes:
if idx < 0 or idx >= action_length:
raise ValueError(f"Action condition index {idx} is out of range for action length {action_length}.")
mask[:, idx, :] = 1.0
return mask
def _forward_robolab_policy(
self,
sp: OmniDiffusionSamplingParams,
inputs: RoboLabPolicyInputs,
pipeline_start: float,
) -> DiffusionOutput:
if not getattr(self.transformer, "action_gen", False):
raise ValueError(
"Cosmos3 RoboLab policy serving was requested, but the transformer "
"was initialized without action modules. Check that the checkpoint "
"config enables action_gen and includes action weights."
)
action_mode = ACTION_MODE_POLICY
height = inputs.height
width = inputs.width
num_frames = inputs.num_frames
action_chunk_size = int(inputs.action_tensor.shape[0])
num_inference_steps = inputs.num_inference_steps
guidance_scale = float(inputs.guidance_scale)
flow_shift_target = float(inputs.flow_shift)
domain_id = int(inputs.domain_id)
frame_rate = self._get_sp_param(sp, "resolved_frame_rate") or self._get_sp_param(sp, "frame_rate") or inputs.fps
max_sequence_length = (
self._get_sp_param(sp, "max_sequence_length", COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH)
or COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH
)
use_system_prompt = bool(self._get_sp_param(sp, "use_system_prompt", False))
self._guidance_scale = guidance_scale
self._num_timesteps = num_inference_steps
generator = sp.generator
if generator is None:
generator = torch.Generator(device=self.device).manual_seed(int(inputs.seed))
cond_ids, cond_mask, uncond_ids, uncond_mask = self._format_and_tokenize_prompts(
inputs.prompt,
"",
num_frames,
frame_rate,
height,
width,
max_sequence_length,
sp,
use_system_prompt,
is_t2i=False,
)
action_video_tensor = inputs.video_tensor
if action_video_tensor.ndim == 4:
action_video_tensor = action_video_tensor.unsqueeze(0)
if action_video_tensor.ndim != 5:
raise ValueError(
"Cosmos3 RoboLab action video tensor must have shape [1, 3, T, H, W] "
f"or [3, T, H, W], got {tuple(action_video_tensor.shape)}."
)
if action_video_tensor.shape[2] < num_frames:
pad = action_video_tensor[:, :, -1:].repeat(1, 1, num_frames - action_video_tensor.shape[2], 1, 1)
action_video_tensor = torch.cat([action_video_tensor, pad], dim=2)
elif action_video_tensor.shape[2] > num_frames:
action_video_tensor = action_video_tensor[:, :, :num_frames]
action_latents, action_velocity_mask, action_condition_latents, raw_action_dim = self._prepare_action_latents(
mode=action_mode,
action_chunk_size=action_chunk_size,
raw_action_dim=int(inputs.raw_action_dim),
generator=generator,
sp=sp,
clean_action=inputs.action_tensor,
condition_indexes=inputs.action_condition_indexes,
)
action_offset = int(inputs.action_start_frame_offset)
latents, velocity_mask, condition_latents = self._prepare_latents_action_video(
action_video_tensor,
action_mode,
height,
width,
num_frames,
generator,
image_size=inputs.image_size,
)
image_latent = condition_latents[:, :, 0:1]
video_shape = (latents.shape[2], latents.shape[3], latents.shape[4])
shared_kwargs = dict(
video_shape=video_shape,
fps=frame_rate,
noisy_frame_mask=velocity_mask,
action_domain_ids=torch.tensor([domain_id], dtype=torch.long, device=self.device),
action_noisy_mask=action_velocity_mask,
action_start_frame_offset=action_offset,
action_fps=float(self._get_sp_param(sp, "action_fps", frame_rate) or frame_rate),
)
scheduler = build_robolab_unipc_scheduler(num_inference_steps, flow_shift_target, self.device)
_, action_latents = self.diffuse(
latents=latents,
timesteps=scheduler.timesteps,
cond_ids=cond_ids,
cond_mask=cond_mask,
uncond_ids=uncond_ids,
uncond_mask=uncond_mask,
guidance_scale=guidance_scale,
shared_kwargs=shared_kwargs,
action_latents=action_latents,
action_velocity_mask=action_velocity_mask,
action_condition_latents=action_condition_latents,
sound_latents=None,
velocity_mask=velocity_mask,
image_latent=image_latent,
condition_latents=condition_latents,
guidance_interval=None,
raw_action_dim=raw_action_dim,
scheduler=scheduler,
)
if _is_rank_zero():
logger.info("Total pipeline time: %.2fs", time.time() - pipeline_start)
action = action_latents[:, :, :raw_action_dim].detach().cpu()
custom_action_output: dict[str, Any] = {
"action": action,
"raw_action_dim": raw_action_dim,
"action_mode": action_mode,
"domain_id": domain_id,
"action_only_output": True,
"robolab_action_postprocess": make_robolab_action_postprocess_inputs(inputs),
}
return DiffusionOutput(output={}, custom_output=custom_action_output)
@staticmethod
def _truthy(value) -> bool:
if isinstance(value, str):
@@ -1313,7 +1637,28 @@ class Cosmos3OmniDiffusersPipeline(
return latent.to(self.dtype)
def _encode_video_tensor(self, video_tensor: torch.Tensor) -> torch.Tensor:
def _latent_hw_from_image_size(self, image_size: Any | None) -> tuple[int, int] | None:
if image_size is None:
return None
if isinstance(image_size, torch.Tensor):
frame_size = image_size.detach().cpu().flatten()
else:
frame_size = torch.as_tensor(image_size).flatten()
if frame_size.numel() < 4:
return None
orig_h = int(frame_size[2].item())
orig_w = int(frame_size[3].item())
spatial_factor = int(self.vae_scale_factor_spatial)
return max(orig_h // spatial_factor, 1), max(orig_w // spatial_factor, 1)
def _crop_latent_to_image_size(self, latent: torch.Tensor, image_size: Any | None) -> torch.Tensor:
latent_hw = self._latent_hw_from_image_size(image_size)
if latent_hw is None:
return latent
h_latent, w_latent = latent_hw
return latent[:, :, :, :h_latent, :w_latent].contiguous()
def _encode_video_tensor(self, video_tensor: torch.Tensor, image_size: Any | None = None) -> torch.Tensor:
"""VAE-encode a preprocessed pixel video [1, 3, T, H, W]."""
if video_tensor.ndim == 4:
video_tensor = video_tensor.unsqueeze(0)
@@ -1335,6 +1680,7 @@ class Cosmos3OmniDiffusersPipeline(
scaling_factor = getattr(self.vae.config, "scaling_factor", 1.0)
latent = latent * scaling_factor
latent = self._crop_latent_to_image_size(latent, image_size)
return latent.to(self.dtype)
def _prepare_latents_i2v(
@@ -1395,7 +1741,7 @@ class Cosmos3OmniDiffusersPipeline(
T_lat = (num_frames - 1) // self.vae_scale_factor_temporal + 1
H_lat = video_tensor.shape[-2] // self.vae_scale_factor_spatial
W_lat = video_tensor.shape[-1] // self.vae_scale_factor_spatial
indexes = _normalize_condition_frame_indexes_vision(condition_frame_indexes_vision)
indexes = normalize_condition_frame_indexes_vision(condition_frame_indexes_vision)
out_of_range = [index for index in indexes if index >= T_lat]
if out_of_range:
raise ValueError(
@@ -1409,7 +1755,7 @@ class Cosmos3OmniDiffusersPipeline(
device=self.device,
dtype=self.dtype,
)
condition_pixel_frames = _condition_pixel_frame_count(indexes, self.vae_scale_factor_temporal)
condition_pixel_frames = condition_pixel_frame_count(indexes, self.vae_scale_factor_temporal)
condition_video = video_tensor[:, :, :condition_pixel_frames]
if condition_video.shape[2] < condition_pixel_frames:
pad = condition_video[:, :, -1:].repeat(1, 1, condition_pixel_frames - condition_video.shape[2], 1, 1)
@@ -1445,13 +1791,18 @@ class Cosmos3OmniDiffusersPipeline(
width: int,
num_frames: int,
generator: torch.Generator,
image_size: Any | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Prepare video latents for action modes with mode-specific conditioning."""
del height, width
C = self.transformer.latent_channel_size
T_lat = (num_frames - 1) // self.vae_scale_factor_temporal + 1
H_lat = video_tensor.shape[-2] // self.vae_scale_factor_spatial
W_lat = video_tensor.shape[-1] // self.vae_scale_factor_spatial
latent_hw = self._latent_hw_from_image_size(image_size)
if latent_hw is None:
H_lat = video_tensor.shape[-2] // self.vae_scale_factor_spatial
W_lat = video_tensor.shape[-1] // self.vae_scale_factor_spatial
else:
H_lat, W_lat = latent_hw
noise = randn_tensor(
(1, C, T_lat, H_lat, W_lat),
@@ -1459,7 +1810,7 @@ class Cosmos3OmniDiffusersPipeline(
device=self.device,
dtype=self.dtype,
)
cond_latent = self._encode_video_tensor(video_tensor)
cond_latent = self._encode_video_tensor(video_tensor, image_size=image_size)
if cond_latent.shape[2:] != noise.shape[2:]:
raise ValueError(
"Cosmos3 action video latent shape mismatch: "
@@ -1484,9 +1835,25 @@ class Cosmos3OmniDiffusersPipeline(
raw_action_dim: int | None,
generator: torch.Generator,
sp,
clean_action: torch.Tensor | None = None,
condition_indexes: list[int] | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]:
action_dim = int(getattr(self.transformer, "action_dim", 64))
if mode == ACTION_MODE_FORWARD_DYNAMICS:
if clean_action is not None:
action = clean_action.detach().to(dtype=torch.float32)
if action.ndim == 3 and action.shape[0] == 1:
action = action.squeeze(0)
if action.ndim != 2:
raise ValueError(f"Cosmos3 clean action must have shape [T, D], got {tuple(action.shape)}.")
if action.shape[0] < action_chunk_size:
pad = action[-1:].repeat(action_chunk_size - action.shape[0], 1)
action = torch.cat([action, pad], dim=0)
elif action.shape[0] > action_chunk_size:
action = action[:action_chunk_size]
if raw_action_dim is None:
raw_action_dim = int(action.shape[-1])
clean_action = pad_action_to_dim(action, action_dim)
elif mode == ACTION_MODE_FORWARD_DYNAMICS:
action = load_action_tensor(self._get_sp_param(sp, "action", None))
if action.shape[0] < action_chunk_size:
pad = action[-1:].repeat(action_chunk_size - action.shape[0], 1)
@@ -1508,12 +1875,20 @@ class Cosmos3OmniDiffusersPipeline(
raise ValueError(f"Cosmos3 raw_action_dim must be in [1, {action_dim}], got {raw_action_dim}.")
clean_action = clean_action.to(device=self.device, dtype=self.dtype).unsqueeze(0)
condition_mask = build_action_condition_mask(
mode,
action_chunk_size,
device=self.device,
dtype=self.dtype,
)
if condition_indexes is None:
condition_mask = build_action_condition_mask(
mode,
action_chunk_size,
device=self.device,
dtype=self.dtype,
)
else:
condition_mask = self._build_action_condition_mask_from_indexes(
condition_indexes,
action_chunk_size,
device=self.device,
dtype=self.dtype,
)
noise = randn_tensor(
(1, action_chunk_size, action_dim),
generator=generator,
@@ -1548,6 +1923,7 @@ class Cosmos3OmniDiffusersPipeline(
condition_latents: torch.Tensor | None = None,
guidance_interval: tuple[float, float] | None = None,
raw_action_dim: int | None = None,
scheduler: Any | None = None,
) -> torch.Tensor | tuple[torch.Tensor, ...]:
"""Denoising loop with 3-mode CFG support (parallel, sequential, none).
@@ -1578,6 +1954,7 @@ class Cosmos3OmniDiffusersPipeline(
"""
do_cfg = guidance_scale > 1.0
cfg_parallel = self._cfg_parallel_active() and do_cfg
step_scheduler = scheduler if scheduler is not None else self.scheduler
self.transformer.reset_cache()
def _cfg_active_at(t: torch.Tensor) -> bool:
@@ -1655,11 +2032,11 @@ class Cosmos3OmniDiffusersPipeline(
if raw_action_dim is not None and 0 < raw_action_dim < action_pred.shape[-1]:
action_pred[..., raw_action_dim:] = 0
if action_latents is None and sound_latents is None:
latents = self.scheduler.step(video_pred, t, latents, return_dict=False)[0]
latents = step_scheduler.step(video_pred, t, latents, return_dict=False)[0]
else:
packed_noise, shapes, numels = _pack_joint(video_pred, action_pred, sound_pred)
packed_latents, _, _ = _pack_joint(latents, action_latents, sound_latents)
packed_next = self.scheduler.step(packed_noise, t, packed_latents, return_dict=False)[0]
packed_next = step_scheduler.step(packed_noise, t, packed_latents, return_dict=False)[0]
unpacked = _unpack_joint(packed_next, shapes, numels)
latents = unpacked[0]
idx = 1
@@ -1811,7 +2188,12 @@ class Cosmos3OmniDiffusersPipeline(
if len(req.prompts) > 1:
raise ValueError("Cosmos3OmniDiffusersPipeline currently supports a single prompt per request.")
sp = req.sampling_params
prompt_data = req.prompts[0]
robolab_inputs = self._build_robolab_policy_inputs(sp, prompt_data, getattr(req, "request_id", None))
if robolab_inputs is not None:
return self._forward_robolab_policy(sp, robolab_inputs, pipeline_start)
if isinstance(prompt_data, str):
prompt = prompt_data
negative_prompt = None
@@ -1824,7 +2206,6 @@ class Cosmos3OmniDiffusersPipeline(
image_tensor = additional_info.get("preprocessed_image")
video_tensor = additional_info.get("preprocessed_video")
sp = req.sampling_params
is_t2i = self._is_t2i_request(req)
sound_enabled = self._is_sound_request(prompt_data, sp)
action_mode = self._get_action_mode(prompt_data, sp)
@@ -1948,8 +2329,7 @@ class Cosmos3OmniDiffusersPipeline(
self._num_timesteps = num_inference_steps
# Always resolve to a concrete target shift for this request, then
# update the scheduler. This is what guarantees mode-to-mode
# transitions restore the right schedule (no T2I to T2V leak).
# update the shared Diffusers scheduler.
self._set_flow_shift(flow_shift_target)
generator = sp.generator
@@ -2010,12 +2390,16 @@ class Cosmos3OmniDiffusersPipeline(
raw_action_dim_param = self._get_sp_param(sp, "raw_action_dim", None)
raw_action_dim = int(raw_action_dim_param) if raw_action_dim_param is not None else None
clean_action = None
action_condition_indexes = None
action_prepared = self._prepare_action_latents(
mode=action_mode,
action_chunk_size=action_chunk_size,
raw_action_dim=raw_action_dim,
generator=generator,
sp=sp,
clean_action=clean_action,
condition_indexes=action_condition_indexes,
)
action_latents, action_velocity_mask, action_condition_latents, raw_action_dim = action_prepared
action_offset = action_start_frame_offset(action_mode, action_chunk_size, num_frames)
@@ -2031,7 +2415,7 @@ class Cosmos3OmniDiffusersPipeline(
)
image_latent = condition_latents[:, :, 0:1]
elif is_v2v:
condition_frame_indexes_vision = _normalize_condition_frame_indexes_vision(
condition_frame_indexes_vision = normalize_condition_frame_indexes_vision(
self._get_sp_param(
sp,
"condition_frame_indexes_vision",
@@ -2088,9 +2472,10 @@ class Cosmos3OmniDiffusersPipeline(
def _run_diffusion(start_latents):
self.scheduler.set_timesteps(num_inference_steps, device=self.device)
scheduler = self.scheduler
return self.diffuse(
latents=start_latents,
timesteps=self.scheduler.timesteps,
timesteps=scheduler.timesteps,
cond_ids=cond_ids,
cond_mask=cond_mask,
uncond_ids=uncond_ids,
@@ -2106,6 +2491,7 @@ class Cosmos3OmniDiffusersPipeline(
condition_latents=condition_latents,
guidance_interval=guidance_interval,
raw_action_dim=raw_action_dim,
scheduler=scheduler,
)
if is_t2i and batch_size > 1:
@@ -2153,14 +2539,12 @@ class Cosmos3OmniDiffusersPipeline(
if action_latents is None or raw_action_dim is None or domain_id is None:
raise ValueError("Cosmos3 action generation finished without action latents.")
action = action_latents[:, :, :raw_action_dim].detach().cpu()
return DiffusionOutput(
output={"video": video},
custom_output={
"action": action,
"raw_action_dim": raw_action_dim,
"action_mode": action_mode,
"domain_id": domain_id,
},
)
custom_action_output: dict[str, Any] = {
"action": action,
"raw_action_dim": raw_action_dim,
"action_mode": action_mode,
"domain_id": domain_id,
}
return DiffusionOutput(output={"video": video}, custom_output=custom_action_output)
return DiffusionOutput(output={"image": video} if is_t2i else {"video": video})
@@ -33,12 +33,22 @@ from vllm_omni.diffusion.attention.layer import Attention as FrameworkAttention
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.distributed.sp_plan import SequenceParallelInput, SequenceParallelOutput
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
from vllm_omni.diffusion.layers.norm import RMSNorm
from vllm_omni.diffusion.layers.norm import RMSNorm as _VllmRMSNorm
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
class RMSNorm(_VllmRMSNorm):
"""Cosmos3-local RMSNorm that uses the FP32 native implementation."""
def forward_cuda(self, x: torch.Tensor) -> torch.Tensor:
return self.forward_native(x)
def forward_hip(self, x: torch.Tensor) -> torch.Tensor:
return self.forward_native(x)
def _get_ulysses_state() -> tuple[int, int, dist.ProcessGroup | None]:
"""Return (ulysses_size, ulysses_rank, ulysses_pg) from vllm-omni parallel state.
@@ -539,8 +549,8 @@ class Cosmos3CausalAttention(nn.Module):
v = self.to_v(hidden_states).view(B, S, self.num_kv_heads_local, self.head_dim)
# Per-head QK norm
q = F.rms_norm(q, (q.shape[-1],), self.norm_q.weight, self.norm_q.variance_epsilon)
k = F.rms_norm(k, (k.shape[-1],), self.norm_k.weight, self.norm_k.variance_epsilon)
q = F.rms_norm(q, (self.head_dim,), self.norm_q.weight, eps=self.norm_q.variance_epsilon)
k = F.rms_norm(k, (self.head_dim,), self.norm_k.weight, eps=self.norm_k.variance_epsilon)
# Qwen3-style RoPE
q, k = _apply_rotary_pos_emb(q, k, freqs_cos, freqs_sin)
@@ -704,8 +714,8 @@ class Cosmos3CrossAttention(nn.Module):
v = self.to_v(hidden_states).view(B, S_gen, self.num_kv_heads_local, self.head_dim)
# Per-head QK norm
q = F.rms_norm(q, (q.shape[-1],), self.norm_q.weight, self.norm_q.variance_epsilon)
k = F.rms_norm(k, (k.shape[-1],), self.norm_k.weight, self.norm_k.variance_epsilon)
q = F.rms_norm(q, (self.head_dim,), self.norm_q.weight, eps=self.norm_q.variance_epsilon)
k = F.rms_norm(k, (self.head_dim,), self.norm_k.weight, eps=self.norm_k.variance_epsilon)
# Qwen3-style RoPE
q, k = _apply_rotary_pos_emb(q, k, freqs_cos, freqs_sin)
@@ -1446,7 +1456,8 @@ class Cosmos3VFMTransformer(nn.Module):
"Cosmos3 action_noisy_mask must have shape [B, T_action, 1], "
f"got {tuple(action_noisy_mask.shape)}."
)
hidden_action = hidden_action + time_embed.unsqueeze(1) * action_noisy_mask.to(hidden_action.dtype)
action_noisy_mask = action_noisy_mask.to(dtype=hidden_action.dtype, device=hidden_action.device)
hidden_action = hidden_action + time_embed.unsqueeze(1) * action_noisy_mask
if hidden_sound is not None:
hidden_sound = hidden_sound + time_embed.unsqueeze(1)
+371
View File
@@ -0,0 +1,371 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import zlib
from collections.abc import Iterable
from dataclasses import dataclass
from importlib import import_module
from typing import Any
import numpy as np
import PIL.Image
import torch
import torch.nn.functional as F
from vllm.logger import init_logger
from vllm_omni.diffusion.models.progress_bar import _is_rank_zero
logger = init_logger(__name__)
COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION = (0, 1)
COSMOS3_DEFAULT_CONDITION_VIDEO_KEEP = "first"
# Mirrors the WAN VAE's temporal compression. Authoritative value is
# ``self.vae.config.scale_factor_temporal`` at runtime; this constant exists so
# off-line / API code that runs before the pipeline is constructed can compute
# pixel-frame budgets without instantiating the VAE.
COSMOS3_VAE_TEMPORAL_COMPRESSION = 4
ROBOLAB_DEFAULT_CONDITIONING_FPS = 15.0
ROBOLAB_DEFAULT_ACTION_CHUNK_SIZE = 32
ROBOLAB_DEFAULT_IMAGE_HEIGHT = 540
ROBOLAB_DEFAULT_IMAGE_WIDTH = 640
ROBOLAB_DEFAULT_RAW_ACTION_DIM = 8
ROBOLAB_DEFAULT_DOMAIN_NAME = "droid_lerobot"
ROBOLAB_DEFAULT_RESOLUTION = "480"
ROBOLAB_DEFAULT_GUIDANCE_SCALE = 3.0
ROBOLAB_DEFAULT_NUM_INFERENCE_STEPS = 4
ROBOLAB_DEFAULT_FLOW_SHIFT = 5.0
ROBOLAB_DEFAULT_SEED = 0
ROBOLAB_DEFAULT_ACTION_SPACE = "joint_pos"
ROBOLAB_MIDTRAIN_RAW_ACTION_DIM = 10
ROBOLAB_MIDTRAIN_POSE_ACTION_DIM = 9 # xyz position + rot6d orientation
ROBOLAB_CONCAT_VIEW_DESCRIPTION = (
"The top row is from the wrist-mounted camera. "
"The bottom row contains two horizontally concatenated third-person perspective views of the scene from opposite "
"sides, with the robot visible."
)
@dataclass(frozen=True)
class RoboLabPolicyInputs:
prompt: str
video_tensor: torch.Tensor
action_tensor: torch.Tensor
action_condition_indexes: list[int]
action_start_frame_offset: int
raw_action_dim: int
domain_id: int
fps: float
height: int
width: int
image_size: Any
num_frames: int
num_inference_steps: int
guidance_scale: float
flow_shift: float
seed: int
history_length: int
action_space: str
observation: dict[str, Any]
@dataclass(frozen=True)
class RoboLabActionPostprocessInputs:
history_length: int
action_space: str
eef_pos: np.ndarray | None = None
eef_quat: np.ndarray | None = None
def make_robolab_action_postprocess_inputs(inputs: RoboLabPolicyInputs) -> RoboLabActionPostprocessInputs:
if inputs.action_space != "midtrain":
return RoboLabActionPostprocessInputs(
history_length=inputs.history_length,
action_space=inputs.action_space,
)
obs = inputs.observation
return RoboLabActionPostprocessInputs(
history_length=inputs.history_length,
action_space=inputs.action_space,
eef_pos=ensure_2d_float_array(obs["observation/eef_pos"], "observation/eef_pos", 3),
eef_quat=ensure_2d_float_array(obs["observation/eef_quat"], "observation/eef_quat", 4),
)
def normalize_condition_frame_indexes_vision(value: Any) -> tuple[int, ...]:
"""Normalize Cosmos3 vision-conditioning latent frame indexes."""
if value is None:
return COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION
if isinstance(value, str):
value = [item.strip() for item in value.split(",") if item.strip()]
elif isinstance(value, int):
value = [value]
if not isinstance(value, Iterable):
raise TypeError(
"Cosmos3 condition_frame_indexes_vision must be an int, comma-separated string, "
f"or iterable of ints; got {type(value)!r}."
)
indexes = tuple(sorted({int(index) for index in value}))
if not indexes:
raise ValueError("Cosmos3 condition_frame_indexes_vision must contain at least one index.")
if any(index < 0 for index in indexes):
raise ValueError(f"Cosmos3 condition_frame_indexes_vision must be non-negative, got {indexes}.")
return indexes
def condition_pixel_frame_count(
condition_frame_indexes_vision: Iterable[int],
temporal_compression: int = COSMOS3_VAE_TEMPORAL_COMPRESSION,
) -> int:
return max(condition_frame_indexes_vision) * int(temporal_compression) + 1
def normalize_condition_video_keep(value: Any) -> str:
keep = str(value or COSMOS3_DEFAULT_CONDITION_VIDEO_KEEP).strip().lower()
if keep not in {"first", "last"}:
raise ValueError("Cosmos3 condition_video_keep must be either 'first' or 'last'.")
return keep
def normalize_robolab_action_space(value: Any) -> str:
action_space = str(value or ROBOLAB_DEFAULT_ACTION_SPACE).strip().lower()
aliases = {
"jointpos": "joint_pos",
"joint_pos": "joint_pos",
"abs_ik": "midtrain",
"midtrain": "midtrain",
}
if action_space not in aliases:
raise ValueError(f"Unsupported RoboLab action_space={value!r}; expected joint_pos/jointpos or midtrain/abs_ik.")
return aliases[action_space]
def ensure_rgb_uint8_image(value: Any, key: str) -> np.ndarray:
image = np.asarray(value)
if image.ndim != 3 or image.shape[-1] != 3:
raise ValueError(f"{key!r} must have shape [H, W, 3], got {image.shape}.")
if image.dtype != np.uint8:
image = np.clip(image, 0, 255).astype(np.uint8)
return np.ascontiguousarray(image)
def ensure_2d_float_array(value: Any, key: str, width: int | None = None) -> np.ndarray:
array = np.asarray(value, dtype=np.float32)
if array.ndim == 1:
array = array[None, :]
if array.ndim != 2:
raise ValueError(f"{key!r} must have shape [T, D] or [D], got {array.shape}.")
if width is not None and array.shape[-1] != width:
raise ValueError(f"{key!r} must have width {width}, got {array.shape[-1]}.")
return np.ascontiguousarray(array)
def ensure_gripper_array(value: Any) -> np.ndarray:
array = np.asarray(value, dtype=np.float32)
if array.ndim == 0:
array = array.reshape(1, 1)
elif array.ndim == 1:
array = array[:, None]
if array.ndim != 2 or array.shape[-1] != 1:
raise ValueError(f"'observation/gripper_position' must have shape [T, 1], [T], or scalar, got {array.shape}.")
return np.ascontiguousarray(array)
def resize_rgb_uint8(image: np.ndarray, size: tuple[int, int]) -> np.ndarray:
tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0).float()
resized = F.interpolate(tensor, size=size, mode="bilinear", align_corners=False)
return np.clip(np.round(resized.squeeze(0).permute(1, 2, 0).numpy()), 0, 255).astype(np.uint8)
def compose_robolab_views(obs: dict[str, Any]) -> np.ndarray | None:
required_keys = (
"observation/wrist_image_left",
"observation/exterior_image_1_left",
"observation/exterior_image_2_left",
)
if not all(key in obs for key in required_keys):
return None
wrist = ensure_rgb_uint8_image(obs["observation/wrist_image_left"], "observation/wrist_image_left")
left_raw = ensure_rgb_uint8_image(obs["observation/exterior_image_1_left"], "observation/exterior_image_1_left")
right_raw = ensure_rgb_uint8_image(obs["observation/exterior_image_2_left"], "observation/exterior_image_2_left")
half_h, half_w = wrist.shape[0] // 2, wrist.shape[1] // 2
left = resize_rgb_uint8(left_raw, (half_h, half_w))
right = resize_rgb_uint8(right_raw, (half_h, half_w))
return np.concatenate([wrist, np.concatenate([left, right], axis=1)], axis=0)
def extract_robolab_image(obs: dict[str, Any]) -> np.ndarray:
if "observation/image" in obs:
return ensure_rgb_uint8_image(obs["observation/image"], "observation/image")
image = compose_robolab_views(obs)
if image is not None:
return image
raise ValueError("Observation must contain 'observation/image' or RoboLab wrist/exterior image keys.")
def extract_robolab_prompt_image(prompt_data: Any | None) -> np.ndarray | None:
if not isinstance(prompt_data, dict):
return None
multi_modal_data = prompt_data.get("multi_modal_data", {}) or {}
image = multi_modal_data.get("image")
if image is None:
return None
if isinstance(image, PIL.Image.Image):
return np.asarray(image.convert("RGB"))
return ensure_rgb_uint8_image(image, "multi_modal_data.image")
def lazy_import(module_name: str, symbol_name: str, error_message: str):
try:
module = import_module(module_name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(error_message) from exc
return getattr(module, symbol_name)
def lazy_action_transform_pipeline(max_action_dim: int):
ActionTransformPipeline = lazy_import(
"cosmos_framework.data.vfm.action.transforms",
"ActionTransformPipeline",
"Cosmos3 RoboLab policy serving requires cosmos_framework on PYTHONPATH so the "
"golden ActionTransformPipeline can be reused.",
)
return ActionTransformPipeline(max_action_dim=max_action_dim, cfg_dropout_rate=0.0)
def build_robolab_unipc_scheduler(num_steps: int, shift: float, device: torch.device):
FlowUniPCMultistepScheduler = lazy_import(
"cosmos_framework.model.vfm.diffusion.samplers.fm_solvers_unipc",
"FlowUniPCMultistepScheduler",
(
"Cosmos3 RoboLab policy serving requires cosmos_framework on PYTHONPATH so the "
"golden FlowUniPCMultistepScheduler can be reused."
),
)
scheduler = FlowUniPCMultistepScheduler(
num_train_timesteps=1000,
shift=1.0,
use_dynamic_shifting=False,
)
scheduler.set_timesteps(num_steps, device=device, shift=float(shift))
return scheduler
def convert_midtrain_rotation(value: Any, src: str, dst: str) -> np.ndarray:
convert_rotation = lazy_import(
"cosmos_framework.data.vfm.action.pose_utils",
"convert_rotation",
"Cosmos3 RoboLab midtrain action serving requires cosmos_framework pose_utils on PYTHONPATH.",
)
return convert_rotation(value, src, dst)
def pose_abs_to_rel(*args, **kwargs) -> np.ndarray:
pose_abs_to_rel_func = lazy_import(
"cosmos_framework.data.vfm.action.pose_utils",
"pose_abs_to_rel",
"Cosmos3 RoboLab midtrain action serving requires cosmos_framework pose_utils on PYTHONPATH.",
)
return pose_abs_to_rel_func(*args, **kwargs)
def pose_rel_to_abs(*args, **kwargs) -> np.ndarray:
pose_rel_to_abs_func = lazy_import(
"cosmos_framework.data.vfm.action.pose_utils",
"pose_rel_to_abs",
"Cosmos3 RoboLab midtrain action serving requires cosmos_framework pose_utils on PYTHONPATH.",
)
return pose_rel_to_abs_func(*args, **kwargs)
def build_abs_pose_from_components(*args, **kwargs) -> np.ndarray:
build_abs_pose_from_components_func = lazy_import(
"cosmos_framework.data.vfm.action.pose_utils",
"build_abs_pose_from_components",
"Cosmos3 RoboLab midtrain action serving requires cosmos_framework pose_utils on PYTHONPATH.",
)
return build_abs_pose_from_components_func(*args, **kwargs)
def next_robolab_seed(extra: dict[str, Any], obs: dict[str, Any], request_id: str | None) -> int:
base_seed = int(extra.get("robolab_seed") or ROBOLAB_DEFAULT_SEED)
deterministic_seed = str(extra.get("deterministic_seed", "")).strip().lower() in {"1", "true", "yes", "on"}
if deterministic_seed:
return base_seed
explicit_seed = extra.get("seed")
if explicit_seed is not None:
return int(explicit_seed)
seed_key = "|".join(
str(part)
for part in (
base_seed,
extra.get("session_id", ""),
request_id or "",
obs.get("prompt", ""),
)
)
return zlib.crc32(seed_key.encode("utf-8")) & 0x7FFFFFFF
def log_robolab_action_summary(label: str, value: Any) -> None:
if not _is_rank_zero():
return
if isinstance(value, torch.Tensor):
array = value.detach().float().cpu().numpy()
else:
array = np.asarray(value, dtype=np.float32)
finite = np.isfinite(array)
if finite.any():
finite_min = float(array[finite].min())
finite_max = float(array[finite].max())
else:
finite_min = None
finite_max = None
if array.ndim == 0:
head = array.reshape(1).tolist()
else:
head = array.reshape(-1, array.shape[-1])[:3].tolist()
logger.info(
"RoboLab action summary %s: shape=%s nan=%d finite=%d finite_min=%s finite_max=%s head=%s",
label,
tuple(array.shape),
int(np.isnan(array).sum()),
int(finite.sum()),
finite_min,
finite_max,
head,
)
def postprocess_robolab_action(action: torch.Tensor, inputs: RoboLabActionPostprocessInputs) -> np.ndarray:
action_np = action[0].float().cpu().numpy()
log_robolab_action_summary("raw_model_action", action_np)
history_length = int(inputs.history_length)
action_np = action_np[history_length:]
action_np[:, -1] = 1.0 - action_np[:, -1]
if inputs.action_space == "midtrain":
if inputs.eef_pos is None or inputs.eef_quat is None:
raise ValueError("RoboLab midtrain action postprocess requires eef_pos and eef_quat metadata.")
initial_pose = np.eye(4, dtype=np.float32)
initial_pose[:3, :3] = convert_midtrain_rotation(inputs.eef_quat[-1], "quat_xyzw", "matrix")
initial_pose[:3, 3] = inputs.eef_pos[-1]
abs_pose = pose_rel_to_abs(
action_np[:, :ROBOLAB_MIDTRAIN_POSE_ACTION_DIM],
rotation_format="rot6d",
pose_convention="backward_framewise",
initial_pose=initial_pose,
)
position = abs_pose[1:, :3, 3]
quat_xyzw = convert_midtrain_rotation(abs_pose[1:, :3, :3], "matrix", "quat_xyzw")
action_np = np.concatenate([position, quat_xyzw, action_np[:, ROBOLAB_MIDTRAIN_POSE_ACTION_DIM:]], axis=-1)
log_robolab_action_summary("postprocessed_robolab_action", action_np)
return np.asarray(action_np, dtype=np.float32)
+40
View File
@@ -512,6 +512,20 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
"HiDreamImagePipeline": "get_hidream_image_post_process_func",
}
_DIFFUSION_ACTION_POST_PROCESS_FUNCS = {
# arch: action_post_process_func
# `action_post_process_func` function must be placed in {mod_folder}/{mod_relname}.py,
# where mod_folder and mod_relname are defined and mapped using `_DIFFUSION_MODELS` via the `arch` key.
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_action_post_process_func",
}
_DIFFUSION_IR_OP_PRIORITY_FUNCS = {
# arch: ir_op_priority_func
# `ir_op_priority_func` function must be placed in {mod_folder}/{mod_relname}.py,
# where mod_folder and mod_relname are defined and mapped using `_DIFFUSION_MODELS` via the `arch` key.
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_ir_op_priority_func",
}
_DIFFUSION_PRE_PROCESS_FUNCS = {
# arch: pre_process_func
# `pre_process_func` function must be placed in {mod_folder}/{mod_relname}.py,
@@ -543,6 +557,8 @@ def register_diffusion_model(
class_name: str,
pre_process_func_name: str | None = None,
post_process_func_name: str | None = None,
action_post_process_func_name: str | None = None,
ir_op_priority_func_name: str | None = None,
) -> None:
"""Register a diffusion model pipeline from an out-of-tree plugin.
@@ -561,6 +577,12 @@ def register_diffusion_model(
post_process_func_name: Optional name of the post-process function
located in *module_name*. Pass ``None`` to keep the existing
entry when replacing a built-in model.
action_post_process_func_name: Optional name of the action post-process
function located in *module_name*. Pass ``None`` to keep the
existing entry when replacing a built-in model.
ir_op_priority_func_name: Optional name of the IR op priority merge
function located in *module_name*. Pass ``None`` to keep the
existing entry when replacing a built-in model.
"""
# Register model class in DiffusionModelRegistry
DiffusionModelRegistry.register_model(
@@ -578,6 +600,10 @@ def register_diffusion_model(
_DIFFUSION_PRE_PROCESS_FUNCS[model_arch] = pre_process_func_name
if post_process_func_name is not None:
_DIFFUSION_POST_PROCESS_FUNCS[model_arch] = post_process_func_name
if action_post_process_func_name is not None:
_DIFFUSION_ACTION_POST_PROCESS_FUNCS[model_arch] = action_post_process_func_name
if ir_op_priority_func_name is not None:
_DIFFUSION_IR_OP_PRIORITY_FUNCS[model_arch] = ir_op_priority_func_name
logger.info(
"Registered diffusion model %s -> %s.%s",
@@ -608,6 +634,20 @@ def get_diffusion_post_process_func(od_config: OmniDiffusionConfig):
return _load_process_func(od_config, func_name)
def get_diffusion_action_post_process_func(od_config: OmniDiffusionConfig):
if od_config.model_class_name not in _DIFFUSION_ACTION_POST_PROCESS_FUNCS:
return None
func_name = _DIFFUSION_ACTION_POST_PROCESS_FUNCS[od_config.model_class_name]
return _load_process_func(od_config, func_name)
def get_diffusion_ir_op_priority_func(od_config: OmniDiffusionConfig):
if od_config.model_class_name not in _DIFFUSION_IR_OP_PRIORITY_FUNCS:
return None
func_name = _DIFFUSION_IR_OP_PRIORITY_FUNCS[od_config.model_class_name]
return _load_process_func(od_config, func_name)
def get_diffusion_pre_process_func(od_config: OmniDiffusionConfig):
if od_config.model_class_name not in _DIFFUSION_PRE_PROCESS_FUNCS:
return None # Return None if no pre-processing function is registered (for backward compatibility)
+10 -1
View File
@@ -44,6 +44,7 @@ from vllm_omni.diffusion.distributed.parallel_state import (
from vllm_omni.diffusion.forward_context import set_forward_context
from vllm_omni.diffusion.ipc import DIFFUSION_RPC_RESULT_ENVELOPE, pack_diffusion_output_shm
from vllm_omni.diffusion.lora.manager import DiffusionLoRAManager
from vllm_omni.diffusion.registry import get_diffusion_ir_op_priority_func
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.sched.interface import DiffusionSchedulerOutput
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
@@ -160,6 +161,14 @@ def _create_diffusion_worker_vllm_config(device: torch.device, od_config: OmniDi
return vllm_config
def _resolve_ir_op_priority(od_config: OmniDiffusionConfig, vllm_config: VllmConfig) -> Any:
ir_op_priority = current_omni_platform.get_default_ir_op_priority(vllm_config)
ir_op_priority_func = get_diffusion_ir_op_priority_func(od_config)
if ir_op_priority_func is not None:
ir_op_priority = ir_op_priority_func(ir_op_priority, vllm_config=vllm_config)
return ir_op_priority
class DiffusionWorker:
"""
A worker that manages GPU infrastructure and delegates to the model runner.
@@ -235,7 +244,7 @@ class DiffusionWorker:
vllm_config.quant_config = self.od_config.quantization_config
# Since vLLM v0.20.0, IR wraps GPU ops. Set IR op priority preference to enforce GPU op fusion during wrapping.
# Also need to log, because vLLM internally logs another line in VllmConfig.__post_init__. Avoid confusion.
vllm_config.kernel_config.ir_op_priority = current_omni_platform.get_default_ir_op_priority(vllm_config)
vllm_config.kernel_config.ir_op_priority = _resolve_ir_op_priority(self.od_config, vllm_config)
logger.info(
"Final IR op priority after setting vLLM-Omni overrides: %s", vllm_config.kernel_config.ir_op_priority
)
+23 -12
View File
@@ -195,18 +195,21 @@ class OmniOpenAIServingVideo:
# Merge extra_params into extra_args
gen_params.extra_args.update(request.extra_params)
# Redact the inline ``action`` array (hundreds of floats) when
# logging so it doesn't flood the logs; everything else is logged
# verbatim.
# Redact inline arrays when logging so RoboLab policy requests do
# not flood the server log with image/state payloads.
loggable = request.extra_params
action_val = loggable.get("action")
if action_val is not None:
summary = (
f"<{type(action_val).__name__} len={len(action_val)}>"
if hasattr(action_val, "__len__")
else f"<{type(action_val).__name__}>"
redacted = {}
for key in ("action", "robot_obs", "observation"):
value = loggable.get(key)
if value is None:
continue
redacted[key] = (
f"<{type(value).__name__} len={len(value)}>"
if hasattr(value, "__len__")
else f"<{type(value).__name__}>"
)
loggable = {**loggable, "action": summary}
if redacted:
loggable = {**loggable, **redacted}
logger.info("Applied extra_params: %s", loggable)
self._apply_lora(request.lora, gen_params)
@@ -220,7 +223,9 @@ class OmniOpenAIServingVideo:
)
result = await self._run_generation(prompt, gen_params, reference_id)
videos = self._extract_video_outputs(result)
custom_output = self._extract_custom_output(result)
action_only = isinstance(custom_output, dict) and bool(custom_output.get("action_only_output"))
videos = [{"action_only_output": True}] if action_only else self._extract_video_outputs(result)
audios = self._extract_audio_outputs(result, expected_count=len(videos))
actions = self._extract_action_outputs(result, expected_count=len(videos))
audio_sample_rate = self._resolve_audio_sample_rate(result)
@@ -314,6 +319,11 @@ class OmniOpenAIServingVideo:
if "video_codec_options" in request.extra_params:
video_codec_options = request.extra_params["video_codec_options"]
action = artifacts.actions[0]
if action is not None and isinstance(artifacts.videos[0], dict):
logger.info("Action-only video request %s completed; skipping MP4 encoding.", reference_id)
return b"", artifacts.stage_durations, artifacts.peak_memory_mb, action
_t_encode_start = time.perf_counter()
video_bytes = _encode_video_bytes(
artifacts.videos[0],
@@ -510,7 +520,8 @@ class OmniOpenAIServingVideo:
if not custom_output or "action" not in custom_output:
return [None] * expected_count
action_items = cls._split_action_payload(custom_output["action"], expected_count)
action_payload = custom_output.get("actions", custom_output["action"])
action_items = cls._split_action_payload(action_payload, expected_count)
return [
cls._make_video_action(action_item, custom_output) if action_item is not None else None
for action_item in action_items
+66 -11
View File
@@ -7,6 +7,10 @@ Protocol (compatible with OpenPI policy clients):
Connect -> server sends msgpack(PolicyServerConfig fields)
Infer -> client sends msgpack(obs), server sends msgpack(ndarray)
Reset -> client sends msgpack({endpoint:reset}), server sends msgpack(status)
NumPy values use the msgpack-numpy marker mapping:
ndarray -> {nd: true, type, kind, shape, data}
scalar -> {nd: false, type, kind, data}
"""
from __future__ import annotations
@@ -14,6 +18,8 @@ from __future__ import annotations
import asyncio
from typing import Any
import msgspec
import numpy as np
from fastapi import WebSocket
from starlette.websockets import WebSocketDisconnect
from vllm.logger import init_logger
@@ -25,26 +31,75 @@ from vllm_omni.entrypoints.openpi.serving import (
logger = init_logger(__name__)
_DEFAULT_IDLE_TIMEOUT = 30.0
MAX_OPENPI_PAYLOAD_BYTES = 64 * 1024 * 1024
_MISSING = object()
def _get_msgpack_numpy() -> Any:
try:
from openpi_client import msgpack_numpy
except ImportError as exc:
raise ImportError(
"The `/v1/realtime/robot/openpi` endpoint requires the optional "
"`openpi-client` dependency. Install it with `pip install openpi-client`."
) from exc
def _pack_numpy(obj: Any) -> Any:
if isinstance(obj, (np.ndarray, np.generic)) and obj.dtype.kind in ("V", "O", "c"):
raise ValueError(f"Unsupported dtype: {obj.dtype}")
if isinstance(obj, np.ndarray):
if not obj.flags.c_contiguous:
obj = np.ascontiguousarray(obj)
return {
b"nd": True,
b"data": obj.tobytes(),
b"type": obj.dtype.str,
b"kind": obj.dtype.kind,
b"shape": obj.shape,
}
if isinstance(obj, np.generic):
return {
b"nd": False,
b"data": obj.tobytes(),
b"type": obj.dtype.str,
b"kind": obj.dtype.kind,
}
raise TypeError(f"Unsupported type: {type(obj)!r}")
return msgpack_numpy
def _mapping_get(obj: dict[Any, Any], key: str, default: Any = None) -> Any:
return obj.get(key, obj.get(key.encode(), default))
def _decode_marker_text(value: Any) -> str:
if isinstance(value, bytes):
return value.decode()
return str(value)
def _unpack_numpy(obj: Any) -> Any:
if isinstance(obj, dict):
nd = _mapping_get(obj, "nd", _MISSING)
dtype = _mapping_get(obj, "type", _MISSING)
kind = _mapping_get(obj, "kind", _MISSING)
data = _mapping_get(obj, "data", _MISSING)
if nd is not _MISSING and dtype is not _MISSING and kind is not _MISSING and data is not _MISSING:
dtype_obj = np.dtype(_decode_marker_text(dtype))
kind_text = _decode_marker_text(kind)
if dtype_obj.kind != kind_text:
raise ValueError(f"NumPy dtype marker kind mismatch: {dtype_obj.kind!r} != {kind_text!r}")
if dtype_obj.kind in ("V", "O", "c"):
raise ValueError(f"Unsupported dtype: {dtype_obj}")
array = np.frombuffer(data, dtype=dtype_obj).copy()
if nd:
shape = _mapping_get(obj, "shape", _MISSING)
if shape is _MISSING:
raise ValueError("NumPy ndarray marker is missing shape")
return array.reshape(tuple(shape))
return array[0]
return {key: _unpack_numpy(value) for key, value in obj.items()}
if isinstance(obj, list):
return [_unpack_numpy(value) for value in obj]
return obj
def _pack(obj: Any) -> bytes:
return _get_msgpack_numpy().packb(obj)
return msgspec.msgpack.encode(obj, enc_hook=_pack_numpy)
def _unpack(data: bytes) -> Any:
return _get_msgpack_numpy().unpackb(data)
return _unpack_numpy(msgspec.msgpack.decode(data))
class RobotRealtimeConnection: