diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f4eed80ba..1a5a65838 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,9 +10,11 @@ default_stages: [pre-commit, pre-push, manual]
# hooks must not rewrite it either.
# - unirl/models/boogu_image/vendor/ : pristine boogu-project/Boogu-Image model code
# (see VENDOR_COMMIT.txt); same re-vendor rationale as bagel/vendor/.
+# - unirl/models/sensenova_u1/vendor/ : pristine OpenSenseNova/SenseNova-U1
+# NEO-Unify model code (see VENDOR_COMMIT.txt).
# - benchmarks/*/*/data/ : vendored upstream benchmark prompt sets (GenEval,
# PartiPrompts, VBench, ...); kept byte-identical so upstream checksums verify.
-exclude: ^(unirl-reward-service/|unirl/models/bagel/vendor/|unirl/models/boogu_image/vendor/|benchmarks/[^/]+/[^/]+/data/)
+exclude: ^(unirl-reward-service/|unirl/models/bagel/vendor/|unirl/models/boogu_image/vendor/|unirl/models/sensenova_u1/vendor/|benchmarks/[^/]+/[^/]+/data/)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
diff --git a/INSTALL.md b/INSTALL.md
index 34bbcfed9..e6e46fe8c 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -4,7 +4,7 @@ UniRL ships two mutually exclusive inference engines (`vllm` and `sglang`) — i
| Engine | CUDA | glibc |
|---|---|---|
-| **vllm-omni** | 12.9 | ≥ 2.28 |
+| **vllm-omni** | 12.9 | ≥ 2.31 |
| **sglang** | 13.0 | ≥ 2.34 |
## vllm-omni
@@ -15,6 +15,11 @@ export VLLM_USE_PRECOMPILED=1 # else 30+ min CUDA build
uv pip install -e ".[vllm,train,infer]"
```
+The vLLM 0.22 CUDA wheel itself is `manylinux_2_28`, but its required
+`llguidance` wheel is `manylinux_2_31`. On a glibc 2.28 image, use a newer
+runtime/container or build `llguidance` from source with Rust; otherwise the
+resolver falls back to an unsupported or stalled source build.
+
## sglang
```bash
diff --git a/README.md b/README.md
index c36b502bf..f3df512bd 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,7 @@ dimension; all listed models are supported (✅).
| Prompt-Enhancer | LLM + diffusion | Text → Text → Image | ✅ |
| HunyuanImage3 | Unified AR + diffusion | Text → Image | ✅ |
| Bagel | Unified AR + diffusion | Text / Text + Image → Image | ✅ |
+| SenseNova-U1.5 | Unified MoT pixel flow | Text → Image | ✅ |
diff --git a/examples/README.md b/examples/README.md
index 723180fad..218cf69ed 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -15,7 +15,7 @@ entrypoint's built-in `config_name` — a safe place to start.
| Domain | Entrypoint | Default recipe (start here) | Models |
|---|---|---|---|
-| [`diffusion/`](diffusion/) | `python -m unirl.train_diffusion` | `diffusion/sd3/sd3_trainside` | `sd3`, `qwen_image`, `flux2_klein`, `wan21`, `wan22`, `hunyuan_video10`, `hunyuan_video15` |
+| [`diffusion/`](diffusion/) | `python -m unirl.train_diffusion` | `diffusion/sd3/sd3_trainside` | `sd3`, `qwen_image`, `flux2_klein`, `sensenova_u1_5`, `wan21`, `wan22`, `hunyuan_video10`, `hunyuan_video15` |
| [`ar/`](ar/) | `python -m unirl.train_ar` | `ar/qwen_vl_grpo_geo3k_mc_4x8`, `ar/qwen3_drpo_4b_base_dapo_sglang` | `qwen_vl` (vision-language), `qwen3` (text-only) |
| [`sft/`](sft/) | `python -m unirl.train_sft` | `sft/qwen3_sft` | `qwen3`, `qwen_vl`, `bagel`, `sd3`, `cosmos3`, `wan21` |
| [`pe/`](pe/) | `python -m unirl.train_pe` | `pe/pe_trainside_pickscore` | `pe` (Qwen3 rewriter + SD3, PickScore/WISE reward) |
@@ -95,7 +95,7 @@ related recipes sort together.
| Segment | Position | Values (examples) | Omit when |
|---|---|---|---|
-| `model` | required, first | `sd3`, `qwen_image`, `flux2_klein`, `wan21`, `wan22`, `hunyuan_video10`, `hunyuan_video15`, `qwen_vl`, `qwen3`, `hi3` | never |
+| `model` | required, first | `sd3`, `qwen_image`, `flux2_klein`, `sensenova_u1_5`, `wan21`, `wan22`, `hunyuan_video10`, `hunyuan_video15`, `qwen_vl`, `qwen3`, `hi3` | never |
| `task` | after model | `t2v`, `i2v` | text-to-image (the implicit default) |
| `size` | after task | `4b`, `14b` | only one size in the family |
| `algorithm` | middle | `dancegrpo`, `mixgrpo`, `nft`, `flowdppo`, `grpo`, `drpo` | plain FlowGRPO (diffusion default); GRPO (AR default) |
diff --git a/examples/diffusion/sensenova_u1_5/sensenova_u1_5_trainside.yaml b/examples/diffusion/sensenova_u1_5/sensenova_u1_5_trainside.yaml
new file mode 100644
index 000000000..0dd4d5a74
--- /dev/null
+++ b/examples/diffusion/sensenova_u1_5/sensenova_u1_5_trainside.yaml
@@ -0,0 +1,137 @@
+# @package _global_
+# SenseNova-U1.5-8B-MoT-Preview pixel-flow GRPO with trainside rollout.
+#
+# Launch:
+# SENSENOVA_U1_PATH=/path/to/SenseNova-U1.5-8B-MoT-Preview \
+# python -m unirl.train_diffusion --config-name diffusion/sensenova_u1_5/sensenova_u1_5_trainside
+
+num_devices: 8
+batch_size: 8
+adv_use_global_std: true
+num_rollouts: 10000
+
+logging:
+ report_to_wandb: false
+ project_name: unirl
+ run_name: sensenova-u1.5-grpo
+ entity: null
+ tags: [sensenova-u1.5, pixel-flow, trainside, fullft]
+
+bundle:
+ _target_: unirl.models.sensenova_u1.bundle.SenseNovaU1Bundle.from_config
+ config:
+ _target_: unirl.models.sensenova_u1.config.SenseNovaU1PipelineConfig
+ pretrained_model_ckpt_path: ${oc.env:SENSENOVA_U1_PATH,sensenova/SenseNova-U1.5-8B-MoT-Preview}
+ # Qwen3DecoderLayer interleaves frozen und and trainable gen parameters.
+ # FSDP2 requires one original dtype per shard group, so keep the complete
+ # model in fp32 masters and use bf16 only through mixed-precision compute.
+ model_precision: fp32
+ autocast_precision: bf16
+ trajectory_precision: bf16
+ logprob_precision: fp32
+ timestep_shift: 3.0
+ attention_backend: auto
+ # Matches the released U1.5 pretraining recipe: freeze the shared /
+ # understanding path, train fm_modules + every *_mot_gen branch.
+ full_finetune_generation: true
+
+pipeline:
+ _target_: unirl.models.sensenova_u1.pipeline.SenseNovaU1Pipeline
+ shift: ${bundle.config.timestep_shift}
+ autocast_precision: ${bundle.config.autocast_precision}
+ trajectory_precision: ${bundle.config.trajectory_precision}
+ logprob_precision: ${bundle.config.logprob_precision}
+ strategy:
+ _target_: unirl.sde.kernels.FlowSDEStrategy
+
+backend:
+ _target_: unirl.train.backend.fsdp.FSDPBackend
+ block_class_names: ["Qwen3DecoderLayer", "NEOVisionModel"]
+ trainable_attr: transformer
+ fsdp_cfg:
+ _target_: unirl.train.configs.FSDPConfig
+ param_dtype: bf16
+ master_dtype: fp32
+ cpu_offload: false
+ mixed_precision: true
+ fsdp_mode: full
+ reshard_after_forward: true
+ activation_checkpointing: true
+ use_torch_compile: false
+ # The trainable fm_modules tree is outside the decoder blocks.
+ root_wrap: true
+ optimizer_cfg:
+ _target_: unirl.train.backend.base.OptimizerConfig
+ learning_rate: 2.0e-5
+ adam_beta1: 0.9
+ adam_beta2: 0.95
+ adam_epsilon: 1.0e-8
+ weight_decay: 0.0
+ scheduler_cfg:
+ _target_: unirl.train.backend.base.LrSchedulerConfig
+ type: constant
+ warmup_steps: 0
+ total_steps: ${num_rollouts}
+
+rollout:
+ _target_: unirl.rollout.engine.trainside.engine.TrainsideRolloutEngine
+ stage_attrs: [diffusion]
+ # Prefix KV caches and pixel-flow trajectories are intentionally per-sample.
+ forward_batch_size: 1
+
+reward:
+ _target_: unirl.reward.service.RewardService
+ backend:
+ _target_: unirl.reward.local.pickscore.PickScoreRewardScorer
+ base_device: cuda
+ config:
+ _target_: unirl.reward.local.pickscore.PickScoreSpec
+ batch_size: 4
+ device: auto
+ processor_id: laion/CLIP-ViT-H-14-laion2B-s32B-b79K
+ model_id: yuvalkirstain/PickScore_v1
+
+algorithm:
+ _target_: unirl.algorithms.flowgrpo.FlowGRPO
+ stage_attr: diffusion
+ clip_range: 1.0e-4
+ clip_schedule: constant
+ conditions_cls:
+ _target_: hydra.utils.get_class
+ path: unirl.models.sensenova_u1.conditions.SenseNovaU1Conditions
+ params: ${sampling}
+
+stack:
+ _target_: unirl.train.stack.TrainStack
+ micro_batch_size: 1
+ max_grad_norm: 1.0
+ num_updates_per_batch: 2
+
+data_source:
+ _target_: unirl.data.data_source.MultimodalRLDataSource
+ args:
+ run:
+ data_path: datasets/pickscore/train.txt
+ eval_data_path: datasets/pickscore/test.txt
+ seed: 42
+ algorithm:
+ prompts_per_rollout: ${batch_size}
+
+sampling:
+ _target_: unirl.models.sensenova_u1.diffusion.SenseNovaU1DiffusionParams
+ num_inference_steps: 20
+ guidance_scale: 4.0
+ cfg_norm: global
+ cfg_interval: [0.0, 1.0]
+ t_eps: 0.02
+ height: 512
+ width: 512
+ eta: 0.7
+ samples_per_prompt: 4
+ seed: 42
+ init_same_noise: false
+ scheduler:
+ _target_: unirl.sde.index_schedule.AllSDEScheduler
+ num_timesteps: ${..num_inference_steps}
+ num_sde_steps: 3
+ timestep_fraction: [0.0, 0.5]
diff --git a/examples/diffusion/sensenova_u1_5/sensenova_u1_5_vllmomni_full_ipc.yaml b/examples/diffusion/sensenova_u1_5/sensenova_u1_5_vllmomni_full_ipc.yaml
new file mode 100644
index 000000000..02f6522b5
--- /dev/null
+++ b/examples/diffusion/sensenova_u1_5/sensenova_u1_5_vllmomni_full_ipc.yaml
@@ -0,0 +1,148 @@
+# @package _global_
+# SenseNova-U1.5 pixel-flow GRPO with colocated vLLM-Omni rollout and
+# bucketed CUDA-IPC full-weight synchronization.
+#
+# Launch:
+# SENSENOVA_U1_PATH=/path/to/SenseNova-U1.5-8B-MoT-Preview \
+# python -m unirl.train_diffusion \
+# --config-name diffusion/sensenova_u1_5/sensenova_u1_5_vllmomni_full_ipc
+
+num_devices: 8
+batch_size: 8
+adv_use_global_std: true
+num_rollouts: 10000
+weight_sync_interval: 1
+
+logging:
+ report_to_wandb: false
+ project_name: unirl
+ run_name: sensenova-u1.5-grpo-vllmomni
+ entity: null
+ tags: [sensenova-u1.5, pixel-flow, vllm-omni, fullft, ipc]
+
+bundle:
+ _target_: unirl.models.sensenova_u1.bundle.SenseNovaU1Bundle.from_config
+ config:
+ _target_: unirl.models.sensenova_u1.config.SenseNovaU1PipelineConfig
+ pretrained_model_ckpt_path: ${oc.env:SENSENOVA_U1_PATH,sensenova/SenseNova-U1.5-8B-MoT-Preview}
+ model_precision: fp32
+ autocast_precision: bf16
+ trajectory_precision: bf16
+ logprob_precision: fp32
+ timestep_shift: 3.0
+ attention_backend: auto
+ full_finetune_generation: true
+
+pipeline:
+ _target_: unirl.models.sensenova_u1.pipeline.SenseNovaU1Pipeline
+ shift: ${bundle.config.timestep_shift}
+ autocast_precision: ${bundle.config.autocast_precision}
+ trajectory_precision: ${bundle.config.trajectory_precision}
+ logprob_precision: ${bundle.config.logprob_precision}
+ strategy:
+ _target_: unirl.sde.kernels.FlowSDEStrategy
+
+backend:
+ _target_: unirl.train.backend.fsdp.FSDPBackend
+ block_class_names: ["Qwen3DecoderLayer", "NEOVisionModel"]
+ trainable_attr: transformer
+ fsdp_cfg:
+ _target_: unirl.train.configs.FSDPConfig
+ param_dtype: bf16
+ master_dtype: fp32
+ cpu_offload: false
+ mixed_precision: true
+ fsdp_mode: full
+ reshard_after_forward: true
+ activation_checkpointing: true
+ use_torch_compile: false
+ root_wrap: true
+ optimizer_cfg:
+ _target_: unirl.train.backend.base.OptimizerConfig
+ learning_rate: 2.0e-5
+ adam_beta1: 0.9
+ adam_beta2: 0.95
+ adam_epsilon: 1.0e-8
+ weight_decay: 0.0
+ scheduler_cfg:
+ _target_: unirl.train.backend.base.LrSchedulerConfig
+ type: constant
+ warmup_steps: 0
+ total_steps: ${num_rollouts}
+
+rollout:
+ _target_: unirl.rollout.engine.vllm_omni.engine.VLLMOmniRolloutEngine
+ model_config: ${bundle.config}
+ config:
+ _target_: unirl.rollout.engine.vllm_omni.config.VLLMOmniEngineConfig
+ model_path: ${bundle.config.pretrained_model_ckpt_path}
+ modality: sensenova_u1_t2i
+ enable_sleep_mode: true
+
+reward:
+ _target_: unirl.reward.service.RewardService
+ backend:
+ _target_: unirl.reward.local.pickscore.PickScoreRewardScorer
+ base_device: cuda
+ config:
+ _target_: unirl.reward.local.pickscore.PickScoreSpec
+ batch_size: 4
+ device: auto
+ processor_id: laion/CLIP-ViT-H-14-laion2B-s32B-b79K
+ model_id: yuvalkirstain/PickScore_v1
+
+algorithm:
+ _target_: unirl.algorithms.flowgrpo.FlowGRPO
+ stage_attr: diffusion
+ old_logp_source: replay
+ clip_range: 1.0e-4
+ clip_schedule: constant
+ conditions_cls:
+ _target_: hydra.utils.get_class
+ path: unirl.models.sensenova_u1.conditions.SenseNovaU1Conditions
+ params: ${sampling}
+
+stack:
+ _target_: unirl.train.stack.TrainStack
+ micro_batch_size: 1
+ max_grad_norm: 1.0
+ num_updates_per_batch: 2
+
+data_source:
+ _target_: unirl.data.data_source.MultimodalRLDataSource
+ args:
+ run:
+ data_path: datasets/pickscore/train.txt
+ eval_data_path: datasets/pickscore/test.txt
+ seed: 42
+ algorithm:
+ prompts_per_rollout: ${batch_size}
+
+sampling:
+ _target_: unirl.models.sensenova_u1.diffusion.SenseNovaU1DiffusionParams
+ num_inference_steps: 20
+ guidance_scale: 4.0
+ cfg_norm: global
+ cfg_interval: [0.0, 1.0]
+ t_eps: 0.02
+ height: 512
+ width: 512
+ eta: 0.7
+ samples_per_prompt: 4
+ seed: 42
+ init_same_noise: false
+ scheduler:
+ _target_: unirl.sde.index_schedule.AllSDEScheduler
+ num_timesteps: ${..num_inference_steps}
+ num_sde_steps: 3
+ timestep_fraction: [0.0, 0.5]
+
+sync:
+ _target_: unirl.distributed.weight_sync.full.ipc.IPCWeightSync
+ bucket_size_mb: 2048
+ use_shm: false
+ flush_cache: true
+ wire_dtype: bf16
+ # Strip SenseNovaU1TrainableModel's facade prefix. vLLM-Omni exposes the
+ # same children directly as language_model.*, vision_model.*, fm_modules.*.
+ name_remap: {"model.*": "*"}
diff --git a/examples/run_reward_curve_verification_taiji.sh b/examples/run_reward_curve_verification_taiji.sh
index 83e939de7..7b7c909bd 100755
--- a/examples/run_reward_curve_verification_taiji.sh
+++ b/examples/run_reward_curve_verification_taiji.sh
@@ -224,8 +224,8 @@ else
require_torch_flavor "2.11.0+cu129"
fi
if [ "${PROFILE}" = "sd3-vllm-omni" ] || [ "${PROFILE}" = "qwen-omni" ]; then
- require_dist_version "vllm" "0.20.0"
- require_dist_version "vllm-omni" "0.20.0"
+ require_dist_version "vllm" "0.22.0+cu129"
+ require_dist_version "vllm-omni" "0.22.0"
fi
WANDB_OVERRIDES=(
diff --git a/pyproject.toml b/pyproject.toml
index 709e417e1..49aea3507 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -73,10 +73,13 @@ sglang = [
"flash-linear-attention[conv1d]==0.5.1 ; sys_platform == 'linux'",
"av>=14.2,<19 ; sys_platform == 'linux'",
]
-# CUDA 12.9 stack: vllm 0.20.0 PyPI wheels are compiled against CUDA 12.9.
+# CUDA 12.9 stack. vLLM-Omni 0.22 is the first stable release with the
+# SenseNova-U1 pixel-head pipeline used by the U1.5 rollout recipe. Select the
+# release's explicit +cu129 wheels: the plain PyPI wheel links CUDA 13 even
+# when resolution also selects torch+cu129.
vllm = [
- "vllm==0.20.0 ; sys_platform == 'linux'",
- "vllm-omni==0.20.0 ; sys_platform == 'linux'",
+ "vllm @ https://github.com/vllm-project/vllm/releases/download/v0.22.0/vllm-0.22.0%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl ; sys_platform == 'linux' and platform_machine == 'x86_64'",
+ "vllm-omni==0.22.0 ; sys_platform == 'linux'",
"torch==2.11.0+cu129 ; sys_platform == 'linux'",
"torchvision==0.26.0+cu129 ; sys_platform == 'linux'",
"torchaudio==2.11.0+cu129 ; sys_platform == 'linux'",
@@ -165,13 +168,9 @@ environments = [
"sys_platform == 'linux' and platform_machine == 'x86_64'",
]
-# vllm's PyPI wheel is manylinux_2_35 and its GitHub +cu129 wheel manylinux_2_31;
-# the fleet pods are glibc 2.28, so vllm must build from sdist on-pod. Its
-# build-requires omits numpy (CMake configure fails), and the build-env torch
-# must match the runtime +cu129 flavor or the compiled kernels link CUDA 13 libs.
-# First build is slow (or export VLLM_USE_PRECOMPILED=1); uv caches it per pod.
-# (Plain-string pin instead of match-runtime: bump together with the torch
-# anchor in the vllm extra above.)
+# Source-build fallback: vLLM's build-requires omits numpy, and the build-env
+# torch must match the runtime +cu129 flavor. The v0.22 direct wheels above are
+# manylinux_2_28 and normally avoid this path on the supported fleet arches.
[tool.uv.extra-build-dependencies]
vllm = [
"numpy",
@@ -209,7 +208,11 @@ target-version = "py312"
# upstream model code carries
# semantically-meaningful trailing whitespace (VideoAlign prompt templates) that
# must not be auto-stripped.
-extend-exclude = ["unirl-reward-service", "unirl/models/bagel/vendor"]
+extend-exclude = [
+ "unirl-reward-service",
+ "unirl/models/bagel/vendor",
+ "unirl/models/sensenova_u1/vendor",
+]
[tool.ruff.lint]
select = ["E", "F", "W", "I"]
diff --git a/tests/models/test_sensenova_u1.py b/tests/models/test_sensenova_u1.py
new file mode 100644
index 000000000..dc2f4d9dd
--- /dev/null
+++ b/tests/models/test_sensenova_u1.py
@@ -0,0 +1,222 @@
+"""Focused CPU tests for SenseNova-U1.5 geometry and flow conventions."""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+
+import torch
+
+from unirl.models.sensenova_u1.conditions import SenseNovaU1Conditions
+from unirl.models.sensenova_u1.config import SenseNovaU1PipelineConfig
+from unirl.models.sensenova_u1.diffusion import (
+ SenseNovaU1DiffusionParams,
+ SenseNovaU1DiffusionStep,
+ resolve_noise_scale,
+)
+from unirl.models.sensenova_u1.pipeline import SenseNovaU1Pipeline
+from unirl.models.sensenova_u1.pixels import packed_pixel_shape, patchify_pixels, unpatchify_pixels
+from unirl.sde.kernels import FlowSDEStrategy
+from unirl.sde.runtime import FlowMatchSchedulePolicy
+
+
+def test_pixel_pack_roundtrip() -> None:
+ pixels = torch.arange(3 * 64 * 96, dtype=torch.float32).reshape(1, 3, 64, 96)
+ packed = patchify_pixels(pixels, patch_size=32)
+
+ assert packed.shape == (1, *packed_pixel_shape((64, 96), patch_size=32))
+ torch.testing.assert_close(
+ unpatchify_pixels(packed, image_shape=(64, 96), patch_size=32),
+ pixels,
+ )
+
+
+def test_driver_noise_uses_upstream_nchw_layout() -> None:
+ sampling = SimpleNamespace(height=512, width=768)
+
+ assert SenseNovaU1Pipeline.latent_shape(model_config=None, sampling_spec=sampling) == (3, 512, 768)
+
+
+def test_data_time_velocity_is_negated_for_sigma_solver() -> None:
+ class ConstantVelocityStep(SenseNovaU1DiffusionStep):
+ def predict_velocity(self, *args, sample, **kwargs):
+ return torch.full_like(sample, 2.0)
+
+ state = torch.zeros(1, 2, 3, dtype=torch.bfloat16)
+ next_state, log_prob, _ = ConstantVelocityStep().step_with_logp(
+ None,
+ None,
+ strategy=FlowSDEStrategy(),
+ sample=state,
+ sigma=torch.tensor(1.0),
+ sigma_next=torch.tensor(0.5),
+ params=None,
+ eta=0.0,
+ )
+
+ # Upstream integrates dx/dt=2 over dt=0.5. The framework integrates over
+ # decreasing sigma, so its noise prediction must be dx/dsigma=-2.
+ torch.testing.assert_close(next_state, torch.ones_like(state))
+ assert next_state.dtype == torch.bfloat16
+ assert log_prob is None
+
+
+def test_resolution_dependent_noise_scale() -> None:
+ model = SimpleNamespace(
+ patch_size=16,
+ downsample_ratio=0.5,
+ noise_scale=1.0,
+ noise_scale_mode="resolution",
+ noise_scale_base_image_seq_len=64,
+ noise_scale_max_value=16.0,
+ )
+
+ assert resolve_noise_scale(model, (256, 256)) == 1.0
+ assert resolve_noise_scale(model, (512, 512)) == 2.0
+ assert resolve_noise_scale(model, (1024, 1024)) == 4.0
+
+
+def test_shifted_schedule_matches_upstream_data_time() -> None:
+ steps = 20
+ shift = 3.0
+ sigmas = FlowMatchSchedulePolicy.static_only(shift).compute_sigma(
+ num_inference_steps=steps,
+ height=512,
+ width=512,
+ )
+ upstream_time = torch.linspace(0.0, 1.0, steps + 1)
+ upstream_sigma = 1.0 - upstream_time
+ upstream_sigma = shift * upstream_sigma / (1.0 + (shift - 1.0) * upstream_sigma)
+
+ torch.testing.assert_close(sigmas, upstream_sigma)
+
+
+def test_stochastic_transition_uses_unit_noise_coordinates() -> None:
+ class ZeroVelocityStep(SenseNovaU1DiffusionStep):
+ def predict_velocity(self, *args, sample, **kwargs):
+ return torch.zeros_like(sample)
+
+ model = SimpleNamespace(
+ patch_size=16,
+ downsample_ratio=0.5,
+ noise_scale=1.0,
+ noise_scale_mode="resolution",
+ noise_scale_base_image_seq_len=64,
+ noise_scale_max_value=16.0,
+ )
+ bundle = SimpleNamespace(model=model)
+ conditions = SimpleNamespace(image_shapes=[(512, 512)])
+ strategy = FlowSDEStrategy()
+ sample = torch.full((1, 2, 3), 2.0)
+ previous = torch.full_like(sample, 1.5)
+ sigma = torch.tensor(0.8)
+ sigma_next = torch.tensor(0.6)
+
+ actual, actual_logp, actual_mean = ZeroVelocityStep().step_with_logp(
+ bundle,
+ conditions,
+ strategy=strategy,
+ sample=sample,
+ sigma=sigma,
+ sigma_next=sigma_next,
+ params=None,
+ prev_sample=previous,
+ sigma_max=0.7,
+ eta=0.5,
+ )
+ expected, expected_logp, expected_mean = strategy.denoise(
+ noise_pred=torch.zeros_like(sample),
+ sample=sample / 2.0,
+ sigma=sigma,
+ sigma_next=sigma_next,
+ prev_sample=previous / 2.0,
+ sigma_max=0.7,
+ eta=0.5,
+ )
+
+ torch.testing.assert_close(actual, expected * 2.0)
+ torch.testing.assert_close(actual_mean, expected_mean * 2.0)
+ torch.testing.assert_close(actual_logp, expected_logp)
+
+
+def test_condition_cache_moves_with_indexes() -> None:
+ cache = SimpleNamespace(
+ layers=[
+ SimpleNamespace(
+ keys=torch.ones(1),
+ values=torch.zeros(1),
+ )
+ ]
+ )
+ indexes = torch.zeros(3, 1, dtype=torch.long)
+ conditions = SenseNovaU1Conditions(
+ prompts=["prompt", "prompt"],
+ condition_caches=[cache, cache],
+ uncondition_caches=[None, None],
+ condition_image_indexes=[indexes, indexes],
+ uncondition_image_indexes=[None, None],
+ image_shapes=[(32, 32), (32, 32)],
+ )
+
+ moved = conditions.to_device("meta")
+
+ assert moved.condition_caches[0].layers[0].keys.device.type == "meta"
+ assert moved.condition_caches[0].layers[0].values.device.type == "meta"
+ assert moved.condition_caches[0] is moved.condition_caches[1]
+ assert moved.condition_image_indexes[0].device.type == "meta"
+ assert cache.layers[0].keys.device.type == "cpu"
+
+
+def test_sensenova_defaults_match_official_precision() -> None:
+ config = SenseNovaU1PipelineConfig(pretrained_model_ckpt_path="unused")
+ params = SenseNovaU1DiffusionParams()
+
+ assert config.trajectory_precision == "bf16"
+ assert params.trajectory_precision == "bf16"
+ assert params.logprob_precision == "fp32"
+
+
+def test_cfg_combinations_match_upstream_formulas() -> None:
+ step = SenseNovaU1DiffusionStep()
+ condition = torch.tensor([[[2.0, 0.0], [0.0, 1.0]]])
+ uncondition = torch.tensor([[[0.5, 0.5], [0.5, 0.5]]])
+ classical = uncondition + 4.0 * (condition - uncondition)
+
+ none = step._apply_cfg(
+ condition,
+ uncondition,
+ guidance=4.0,
+ cfg_norm="none",
+ step_index=1,
+ )
+ global_norm = step._apply_cfg(
+ condition,
+ uncondition,
+ guidance=4.0,
+ cfg_norm="global",
+ step_index=1,
+ )
+ channel_norm = step._apply_cfg(
+ condition,
+ uncondition,
+ guidance=4.0,
+ cfg_norm="channel",
+ step_index=1,
+ )
+ zero_first = step._apply_cfg(
+ condition,
+ uncondition,
+ guidance=4.0,
+ cfg_norm="cfg_zero_star",
+ step_index=0,
+ )
+ expected_global = classical * (
+ torch.norm(condition, dim=(1, 2), keepdim=True) / (torch.norm(classical, dim=(1, 2), keepdim=True) + 1e-8)
+ ).clamp(0.0, 1.0)
+ expected_channel = classical * (
+ torch.norm(condition, dim=-1, keepdim=True) / (torch.norm(classical, dim=-1, keepdim=True) + 1e-8)
+ ).clamp(0.0, 1.0)
+
+ torch.testing.assert_close(none, classical)
+ torch.testing.assert_close(global_norm, expected_global)
+ torch.testing.assert_close(channel_norm, expected_channel)
+ torch.testing.assert_close(zero_first, torch.zeros_like(condition))
diff --git a/tests/rollout/test_sensenova_vllm_omni.py b/tests/rollout/test_sensenova_vllm_omni.py
new file mode 100644
index 000000000..9db643de0
--- /dev/null
+++ b/tests/rollout/test_sensenova_vllm_omni.py
@@ -0,0 +1,164 @@
+"""CPU contracts for the SenseNova vLLM-Omni driver adapter."""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+
+import torch
+from PIL import Image
+
+from unirl.models.sensenova_u1.conditions import SenseNovaU1Conditions
+from unirl.models.sensenova_u1.diffusion import SenseNovaU1DiffusionParams
+from unirl.rollout.engine.vllm_omni.adapters.sensenova_u1 import (
+ SenseNovaU1OutputAdapter,
+ SenseNovaU1T2IAdapter,
+)
+from unirl.rollout.engine.vllm_omni.pipelines.sensenova_u1.weight_names import (
+ missing_weight_sync_names,
+)
+from unirl.types.primitives import Texts
+from unirl.types.sample import Part, Sample
+
+
+def _request_sample() -> Sample:
+ root = Part.input(
+ ["prompt-a", "prompt-b"],
+ primitives={"text": Texts(texts=["a lighthouse", "a mountain"])},
+ )
+ params = SenseNovaU1DiffusionParams(
+ num_inference_steps=4,
+ guidance_scale=3.5,
+ cfg_norm="channel",
+ cfg_interval=(0.1, 0.9),
+ t_eps=0.03,
+ height=32,
+ width=64,
+ eta=0.7,
+ samples_per_prompt=2,
+ seed=7,
+ sde_indices=[1, 3],
+ sigmas=torch.tensor([1.0, 0.8, 0.5, 0.2, 0.0]),
+ init_noise_latent_shape=[3, 32, 64],
+ )
+ return Sample.request(root).fork(2, sampling_params=params)
+
+
+def test_sensenova_adapter_groups_prompt_fanout_and_forwards_flow_controls() -> None:
+ model_config = SimpleNamespace(timestep_shift=3.0)
+ adapter = SenseNovaU1T2IAdapter(
+ SimpleNamespace(model_path="unused"),
+ model_config,
+ )
+
+ calls = adapter.build_inputs(_request_sample())
+
+ assert len(calls) == 2
+ assert calls[0].prompts == [{"prompt": "a lighthouse"}]
+ assert calls[1].prompts == [{"prompt": "a mountain"}]
+ kwargs = calls[0].sampling[0].kwargs
+ assert kwargs["num_outputs_per_prompt"] == 2
+ torch.testing.assert_close(
+ torch.tensor(kwargs["sigmas"]),
+ torch.tensor([1.0, 0.8, 0.5, 0.2]),
+ )
+ assert kwargs["guidance_scale"] == 3.5
+ assert kwargs["eta"] == 0.7
+ assert kwargs["extra_args"]["batch_size"] == 2
+ assert kwargs["extra_args"]["cfg_scale"] == 3.5
+ assert kwargs["extra_args"]["cfg_norm"] == "channel"
+ assert kwargs["extra_args"]["cfg_interval"] == [0.1, 0.9]
+ assert kwargs["extra_args"]["timestep_shift"] == 3.0
+ assert kwargs["extra_args"]["t_eps"] == 0.03
+ assert kwargs["extra_args"]["sde_indices"] == [1, 3]
+ torch.testing.assert_close(
+ torch.tensor(kwargs["extra_args"]["unirl_sigmas"]),
+ torch.tensor([1.0, 0.8, 0.5, 0.2, 0.0]),
+ )
+ assert kwargs["extra_args"]["init_noise_group_ids"] == [
+ "prompt-a/0",
+ "prompt-a/1",
+ ]
+ assert kwargs["extra_args"]["sde_seed"] == 7
+ assert calls[1].sampling[0].kwargs["extra_args"]["init_noise_group_ids"] == [
+ "prompt-b/0",
+ "prompt-b/1",
+ ]
+ assert calls[1].sampling[0].kwargs["extra_args"]["sde_seed"] == 1_000_010
+
+
+def test_sensenova_adapter_uses_checkpoint_time_shift_for_schedule() -> None:
+ adapter = SenseNovaU1T2IAdapter(
+ SimpleNamespace(model_path="unused"),
+ SimpleNamespace(timestep_shift=3.0),
+ )
+
+ actual = adapter.schedule_policy().compute_sigma(
+ num_inference_steps=4,
+ height=32,
+ width=64,
+ )
+ raw = torch.linspace(1.0, 0.0, 5)
+ expected = 3.0 * raw / (1.0 + 2.0 * raw)
+
+ torch.testing.assert_close(actual, expected)
+
+
+def test_sensenova_output_adapter_flattens_grouped_images_and_prefix_caches() -> None:
+ sample = _request_sample()
+ params = sample.frontier_gen_part(SenseNovaU1DiffusionParams).sampling_params
+
+ def result(prompt: str):
+ cache = SimpleNamespace(layers=[SimpleNamespace(keys=torch.ones(1), values=torch.zeros(1))])
+ capture = {
+ "prompts": [prompt, prompt],
+ "condition_caches": [cache, cache],
+ "uncondition_caches": [cache, cache],
+ "condition_image_indexes": [torch.zeros(3, 1, dtype=torch.long)] * 2,
+ "uncondition_image_indexes": [torch.zeros(3, 1, dtype=torch.long)] * 2,
+ "image_shapes": [(32, 64)] * 2,
+ }
+ return SimpleNamespace(
+ final_output_type="image",
+ stage_id=0,
+ images=[Image.new("RGB", (64, 32)), Image.new("RGB", (64, 32))],
+ trajectory_latents=torch.zeros(2, 4, 1, 1),
+ trajectory_timesteps=params.sigmas,
+ trajectory_log_probs=torch.zeros(2, 2),
+ custom_output={
+ "sde_step_indices": [1, 3],
+ "trajectory_indices": [1, 2, 3, 4],
+ "sensenova_u1_capture": capture,
+ },
+ )
+
+ output = SenseNovaU1OutputAdapter("sensenova_u1_t2i").build(
+ sample,
+ [[result("a lighthouse")], [result("a mountain")]],
+ )
+
+ frontier = output.frontier_gen_part(SenseNovaU1DiffusionParams)
+ conditions = SenseNovaU1Conditions.from_dict(frontier.conditions)
+ assert conditions.prompts == ["a lighthouse", "a lighthouse", "a mountain", "a mountain"]
+ assert conditions.image_shapes == [(32, 64)] * 4
+ assert len(frontier.primitives["image"]) == 4
+ assert frontier.segment.latents.shape[0] == 4
+ assert frontier.segment.indices.tolist() == [1, 2, 3, 4]
+
+
+def test_sensenova_weight_sync_names_cover_fused_worker_layout() -> None:
+ parameter_names = {
+ "language_model.model.layers.0.self_attn.qkv_proj_mot_gen.weight",
+ "language_model.model.layers.0.mlp_mot_gen.gate_up_proj.weight",
+ "fm_modules.fm_head.conv1.weight",
+ }
+ incoming = [
+ "language_model.model.layers.0.self_attn.q_proj_mot_gen.weight",
+ "language_model.model.layers.0.self_attn.k_proj_mot_gen.weight",
+ "language_model.model.layers.0.self_attn.v_proj_mot_gen.weight",
+ "language_model.model.layers.0.mlp_mot_gen.gate_proj.weight",
+ "language_model.model.layers.0.mlp_mot_gen.up_proj.weight",
+ "fm_modules.fm_head.conv1.weight",
+ ]
+
+ assert missing_weight_sync_names(incoming, parameter_names) == []
+ assert missing_weight_sync_names([*incoming, "unknown.weight"], parameter_names) == ["unknown.weight"]
diff --git a/unirl/models/sensenova_u1/__init__.py b/unirl/models/sensenova_u1/__init__.py
new file mode 100644
index 000000000..b67d5e9f4
--- /dev/null
+++ b/unirl/models/sensenova_u1/__init__.py
@@ -0,0 +1,22 @@
+"""SenseNova-U1.5 NEO-Unify pixel-flow support."""
+
+from .bundle import SenseNovaU1Bundle
+from .conditions import SenseNovaU1Conditions
+from .config import SENSENOVA_U1_GEN_LORA_TARGETS, SenseNovaU1PipelineConfig
+from .diffusion import (
+ SenseNovaU1DiffusionParams,
+ SenseNovaU1DiffusionStage,
+ SenseNovaU1DiffusionStep,
+)
+from .pipeline import SenseNovaU1Pipeline
+
+__all__ = [
+ "SENSENOVA_U1_GEN_LORA_TARGETS",
+ "SenseNovaU1Bundle",
+ "SenseNovaU1Conditions",
+ "SenseNovaU1DiffusionParams",
+ "SenseNovaU1DiffusionStage",
+ "SenseNovaU1DiffusionStep",
+ "SenseNovaU1Pipeline",
+ "SenseNovaU1PipelineConfig",
+]
diff --git a/unirl/models/sensenova_u1/bundle.py b/unirl/models/sensenova_u1/bundle.py
new file mode 100644
index 000000000..9da1ae4ec
--- /dev/null
+++ b/unirl/models/sensenova_u1/bundle.py
@@ -0,0 +1,233 @@
+"""SenseNova-U1.5 model and tokenizer bundle."""
+
+from __future__ import annotations
+
+import logging
+import os
+from typing import Any, Optional
+
+import torch
+import torch.nn as nn
+
+from unirl.models.types.bundle import Bundle
+from unirl.utils.dtypes import parse_torch_dtype
+
+from .config import SenseNovaU1PipelineConfig
+from .vendor.neo_unify import register as register_neo_unify
+from .vendor.neo_unify import set_attn_backend
+from .vendor.neo_unify.transformers_compat import pretrained_dtype_kwargs
+
+logger = logging.getLogger(__name__)
+
+
+def _set_generation_trainability(model: nn.Module, *, enabled: bool) -> int:
+ """Freeze the shared/understanding path and optionally unfreeze the image branch."""
+ model.requires_grad_(False)
+ if not enabled:
+ return 0
+
+ trainable = 0
+ for name, parameter in model.named_parameters():
+ if name.startswith("fm_modules.") or "_mot_gen" in name:
+ parameter.requires_grad_(True)
+ trainable += parameter.numel()
+ if trainable == 0:
+ raise RuntimeError(
+ "SenseNovaU1Bundle: no generation parameters matched `fm_modules.*` "
+ "or `*_mot_gen`; the vendored model/checkpoint layout is incompatible."
+ )
+ return trainable
+
+
+class SenseNovaU1TrainableModel(nn.Module):
+ """FSDP-callable facade around NEOChatModel's inference-only helper API."""
+
+ def __init__(self, model: nn.Module) -> None:
+ super().__init__()
+ # NEOChatModel.forward is deliberately unimplemented upstream. Keeping
+ # helper calls behind this real forward is still mandatory: the wrapper's
+ # root FSDP hooks gather shared embeddings and sibling fm_modules.
+ self.model = model
+
+ def forward(self, mode: str, **kwargs: Any) -> Any:
+ if mode == "prefix":
+ cache, _ = self.model._t2i_prefix_forward(
+ kwargs["input_ids"],
+ kwargs["indexes"],
+ kwargs["attention_mask"],
+ )
+ return cache
+ if mode == "predict_velocity":
+ return self._predict_velocity(**kwargs)
+ raise ValueError(f"SenseNovaU1TrainableModel.forward: unsupported mode {mode!r}.")
+
+ def _predict_velocity(
+ self,
+ *,
+ normalized_pixels: torch.Tensor,
+ packed_pixels: torch.Tensor,
+ image_indexes: torch.Tensor,
+ prefix_cache: Any,
+ data_time: torch.Tensor,
+ image_shape: tuple[int, int],
+ noise_scale: float,
+ uncondition_image_indexes: Optional[torch.Tensor] = None,
+ uncondition_prefix_cache: Any = None,
+ ) -> Any:
+ model = self.model
+ height, width = (int(v) for v in image_shape)
+ patch = int(model.patch_size)
+ merge = int(1 / float(model.downsample_ratio))
+ grid_h, grid_w = height // patch, width // patch
+ image_tokens = (height // (patch * merge)) * (width // (patch * merge))
+ device = normalized_pixels.device
+
+ grid_hw = torch.tensor([[grid_h, grid_w]], dtype=torch.long, device=device)
+ vision_patches = model.patchify(normalized_pixels, patch, channel_first=True)
+ image_embeds = model.extract_feature(
+ vision_patches.reshape(grid_h * grid_w, -1),
+ gen_model=True,
+ grid_hw=grid_hw,
+ ).reshape(1, image_tokens, -1)
+
+ timestep = data_time.expand(image_tokens)
+ timestep_embeddings = model.fm_modules["timestep_embedder"](timestep).reshape(1, image_tokens, -1)
+ if bool(model.add_noise_scale_embedding):
+ noise_value = torch.full_like(timestep, float(noise_scale) / float(model.noise_scale_max_value))
+ timestep_embeddings = timestep_embeddings + model.fm_modules["noise_scale_embedder"](noise_value).reshape(
+ 1, image_tokens, -1
+ )
+ image_embeds = image_embeds + timestep_embeddings
+
+ condition_velocity = model._t2i_predict_v(
+ image_embeds,
+ image_indexes,
+ {"full_attention": None},
+ prefix_cache,
+ data_time,
+ packed_pixels,
+ image_token_num=image_tokens,
+ timestep_embeddings=timestep_embeddings,
+ image_size=(width, height),
+ )
+ if uncondition_prefix_cache is None:
+ return condition_velocity
+ uncondition_velocity = model._t2i_predict_v(
+ image_embeds,
+ uncondition_image_indexes,
+ {"full_attention": None},
+ uncondition_prefix_cache,
+ data_time,
+ packed_pixels,
+ image_token_num=image_tokens,
+ timestep_embeddings=timestep_embeddings,
+ image_size=(width, height),
+ )
+ return condition_velocity, uncondition_velocity
+
+
+class SenseNovaU1Bundle(Bundle):
+ """SenseNova-U1.5 NEO-Unify backbone plus its Qwen tokenizer."""
+
+ def __init__(
+ self,
+ *,
+ model: nn.Module,
+ tokenizer: Any,
+ dtype: torch.dtype,
+ device: torch.device,
+ pretrained_path: str,
+ config: Optional[SenseNovaU1PipelineConfig] = None,
+ ) -> None:
+ super().__init__()
+ self.config = config
+ self.model = model
+ self.transformer = SenseNovaU1TrainableModel(model)
+ self.tokenizer = tokenizer
+ self.dtype = dtype
+ self.device = device
+ self.pretrained_path = pretrained_path
+
+ @classmethod
+ def from_config(cls, config: SenseNovaU1PipelineConfig) -> "SenseNovaU1Bundle":
+ """Load a local or Hub-format SenseNova-U1 checkpoint."""
+ import fcntl
+
+ serialize = os.environ.get("DIFFRL_MODEL_LOAD_SERIALIZE", "1") != "0"
+ lock_file = open("/tmp/diffrl_model_load.lock", "a+") if serialize else None
+ if lock_file is not None:
+ fcntl.flock(lock_file, fcntl.LOCK_EX)
+ try:
+ return cls._from_config_locked(config)
+ finally:
+ if lock_file is not None:
+ fcntl.flock(lock_file, fcntl.LOCK_UN)
+ lock_file.close()
+
+ @classmethod
+ def _from_config_locked(cls, config: SenseNovaU1PipelineConfig) -> "SenseNovaU1Bundle":
+ import transformers
+ from packaging.version import Version
+ from transformers import AutoConfig, AutoModel, AutoTokenizer
+
+ register_neo_unify()
+ set_attn_backend(config.attention_backend)
+
+ path = config.pretrained_model_ckpt_path
+ device = config.device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
+ if isinstance(device, str):
+ device = torch.device(device)
+ dtype = parse_torch_dtype(config.model_precision, field_name="model_precision")
+
+ hf_config = AutoConfig.from_pretrained(path)
+ if getattr(hf_config, "model_type", None) != "neo_chat":
+ raise ValueError(
+ f"SenseNovaU1Bundle expected model_type='neo_chat' at {path!r}, "
+ f"got {getattr(hf_config, 'model_type', None)!r}."
+ )
+ if not bool(getattr(hf_config, "use_pixel_head", False)):
+ raise ValueError(
+ "SenseNovaU1Bundle currently supports the U1.5 pixel-head checkpoint; "
+ "config.use_pixel_head must be true."
+ )
+
+ dtype_kwargs = pretrained_dtype_kwargs(dtype)
+ if Version(transformers.__version__) < Version("4.57"):
+ # Transformers 4.56 still forwards the newer `dtype` kwarg into the
+ # model constructor instead of consuming it in from_pretrained.
+ dtype_kwargs = {"torch_dtype": dtype}
+ model = AutoModel.from_pretrained(
+ path,
+ config=hf_config,
+ **dtype_kwargs,
+ ).to(device)
+ tokenizer = AutoTokenizer.from_pretrained(path)
+
+ trainable = _set_generation_trainability(
+ model,
+ enabled=bool(config.full_finetune_generation),
+ )
+ model.eval()
+ logger.info(
+ "Loaded SenseNova-U1 from %s with %d generation-path trainable parameters.",
+ path,
+ trainable,
+ )
+ return cls(
+ model=model,
+ tokenizer=tokenizer,
+ dtype=dtype,
+ device=device,
+ pretrained_path=path,
+ config=config,
+ )
+
+ def trainable_module(self) -> nn.Module:
+ """Return the complete wrapper containing every generation-path module."""
+ return self.transformer
+
+
+__all__ = [
+ "SenseNovaU1Bundle",
+ "SenseNovaU1TrainableModel",
+]
diff --git a/unirl/models/sensenova_u1/conditions.py b/unirl/models/sensenova_u1/conditions.py
new file mode 100644
index 000000000..9f21a94f6
--- /dev/null
+++ b/unirl/models/sensenova_u1/conditions.py
@@ -0,0 +1,124 @@
+"""Typed prompt-cache conditions for SenseNova-U1.5 pixel diffusion."""
+
+from __future__ import annotations
+
+import copy
+from dataclasses import dataclass
+from typing import Any, ClassVar, Dict, List, Tuple
+
+import torch
+
+from unirl.config.require import require
+from unirl.distributed.tensor.batch import concat_field
+from unirl.types.conditions.base import Condition, Modality
+
+
+def _cache_tensors(cache: Any):
+ """Yield tensor attributes stored by Transformers cache layers."""
+ for layer in getattr(cache, "layers", ()):
+ for name in ("keys", "values", "flash_k_cache", "flash_v_cache"):
+ value = getattr(layer, name, None)
+ if isinstance(value, torch.Tensor):
+ yield layer, name, value
+
+
+def _move_cache(cache: Any, device: str | torch.device) -> Any:
+ """Move a Transformers cache without aliasing the source across devices."""
+ if cache is None:
+ return None
+ target = torch.device(device)
+ tensors = list(_cache_tensors(cache))
+ if all(value.device == target for _, _, value in tensors):
+ return cache
+ moved = copy.deepcopy(cache)
+ for layer, name, value in _cache_tensors(moved):
+ setattr(layer, name, value.to(target))
+ return moved
+
+
+def _move_shared_caches(caches: List[Any], device: str | torch.device) -> List[Any]:
+ """Move each distinct cache once while preserving same-prompt aliases."""
+ memo: Dict[int, Any] = {}
+ moved: List[Any] = []
+ for cache in caches:
+ if cache is None:
+ moved.append(None)
+ continue
+ key = id(cache)
+ if key not in memo:
+ memo[key] = _move_cache(cache, device)
+ moved.append(memo[key])
+ return moved
+
+
+@dataclass
+class SenseNovaU1Conditions(Condition):
+ """Frozen text-prefix caches and spatial metadata, one entry per sample."""
+
+ modality: ClassVar[Modality] = Modality.IMAGE
+
+ prompts: List[str] = concat_field(default_factory=list)
+ condition_caches: List[Any] = concat_field(default_factory=list)
+ uncondition_caches: List[Any] = concat_field(default_factory=list)
+ condition_image_indexes: List[Any] = concat_field(default_factory=list)
+ uncondition_image_indexes: List[Any] = concat_field(default_factory=list)
+ image_shapes: List[Tuple[int, int]] = concat_field(default_factory=list)
+
+ @property
+ def batch_size(self) -> int:
+ return len(self.prompts)
+
+ def validate(self) -> None:
+ """Require all per-sample fields to remain aligned after batching/slicing."""
+ n = self.batch_size
+ for name in (
+ "condition_caches",
+ "uncondition_caches",
+ "condition_image_indexes",
+ "uncondition_image_indexes",
+ "image_shapes",
+ ):
+ values = getattr(self, name)
+ require(
+ len(values) == n,
+ f"SenseNovaU1Conditions.{name} has {len(values)} entries for batch_size={n}.",
+ )
+
+ def single(self, index: int = 0) -> Tuple[str, Any, Any, Any, Any, Tuple[int, int]]:
+ """Return one sample's prompt, caches, indexes, and ``(H, W)`` shape."""
+ self.validate()
+ require(0 <= index < self.batch_size, f"SenseNovaU1Conditions.single: index {index} is out of range.")
+ return (
+ self.prompts[index],
+ self.condition_caches[index],
+ self.uncondition_caches[index],
+ self.condition_image_indexes[index],
+ self.uncondition_image_indexes[index],
+ tuple(self.image_shapes[index]),
+ )
+
+ @classmethod
+ def from_dict(cls, values: Dict[str, Any]) -> "SenseNovaU1Conditions":
+ """Read conditions from a generated Part."""
+ conditions = values.get("sensenova_u1")
+ if not isinstance(conditions, cls):
+ raise TypeError(
+ "SenseNovaU1Conditions.from_dict expected values['sensenova_u1'] "
+ f"to be SenseNovaU1Conditions, got {type(conditions).__name__}."
+ )
+ return conditions
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Write conditions into a generated Part."""
+ self.validate()
+ return {"sensenova_u1": self}
+
+ def to_device(self, device: str | torch.device) -> "SenseNovaU1Conditions":
+ """Move image indexes and opaque prefix KV caches together."""
+ moved = super().to_device(device)
+ moved.condition_caches = _move_shared_caches(moved.condition_caches, device)
+ moved.uncondition_caches = _move_shared_caches(moved.uncondition_caches, device)
+ return moved
+
+
+__all__ = ["SenseNovaU1Conditions"]
diff --git a/unirl/models/sensenova_u1/config.py b/unirl/models/sensenova_u1/config.py
new file mode 100644
index 000000000..d1129438a
--- /dev/null
+++ b/unirl/models/sensenova_u1/config.py
@@ -0,0 +1,51 @@
+"""Construction config for SenseNova-U1.5."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Tuple
+
+from unirl.config.validation import validate_precision_type
+
+SENSENOVA_U1_GEN_LORA_TARGETS: Tuple[str, ...] = (
+ "self_attn.q_proj_mot_gen",
+ "self_attn.k_proj_mot_gen",
+ "self_attn.v_proj_mot_gen",
+ "self_attn.o_proj_mot_gen",
+ "mlp_mot_gen.gate_proj",
+ "mlp_mot_gen.up_proj",
+ "mlp_mot_gen.down_proj",
+)
+
+
+@dataclass
+class SenseNovaU1PipelineConfig:
+ """Construction args for the released SenseNova-U1.5 pixel-flow model."""
+
+ pretrained_model_ckpt_path: str
+ model_precision: Any = "bf16"
+ device: Any = None
+
+ autocast_precision: str = "bf16"
+ trajectory_precision: str = "bf16"
+ logprob_precision: str = "fp32"
+
+ timestep_shift: float = 3.0
+ attention_backend: str = "auto"
+ full_finetune_generation: bool = True
+
+ # FSDP wraps SenseNovaU1TrainableModel, whose NEOChatModel child is `model`.
+ weight_sync_param_name_prefix: str = "model."
+
+ def __post_init__(self) -> None:
+ validate_precision_type(self.model_precision, field="SenseNovaU1PipelineConfig.model_precision")
+ if self.attention_backend not in {"auto", "flash", "sdpa"}:
+ raise ValueError(
+ "SenseNovaU1PipelineConfig.attention_backend must be one of "
+ f"('auto', 'flash', 'sdpa'); got {self.attention_backend!r}."
+ )
+ if float(self.timestep_shift) <= 0:
+ raise ValueError(f"SenseNovaU1PipelineConfig.timestep_shift must be positive; got {self.timestep_shift}.")
+
+
+__all__ = ["SENSENOVA_U1_GEN_LORA_TARGETS", "SenseNovaU1PipelineConfig"]
diff --git a/unirl/models/sensenova_u1/diffusion.py b/unirl/models/sensenova_u1/diffusion.py
new file mode 100644
index 000000000..eeb1d2e66
--- /dev/null
+++ b/unirl/models/sensenova_u1/diffusion.py
@@ -0,0 +1,481 @@
+"""SenseNova-U1.5 packed-pixel flow sampling and replay."""
+
+from __future__ import annotations
+
+import math
+from contextlib import nullcontext
+from dataclasses import dataclass
+from typing import List, Optional, Set, Tuple
+
+import torch
+
+from unirl.config.require import require
+from unirl.models.types.diffusion import DiffusionStage
+from unirl.models.types.replay_result import ReplayResult
+from unirl.sde.kernels import FlowSDEStrategy, StepStrategy
+from unirl.types.sampling import DiffusionSamplingParams, compute_trajectory_positions
+from unirl.types.segments.latent import LatentSegment
+from unirl.utils.dtypes import parse_torch_dtype
+
+from .bundle import SenseNovaU1Bundle
+from .conditions import SenseNovaU1Conditions
+from .pixels import packed_pixel_shape, patchify_pixels, unpatchify_pixels
+
+CFG_NORM_TYPES = ("none", "global", "channel", "cfg_zero_star")
+
+
+@dataclass
+class SenseNovaU1DiffusionParams(DiffusionSamplingParams):
+ """Sampling knobs specific to the U1.5 pixel-flow head."""
+
+ num_inference_steps: int = 50
+ guidance_scale: float = 4.0
+ height: int = 512
+ width: int = 512
+ eta: float = 1.0
+ cfg_norm: str = "none"
+ cfg_interval: Tuple[float, float] = (0.0, 1.0)
+ t_eps: float = 0.02
+ trajectory_precision: str = "bf16"
+
+ def __post_init__(self) -> None:
+ super().__post_init__()
+ if not isinstance(self.cfg_interval, tuple):
+ self.cfg_interval = tuple(self.cfg_interval)
+ require(
+ len(self.cfg_interval) == 2 and 0.0 <= float(self.cfg_interval[0]) <= float(self.cfg_interval[1]) <= 1.0,
+ f"SenseNovaU1DiffusionParams.cfg_interval must lie within [0, 1]; got {self.cfg_interval!r}.",
+ )
+ require(
+ self.cfg_norm in CFG_NORM_TYPES,
+ f"SenseNovaU1DiffusionParams.cfg_norm must be one of {CFG_NORM_TYPES}; got {self.cfg_norm!r}.",
+ )
+ require(float(self.t_eps) > 0.0, f"SenseNovaU1DiffusionParams.t_eps must be positive; got {self.t_eps}.")
+
+
+def resolve_noise_scale(model: torch.nn.Module, image_shape: Tuple[int, int]) -> float:
+ """Match the checkpoint's resolution-dependent initial pixel-noise scale."""
+ height, width = (int(v) for v in image_shape)
+ patch = int(model.patch_size)
+ merge = int(1 / float(model.downsample_ratio))
+ grid_h, grid_w = height // patch, width // patch
+ scale = float(model.noise_scale)
+ if model.noise_scale_mode in {"resolution", "dynamic", "dynamic_sqrt"}:
+ image_sequence = (grid_h * grid_w) / (merge**2)
+ scale *= math.sqrt(image_sequence / float(model.noise_scale_base_image_seq_len))
+ if model.noise_scale_mode == "dynamic_sqrt":
+ scale = math.sqrt(scale)
+ return min(scale, float(model.noise_scale_max_value))
+
+
+class SenseNovaU1DiffusionStep:
+ """One model prediction plus a framework-owned FlowGRPO transition."""
+
+ @staticmethod
+ def _optimized_scale(positive: torch.Tensor, negative: torch.Tensor) -> torch.Tensor:
+ positive = positive.flatten(1).float()
+ negative = negative.flatten(1).float()
+ numerator = torch.sum(positive * negative, dim=1, keepdim=True)
+ denominator = torch.sum(negative.square(), dim=1, keepdim=True) + 1e-8
+ return numerator / denominator
+
+ def _apply_cfg(
+ self,
+ condition_velocity: torch.Tensor,
+ uncondition_velocity: torch.Tensor,
+ *,
+ guidance: float,
+ cfg_norm: str,
+ step_index: int,
+ ) -> torch.Tensor:
+ """Combine upstream conditional and unconditional velocity predictions."""
+ if cfg_norm == "cfg_zero_star":
+ if int(step_index) == 0:
+ return torch.zeros_like(condition_velocity)
+ alpha = self._optimized_scale(condition_velocity, uncondition_velocity).to(condition_velocity.dtype)
+ alpha = alpha.reshape(-1, 1, 1)
+ return uncondition_velocity * alpha + guidance * (condition_velocity - uncondition_velocity * alpha)
+
+ velocity = uncondition_velocity + guidance * (condition_velocity - uncondition_velocity)
+ if cfg_norm in {"global", "channel"}:
+ device_type = condition_velocity.device.type
+ if device_type == "mps":
+ device_type = "cpu"
+ norm_dims = (1, 2) if cfg_norm == "global" else -1
+ with torch.autocast(device_type=device_type, enabled=False):
+ condition_norm = torch.norm(condition_velocity, dim=norm_dims, keepdim=True)
+ guided_norm = torch.norm(velocity, dim=norm_dims, keepdim=True)
+ scale = (condition_norm / (guided_norm + 1e-8)).clamp(0.0, 1.0)
+ velocity = velocity * scale.to(velocity.dtype)
+ return velocity
+
+ def predict_velocity(
+ self,
+ bundle: SenseNovaU1Bundle,
+ conditions: SenseNovaU1Conditions,
+ *,
+ sample: torch.Tensor,
+ sigma: torch.Tensor,
+ params: SenseNovaU1DiffusionParams,
+ step_index: int,
+ ) -> torch.Tensor:
+ """Predict the upstream data-time velocity ``dx/dt`` for one sample."""
+ require(
+ conditions.batch_size == 1,
+ f"SenseNovaU1DiffusionStep expects one prompt/cache at a time; got {conditions.batch_size}.",
+ )
+ _, condition_cache, uncondition_cache, condition_indexes, uncondition_indexes, image_shape = conditions.single()
+ model = bundle.model
+ device = torch.device(bundle.device)
+ sample = sample.to(device)
+
+ pixel_patch = int(model.patch_size) * int(1 / float(model.downsample_ratio))
+ expected = packed_pixel_shape(image_shape, patch_size=pixel_patch)
+ require(
+ sample.ndim == 3 and tuple(sample.shape[1:]) == expected,
+ f"SenseNovaU1DiffusionStep expected packed pixels [B, {expected[0]}, {expected[1]}], "
+ f"got {tuple(sample.shape)}.",
+ )
+
+ normalized_pixels = unpatchify_pixels(sample, image_shape=image_shape, patch_size=pixel_patch)
+ sigma = sigma.to(device=device, dtype=torch.float32)
+ data_time = 1.0 - sigma
+ noise_scale = resolve_noise_scale(model, image_shape)
+ lo, hi = (float(v) for v in params.cfg_interval)
+ use_cfg = (
+ float(params.guidance_scale) > 1.0 and uncondition_cache is not None and lo <= float(data_time.item()) <= hi
+ )
+ prediction = bundle.transformer(
+ "predict_velocity",
+ normalized_pixels=normalized_pixels,
+ packed_pixels=sample,
+ image_indexes=condition_indexes,
+ prefix_cache=condition_cache,
+ data_time=data_time,
+ image_shape=image_shape,
+ noise_scale=noise_scale,
+ uncondition_image_indexes=uncondition_indexes if use_cfg else None,
+ uncondition_prefix_cache=uncondition_cache if use_cfg else None,
+ )
+ if not use_cfg:
+ return prediction
+ condition_velocity, uncondition_velocity = prediction
+ return self._apply_cfg(
+ condition_velocity,
+ uncondition_velocity,
+ guidance=float(params.guidance_scale),
+ cfg_norm=params.cfg_norm,
+ step_index=step_index,
+ )
+
+ def step_with_logp(
+ self,
+ bundle: SenseNovaU1Bundle,
+ conditions: SenseNovaU1Conditions,
+ *,
+ strategy: StepStrategy,
+ sample: torch.Tensor,
+ sigma: torch.Tensor,
+ sigma_next: torch.Tensor,
+ params: SenseNovaU1DiffusionParams,
+ prev_sample: Optional[torch.Tensor] = None,
+ sigma_max: float | torch.Tensor = 0.99,
+ eta: float = 1.0,
+ step_index: int = 0,
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
+ """Map upstream ``dx/dt`` to framework ``d x / d sigma = -dx/dt``."""
+ velocity = self.predict_velocity(
+ bundle,
+ conditions,
+ sample=sample,
+ sigma=sigma,
+ params=params,
+ step_index=step_index,
+ )
+ if float(eta) < 1e-7 and prev_sample is None and isinstance(strategy, FlowSDEStrategy):
+ # Match upstream t2i_generate exactly on deterministic steps: its
+ # Euler update runs in the trajectory dtype before the next model
+ # call. The generic FlowSDEStrategy promotes state and velocity to
+ # fp32 even when eta=0, which accumulates visible drift over 50
+ # BF16 inference steps.
+ data_time = 1.0 - sigma.to(device=sample.device)
+ next_data_time = 1.0 - sigma_next.to(device=sample.device)
+ next_sample = sample + (next_data_time - data_time) * velocity
+ return next_sample, None, None
+ noise_scale = resolve_noise_scale(bundle.model, tuple(conditions.image_shapes[0]))
+ unit_sample = sample / noise_scale
+ unit_velocity = velocity / noise_scale
+ unit_prev_sample = None if prev_sample is None else prev_sample / noise_scale
+ next_sample, log_prob, prev_mean = strategy.denoise(
+ noise_pred=-unit_velocity,
+ sample=unit_sample,
+ sigma=sigma,
+ sigma_next=sigma_next,
+ eta=float(eta),
+ prev_sample=unit_prev_sample,
+ sigma_max=float(sigma_max),
+ step_index=int(step_index),
+ )
+ return (
+ next_sample * noise_scale,
+ log_prob,
+ None if prev_mean is None else prev_mean * noise_scale,
+ )
+
+
+class SenseNovaU1DiffusionStage(DiffusionStage[SenseNovaU1Conditions]):
+ """Rollout and grad-capable replay over packed normalized RGB pixels."""
+
+ def __init__(
+ self,
+ *,
+ model: SenseNovaU1Bundle,
+ step: Optional[SenseNovaU1DiffusionStep] = None,
+ strategy: Optional[StepStrategy] = None,
+ autocast_precision: str = "bf16",
+ trajectory_precision: str = "bf16",
+ logprob_precision: str = "fp32",
+ ) -> None:
+ self.model = model
+ self.step = step if step is not None else SenseNovaU1DiffusionStep()
+ self.strategy = strategy if strategy is not None else FlowSDEStrategy()
+ self.autocast_dtype = parse_torch_dtype(autocast_precision, field_name="autocast_precision")
+ self.trajectory_dtype = parse_torch_dtype(trajectory_precision, field_name="trajectory_precision")
+ self.logprob_dtype = parse_torch_dtype(logprob_precision, field_name="logprob_precision")
+
+ def _autocast_ctx(self):
+ device = torch.device(self.model.device)
+ if device.type == "cuda" and self.autocast_dtype in (torch.float16, torch.bfloat16):
+ return torch.autocast("cuda", self.autocast_dtype)
+ if device.type == "cpu" and self.autocast_dtype == torch.bfloat16:
+ return torch.autocast("cpu", torch.bfloat16)
+ return nullcontext()
+
+ def _configure_t_eps(self, params: SenseNovaU1DiffusionParams) -> None:
+ """Apply the request-level inference clamp once before model forwards."""
+ self.model.model.config.t_eps = float(params.t_eps)
+
+ @staticmethod
+ def _single_conditions(conditions: SenseNovaU1Conditions, index: int) -> SenseNovaU1Conditions:
+ return conditions.slice(index, index + 1)
+
+ def _diffuse_one(
+ self,
+ conditions: SenseNovaU1Conditions,
+ *,
+ schedule: torch.Tensor,
+ params: SenseNovaU1DiffusionParams,
+ initial_latents: torch.Tensor,
+ ) -> LatentSegment:
+ device = torch.device(self.model.device)
+ schedule = schedule.to(device)
+ total_steps = int(params.num_inference_steps)
+ require(
+ int(schedule.shape[0]) == total_steps + 1,
+ f"SenseNovaU1DiffusionStage: schedule length {schedule.shape[0]} != {total_steps + 1}.",
+ )
+ self.strategy.init_schedule(schedule)
+ sigma_max = schedule[1].float() if total_steps else schedule[0].float()
+
+ image_shape = tuple(conditions.image_shapes[0])
+ noise_scale = resolve_noise_scale(self.model.model, image_shape)
+ state = initial_latents.to(device=device, dtype=self.trajectory_dtype) * noise_scale
+
+ sde_set: Set[int] = set(int(i) for i in (params.sde_indices or []))
+ require(
+ not sde_set or float(params.eta) > 0.0,
+ "SenseNovaU1DiffusionStage: sde_indices are non-empty but eta=0, "
+ "so rollout would emit no transition log-probabilities.",
+ )
+ needed = set(compute_trajectory_positions(sde_set, total_steps))
+ needed.add(total_steps)
+ stored: List[Tuple[int, torch.Tensor]] = []
+ if 0 in needed:
+ stored.append((0, state.detach().clone()))
+ log_probs: List[torch.Tensor] = []
+ means: List[torch.Tensor] = []
+
+ for index in range(total_steps):
+ eta = float(params.eta) if index in sde_set else 0.0
+ with torch.no_grad(), self._autocast_ctx():
+ state, log_prob, mean = self.step.step_with_logp(
+ self.model,
+ conditions,
+ strategy=self.strategy,
+ sample=state,
+ sigma=schedule[index],
+ sigma_next=schedule[index + 1],
+ params=params,
+ sigma_max=sigma_max,
+ eta=eta,
+ step_index=index,
+ )
+ state = state.to(dtype=self.trajectory_dtype)
+ if index + 1 in needed:
+ stored.append((index + 1, state.detach().clone()))
+ if log_prob is not None:
+ log_probs.append(log_prob.to(dtype=self.logprob_dtype))
+ if mean is not None:
+ means.append(mean.detach().to(dtype=self.trajectory_dtype))
+
+ return LatentSegment(
+ latents=torch.stack([value for _, value in stored], dim=1),
+ sigmas=schedule,
+ indices=torch.tensor([index for index, _ in stored], dtype=torch.long, device=device),
+ sde_logp=torch.stack(log_probs, dim=1) if log_probs else None,
+ sde_means=torch.stack(means, dim=1) if means else None,
+ sde_indices=(torch.tensor(sorted(sde_set), dtype=torch.long, device=device) if sde_set else None),
+ )
+
+ def diffuse(
+ self,
+ conditions: SenseNovaU1Conditions,
+ *,
+ schedule: torch.Tensor,
+ params: SenseNovaU1DiffusionParams,
+ initial_latents: Optional[torch.Tensor] = None,
+ ) -> LatentSegment:
+ """Sample each prompt independently to keep prefix caches batch-local."""
+ self._configure_t_eps(params)
+ conditions.validate()
+ device = torch.device(self.model.device)
+ pixel_patch = int(self.model.model.patch_size) * int(1 / float(self.model.model.downsample_ratio))
+ shapes = [packed_pixel_shape(shape, patch_size=pixel_patch) for shape in conditions.image_shapes]
+ require(
+ len(set(shapes)) == 1,
+ f"SenseNovaU1DiffusionStage requires one output shape per batch; got {conditions.image_shapes}.",
+ )
+ expected = shapes[0]
+ batch_size = conditions.batch_size
+
+ if initial_latents is None:
+ from unirl.sde.noise import generate_latents
+
+ initial_latents = generate_latents(
+ batch_size=batch_size,
+ latent_shape=(3, int(params.height), int(params.width)),
+ device=device,
+ dtype=self.trajectory_dtype,
+ init_same_noise=bool(params.init_same_noise),
+ samples_per_prompt=int(params.samples_per_prompt),
+ noise_group_ids=params.noise_group_ids,
+ base_seed=int(params.seed or 0),
+ )
+ spatial_shape = (batch_size, 3, int(params.height), int(params.width))
+ packed_shape = (batch_size, *expected)
+ if tuple(initial_latents.shape) == spatial_shape:
+ initial_latents = patchify_pixels(initial_latents, patch_size=pixel_patch)
+ elif tuple(initial_latents.shape) != packed_shape:
+ raise ValueError(
+ f"SenseNovaU1DiffusionStage initial_latents shape {tuple(initial_latents.shape)} "
+ f"must be spatial {spatial_shape} or packed {packed_shape}."
+ )
+
+ segments = [
+ self._diffuse_one(
+ self._single_conditions(conditions, index),
+ schedule=schedule,
+ params=params,
+ initial_latents=initial_latents[index : index + 1],
+ )
+ for index in range(batch_size)
+ ]
+ if len(segments) == 1:
+ return segments[0]
+ return LatentSegment.concat(segments)
+
+ def replay(
+ self,
+ conditions: SenseNovaU1Conditions,
+ *,
+ segment: LatentSegment,
+ params: SenseNovaU1DiffusionParams,
+ step_indices: Optional[List[int]] = None,
+ ) -> ReplayResult:
+ """Recompute SDE transition likelihoods with gradients."""
+ self._configure_t_eps(params)
+ if segment.sde_indices is None or segment.latents is None or segment.sigmas is None:
+ raise ValueError("SenseNovaU1DiffusionStage.replay requires segment SDE indices, latents, and sigmas.")
+ conditions.validate()
+ require(
+ int(segment.latents.shape[0]) == conditions.batch_size,
+ f"SenseNovaU1DiffusionStage.replay batch mismatch: latents={segment.latents.shape[0]}, "
+ f"conditions={conditions.batch_size}.",
+ )
+ sde_set = set(int(i) for i in segment.sde_indices.tolist())
+ target = [int(i) for i in step_indices] if step_indices is not None else sorted(sde_set)
+ invalid = [i for i in target if i not in sde_set]
+ require(not invalid, f"SenseNovaU1DiffusionStage.replay requested non-SDE steps {invalid}.")
+
+ device = torch.device(self.model.device)
+ schedule = segment.sigmas.to(device)
+ sigma_max = schedule[1].float()
+ batch_log_probs: List[torch.Tensor] = []
+ batch_means: List[torch.Tensor] = []
+ for batch_index in range(conditions.batch_size):
+ single = self._single_conditions(conditions, batch_index)
+ log_probs: List[torch.Tensor] = []
+ means: List[torch.Tensor] = []
+ for step_index in target:
+ state = segment.latents_at(step_index)[batch_index : batch_index + 1].to(device)
+ previous = segment.latents_at(step_index + 1)[batch_index : batch_index + 1].to(device)
+ with self._autocast_ctx():
+ _, log_prob, mean = self.step.step_with_logp(
+ self.model,
+ single,
+ strategy=self.strategy,
+ sample=state,
+ sigma=schedule[step_index],
+ sigma_next=schedule[step_index + 1],
+ params=params,
+ prev_sample=previous,
+ sigma_max=sigma_max,
+ eta=float(params.eta),
+ step_index=step_index,
+ )
+ if log_prob is None or mean is None:
+ raise RuntimeError(
+ f"SenseNovaU1DiffusionStage.replay got a deterministic transition at step {step_index}."
+ )
+ log_probs.append(log_prob)
+ means.append(mean)
+ batch_log_probs.append(torch.stack(log_probs, dim=1))
+ batch_means.append(torch.stack(means, dim=1))
+
+ return ReplayResult(
+ log_probs=torch.cat(batch_log_probs, dim=0).to(dtype=self.logprob_dtype),
+ prev_sample_means=torch.cat(batch_means, dim=0).to(dtype=self.trajectory_dtype),
+ )
+
+ def predict_noise_at_step(
+ self,
+ conditions: SenseNovaU1Conditions,
+ *,
+ sample: torch.Tensor,
+ sigma: torch.Tensor,
+ params: SenseNovaU1DiffusionParams,
+ ) -> torch.Tensor:
+ """Return the framework sigma-time velocity ``dx/dsigma``."""
+ self._configure_t_eps(params)
+ with self._autocast_ctx():
+ return -self.step.predict_velocity(
+ self.model,
+ conditions,
+ sample=sample,
+ sigma=sigma,
+ params=params,
+ # This API has no schedule index. Treat it as a non-initial
+ # prediction so CFG-Zero* does not incorrectly zero every call.
+ step_index=-1,
+ )
+
+ def trainable_module(self) -> torch.nn.Module:
+ return self.model.trainable_module()
+
+
+__all__ = [
+ "SenseNovaU1DiffusionParams",
+ "SenseNovaU1DiffusionStage",
+ "SenseNovaU1DiffusionStep",
+ "resolve_noise_scale",
+]
diff --git a/unirl/models/sensenova_u1/pipeline.py b/unirl/models/sensenova_u1/pipeline.py
new file mode 100644
index 000000000..0bcab866d
--- /dev/null
+++ b/unirl/models/sensenova_u1/pipeline.py
@@ -0,0 +1,234 @@
+"""Sample-native SenseNova-U1.5 text-to-image pipeline."""
+
+from __future__ import annotations
+
+from contextlib import nullcontext
+from typing import Any, Optional
+
+import torch
+
+from unirl.models.types.pipeline import Pipeline
+from unirl.sde.kernels import FlowSDEStrategy, StepStrategy
+from unirl.sde.runtime import FlowMatchSchedulePolicy
+from unirl.types.noise_recipe import NoiseRecipe
+from unirl.types.primitives import Texts
+from unirl.types.sample import Sample
+from unirl.utils.dtypes import parse_torch_dtype
+
+from .bundle import SenseNovaU1Bundle
+from .conditions import SenseNovaU1Conditions
+from .config import SenseNovaU1PipelineConfig
+from .diffusion import SenseNovaU1DiffusionParams, SenseNovaU1DiffusionStage
+from .pixels import SenseNovaU1PixelDecodeStage, packed_pixel_shape
+from .vendor.neo_unify.utils import SYSTEM_MESSAGE_FOR_GEN
+
+DEFAULT_PIXEL_PATCH_SIZE = 32
+IMAGE_START_TOKEN = "
"
+
+
+class SenseNovaU1Pipeline(Pipeline):
+ """SenseNova-U1.5 T2I rollout and replay pipeline."""
+
+ def __init__(
+ self,
+ *,
+ bundle: SenseNovaU1Bundle,
+ diffusion: Optional[SenseNovaU1DiffusionStage] = None,
+ pixel_decode: Optional[SenseNovaU1PixelDecodeStage] = None,
+ strategy: Optional[StepStrategy] = None,
+ shift: float = 3.0,
+ autocast_precision: str = "bf16",
+ trajectory_precision: str = "bf16",
+ logprob_precision: str = "fp32",
+ ) -> None:
+ super().__init__()
+ self.bundle = bundle
+ self.diffusion = (
+ diffusion
+ if diffusion is not None
+ else SenseNovaU1DiffusionStage(
+ model=bundle,
+ strategy=strategy if strategy is not None else FlowSDEStrategy(),
+ autocast_precision=autocast_precision,
+ trajectory_precision=trajectory_precision,
+ logprob_precision=logprob_precision,
+ )
+ )
+ self.pixel_decode = pixel_decode if pixel_decode is not None else SenseNovaU1PixelDecodeStage(bundle)
+ self.shift = float(shift)
+ self.autocast_dtype = parse_torch_dtype(autocast_precision, field_name="autocast_precision")
+
+ @classmethod
+ def from_config(
+ cls,
+ config: SenseNovaU1PipelineConfig,
+ *,
+ strategy: Optional[StepStrategy] = None,
+ ) -> "SenseNovaU1Pipeline":
+ """Load a bundle and assemble the full trainside pipeline."""
+ return cls(
+ bundle=SenseNovaU1Bundle.from_config(config),
+ strategy=strategy,
+ shift=float(config.timestep_shift),
+ autocast_precision=config.autocast_precision,
+ trajectory_precision=config.trajectory_precision,
+ logprob_precision=config.logprob_precision,
+ )
+
+ @classmethod
+ def latent_shape(cls, *, model_config: Any, sampling_spec: Any) -> tuple:
+ """Driver-side NCHW pixel-noise shape, matching upstream seeded RNG."""
+ del model_config
+ height, width = int(sampling_spec.height), int(sampling_spec.width)
+ packed_pixel_shape((height, width), patch_size=DEFAULT_PIXEL_PATCH_SIZE)
+ return (3, height, width)
+
+ def build_schedule_policy(self) -> FlowMatchSchedulePolicy:
+ """U1.5 uses the standard rational FlowMatch shift."""
+ return FlowMatchSchedulePolicy.static_only(self.shift)
+
+ def _autocast_ctx(self):
+ device = torch.device(self.bundle.device)
+ if device.type == "cuda" and self.autocast_dtype in (torch.float16, torch.bfloat16):
+ return torch.autocast("cuda", dtype=self.autocast_dtype)
+ if device.type == "cpu" and self.autocast_dtype == torch.bfloat16:
+ return torch.autocast("cpu", dtype=torch.bfloat16)
+ return nullcontext()
+
+ def _build_prefix(self, prompt: str, *, conditional: bool, image_shape: tuple[int, int]):
+ model = self.bundle.model
+ if conditional:
+ query = model._build_t2i_query(
+ prompt,
+ system_message=SYSTEM_MESSAGE_FOR_GEN,
+ append_text="\n\n\n\n" + IMAGE_START_TOKEN,
+ )
+ else:
+ query = model._build_t2i_query(prompt, append_text=IMAGE_START_TOKEN)
+
+ input_ids, indexes, attention_mask = model._build_t2i_text_inputs(self.bundle.tokenizer, query)
+ cache = self.bundle.transformer(
+ "prefix",
+ input_ids=input_ids,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ )
+
+ height, width = image_shape
+ merge = int(1 / float(model.downsample_ratio))
+ token_h = height // (int(model.patch_size) * merge)
+ token_w = width // (int(model.patch_size) * merge)
+ image_indexes = model._build_t2i_image_indexes(
+ token_h,
+ token_w,
+ indexes.shape[1],
+ device=input_ids.device,
+ )
+ return cache, image_indexes
+
+ def build_conditions(
+ self,
+ texts: Texts,
+ *,
+ negatives: Optional[Texts] = None,
+ guidance_scale: float = 1.0,
+ image_shape: tuple[int, int] = (512, 512),
+ ) -> SenseNovaU1Conditions:
+ """Build frozen conditional/unconditional prefix KV caches."""
+ if negatives is not None and len(negatives.texts) != len(texts.texts):
+ raise ValueError(
+ f"SenseNovaU1Pipeline.build_conditions: negatives={len(negatives.texts)} != prompts={len(texts.texts)}."
+ )
+ packed_pixel_shape(image_shape, patch_size=self.pixel_decode.pixel_patch_size)
+
+ condition_caches = []
+ uncondition_caches = []
+ condition_indexes = []
+ uncondition_indexes = []
+ prefix_cache = {}
+ with torch.no_grad(), self._autocast_ctx():
+ for index, prompt in enumerate(texts.texts):
+ condition_key = ("condition", str(prompt), tuple(image_shape))
+ if condition_key not in prefix_cache:
+ prefix_cache[condition_key] = self._build_prefix(
+ str(prompt),
+ conditional=True,
+ image_shape=image_shape,
+ )
+ cache, image_indexes = prefix_cache[condition_key]
+ condition_caches.append(cache)
+ condition_indexes.append(image_indexes)
+
+ if float(guidance_scale) > 1.0:
+ negative = negatives.texts[index] if negatives is not None else ""
+ uncondition_key = ("uncondition", str(negative), tuple(image_shape))
+ if uncondition_key not in prefix_cache:
+ prefix_cache[uncondition_key] = self._build_prefix(
+ str(negative),
+ conditional=False,
+ image_shape=image_shape,
+ )
+ cache, image_indexes = prefix_cache[uncondition_key]
+ else:
+ cache, image_indexes = None, None
+ uncondition_caches.append(cache)
+ uncondition_indexes.append(image_indexes)
+
+ return SenseNovaU1Conditions(
+ prompts=[str(text) for text in texts.texts],
+ condition_caches=condition_caches,
+ uncondition_caches=uncondition_caches,
+ condition_image_indexes=condition_indexes,
+ uncondition_image_indexes=uncondition_indexes,
+ image_shapes=[tuple(image_shape)] * len(texts.texts),
+ )
+
+ def generate(self, sample: Sample) -> Sample:
+ """Run T2I sampling and fill the pre-forked diffusion frontier."""
+ frontier = sample.parts[-1]
+ params = frontier.sampling_params
+ if not isinstance(params, SenseNovaU1DiffusionParams):
+ raise TypeError(
+ "SenseNovaU1Pipeline.generate requires SenseNovaU1DiffusionParams, "
+ f"got {type(params).__name__ if params is not None else 'None'}."
+ )
+ if params.sigmas is None:
+ raise ValueError(
+ "SenseNovaU1Pipeline.generate: sampling sigmas are not pinned; "
+ "the hosting engine must apply pipeline.build_schedule_policy()."
+ )
+
+ conditioning = sample.conditioning()
+ texts = conditioning[0] if conditioning else None
+ if not isinstance(texts, Texts):
+ raise TypeError(
+ "SenseNovaU1Pipeline.generate expected a Texts prompt, "
+ f"got {type(texts).__name__ if texts is not None else 'None'}."
+ )
+
+ image_shape = (int(params.height), int(params.width))
+ conditions = self.build_conditions(
+ texts,
+ guidance_scale=float(params.guidance_scale),
+ image_shape=image_shape,
+ )
+ initial = NoiseRecipe.from_sample(sample).resolve(
+ device=torch.device(self.bundle.device),
+ dtype=self.diffusion.trajectory_dtype,
+ )
+ segment = self.diffusion.diffuse(
+ conditions,
+ schedule=params.sigmas,
+ params=params,
+ initial_latents=initial,
+ )
+ images = self.pixel_decode.decode(segment, image_shape=image_shape)
+ filled = frontier.fill(
+ segment=segment,
+ primitives={"image": images},
+ conditions=conditions.to_dict(),
+ )
+ return sample.replace_frontier(filled)
+
+
+__all__ = ["SenseNovaU1Pipeline"]
diff --git a/unirl/models/sensenova_u1/pixels.py b/unirl/models/sensenova_u1/pixels.py
new file mode 100644
index 000000000..65d312152
--- /dev/null
+++ b/unirl/models/sensenova_u1/pixels.py
@@ -0,0 +1,111 @@
+"""Packed-pixel geometry and decode stage for SenseNova-U1.5."""
+
+from __future__ import annotations
+
+from contextlib import nullcontext
+from typing import TYPE_CHECKING, Tuple
+
+import torch
+
+from unirl.models.types.codec import DecodeStage
+from unirl.types.primitives import Images
+from unirl.types.segments.latent import LatentSegment
+
+if TYPE_CHECKING:
+ from .bundle import SenseNovaU1Bundle
+
+
+def packed_pixel_shape(
+ image_shape: Tuple[int, int],
+ *,
+ patch_size: int,
+) -> Tuple[int, int]:
+ """Return packed ``(sequence, channels)`` for an ``(H, W)`` RGB image."""
+ height, width = (int(v) for v in image_shape)
+ patch = int(patch_size)
+ if height <= 0 or width <= 0 or height % patch or width % patch:
+ raise ValueError(
+ f"SenseNova-U1 image shape {height}x{width} must be positive and divisible by pixel patch {patch}."
+ )
+ return (height // patch) * (width // patch), 3 * patch * patch
+
+
+def unpatchify_pixels(
+ packed: torch.Tensor,
+ *,
+ image_shape: Tuple[int, int],
+ patch_size: int,
+) -> torch.Tensor:
+ """Convert ``[B, L, 3*p*p]`` packed pixels to ``[B, 3, H, W]``."""
+ height, width = (int(v) for v in image_shape)
+ patch = int(patch_size)
+ expected_sequence, expected_channels = packed_pixel_shape(image_shape, patch_size=patch)
+ if packed.ndim != 3 or tuple(packed.shape[1:]) != (expected_sequence, expected_channels):
+ raise ValueError(
+ "unpatchify_pixels expected packed shape "
+ f"[B, {expected_sequence}, {expected_channels}], got {tuple(packed.shape)}."
+ )
+ batch = int(packed.shape[0])
+ token_h, token_w = height // patch, width // patch
+ blocks = packed.reshape(batch, token_h, token_w, patch, patch, 3)
+ blocks = torch.einsum("nhwpqc->nchpwq", blocks)
+ return blocks.reshape(batch, 3, height, width)
+
+
+def patchify_pixels(
+ pixels: torch.Tensor,
+ *,
+ patch_size: int,
+) -> torch.Tensor:
+ """Convert ``[B, 3, H, W]`` normalized pixels to ``[B, L, 3*p*p]``."""
+ if pixels.ndim != 4 or int(pixels.shape[1]) != 3:
+ raise ValueError(f"patchify_pixels expected [B, 3, H, W], got {tuple(pixels.shape)}.")
+ batch, _, height, width = pixels.shape
+ patch = int(patch_size)
+ packed_pixel_shape((int(height), int(width)), patch_size=patch)
+ token_h, token_w = int(height) // patch, int(width) // patch
+ blocks = pixels.reshape(batch, 3, token_h, patch, token_w, patch)
+ blocks = torch.einsum("nchpwq->nhwpqc", blocks)
+ return blocks.reshape(batch, token_h * token_w, 3 * patch * patch)
+
+
+class SenseNovaU1PixelDecodeStage(DecodeStage[LatentSegment, Images]):
+ """Decode the final packed state; SenseNova-U1.5 diffuses pixels directly."""
+
+ def __init__(self, bundle: "SenseNovaU1Bundle") -> None:
+ self.bundle = bundle
+
+ @property
+ def pixel_patch_size(self) -> int:
+ model = self.bundle.model
+ return int(model.patch_size) * int(1 / float(model.downsample_ratio))
+
+ def decode(
+ self,
+ segment: LatentSegment,
+ *,
+ image_shape: Tuple[int, int],
+ grad: bool = False,
+ activation_checkpoint: bool = False,
+ ) -> Images:
+ """Decode final normalized pixels to an ``Images`` batch in ``[0, 1]``."""
+ del activation_checkpoint
+ if segment.latents is None or segment.latents.ndim != 4:
+ got = None if segment.latents is None else tuple(segment.latents.shape)
+ raise ValueError(f"SenseNovaU1PixelDecodeStage.decode expected latents [B, K, L, C], got {got}.")
+ with nullcontext() if grad else torch.no_grad():
+ normalized = unpatchify_pixels(
+ segment.latents[:, -1].float(),
+ image_shape=image_shape,
+ patch_size=self.pixel_patch_size,
+ )
+ pixels = (normalized * 0.5 + 0.5).clamp(0.0, 1.0)
+ return Images.from_dense(pixels.cpu())
+
+
+__all__ = [
+ "SenseNovaU1PixelDecodeStage",
+ "packed_pixel_shape",
+ "patchify_pixels",
+ "unpatchify_pixels",
+]
diff --git a/unirl/models/sensenova_u1/vendor/VENDOR_COMMIT.txt b/unirl/models/sensenova_u1/vendor/VENDOR_COMMIT.txt
new file mode 100644
index 000000000..6a644d884
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/VENDOR_COMMIT.txt
@@ -0,0 +1,20 @@
+source = OpenSenseNova/SenseNova-U1 (https://github.com/OpenSenseNova/SenseNova-U1)
+commit = a62fd54afed7a8d4c1cb4e10f0c39253d1be6fde
+short = a62fd54
+vendored = 2026-08-24
+license = Apache-2.0
+
+Vendored subtree:
+ src/sensenova_u1/models/neo_unify/
+ -> unirl/models/sensenova_u1/vendor/neo_unify/
+
+The subtree is byte-pristine at import. UniRL adapters outside vendor/ own the
+FlowGRPO sigma-time conversion, stochastic transition math, trajectory replay,
+prompt-cache conditions, FSDP trainability policy, and Sample-native pipeline.
+
+To update:
+ 1. fetch and pin a new upstream commit;
+ 2. replace the complete neo_unify subtree;
+ 3. update this file;
+ 4. rerun checkpoint-load, reference-inference parity, rollout/replay-ratio,
+ and replay-backward tests.
diff --git a/unirl/models/sensenova_u1/vendor/__init__.py b/unirl/models/sensenova_u1/vendor/__init__.py
new file mode 100644
index 000000000..c43c6c6aa
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/__init__.py
@@ -0,0 +1,7 @@
+"""Vendored OpenSenseNova NEO-Unify modeling code.
+
+The source is copied from OpenSenseNova/SenseNova-U1 at the revision recorded
+in ``VENDOR_COMMIT.txt``. UniRL owns sampling, SDE transitions, replay, and
+training orchestration; only the checkpoint-compatible model definitions are
+vendored here.
+"""
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/__init__.py b/unirl/models/sensenova_u1/vendor/neo_unify/__init__.py
new file mode 100644
index 000000000..e413a13d6
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/__init__.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from .configuration_neo_chat import NEOChatConfig, NEOLLMConfig, NEOMoELLMConfig
+from .configuration_neo_vit import NEOVisionConfig
+from .modeling_neo_chat import NEOChatModel
+from .modeling_neo_vit import NEOVisionModel
+from .modeling_qwen3 import (
+ _HAS_FLASH_ATTN as has_flash_attn,
+ effective_attn_backend,
+ get_attn_backend,
+ set_attn_backend,
+)
+from .modeling_qwen3 import Qwen3ForCausalLM
+from .modeling_qwen3_moe import Qwen3MoeForCausalLM
+
+__all__ = [
+ "NEOChatConfig",
+ "NEOLLMConfig",
+ "NEOMoELLMConfig",
+ "NEOVisionConfig",
+ "NEOChatModel",
+ "NEOVisionModel",
+ "Qwen3ForCausalLM",
+ "Qwen3MoeForCausalLM",
+ "register",
+ "set_attn_backend",
+ "get_attn_backend",
+ "effective_attn_backend",
+ "has_flash_attn",
+]
+
+
+_REGISTERED = False
+
+
+def register() -> None:
+ """Register NEO-Unify types with ``transformers.Auto*``.
+
+ After calling this (or simply ``import sensenova_u1``), users can load a
+ SenseNova-U1 checkpoint via plain ``AutoConfig.from_pretrained`` /
+ ``AutoModel.from_pretrained``.
+ """
+ global _REGISTERED
+ if _REGISTERED:
+ return
+
+ from transformers import AutoConfig, AutoModel
+
+ AutoConfig.register("neo_vision", NEOVisionConfig, exist_ok=True)
+ AutoConfig.register("neo_chat", NEOChatConfig, exist_ok=True)
+
+ AutoModel.register(NEOVisionConfig, NEOVisionModel, exist_ok=True)
+ AutoModel.register(NEOChatConfig, NEOChatModel, exist_ok=True)
+
+ _REGISTERED = True
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/configuration_neo_chat.py b/unirl/models/sensenova_u1/vendor/neo_unify/configuration_neo_chat.py
new file mode 100644
index 000000000..eed99b761
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/configuration_neo_chat.py
@@ -0,0 +1,197 @@
+import copy
+
+from transformers import Qwen3Config, Qwen3MoeConfig
+from transformers.utils import logging
+from transformers.configuration_utils import PretrainedConfig
+
+from .configuration_neo_vit import NEOVisionConfig
+
+
+logger = logging.get_logger(__name__)
+
+
+def _restore_legacy_rope_theta(config) -> None:
+ """Expose the v4 rope attribute expected by the vendored model code."""
+ if hasattr(config, "rope_theta"):
+ return
+ rope_parameters = getattr(config, "rope_parameters", None) or {}
+ config.rope_theta = float(rope_parameters.get("rope_theta", 10000.0))
+
+
+class NEOLLMConfig(Qwen3Config):
+ """Config for the dense Qwen3 backbone used by NEO-Unify.
+
+ Extends ``Qwen3Config`` with two extra rope knobs used by the spatial
+ (height/width) rotary axes that are layered on top of the temporal one.
+ """
+
+ def __init__(self, rope_theta_hw=10000.0, max_position_embeddings_hw=10000, **kwargs):
+ super().__init__(**kwargs)
+ _restore_legacy_rope_theta(self)
+ self.rope_theta_hw = rope_theta_hw
+ self.max_position_embeddings_hw = max_position_embeddings_hw
+
+
+class NEOMoELLMConfig(Qwen3MoeConfig):
+ """Config for the Qwen3-MoE backbone used by NEO-Unify.
+
+ Extends ``Qwen3MoeConfig`` with the same ``rope_theta_hw`` /
+ ``max_position_embeddings_hw`` extras as :class:`NEOLLMConfig`, and adds a
+ *generation-path* MoE branch alongside the standard understanding-path one.
+ In the A3B unified model every decoder layer carries two parallel sparse
+ MoE blocks routed by the per-token ``image_gen_indicators`` mask:
+
+ * ``mlp`` - sparse MoE for the understanding path
+ (``num_experts`` experts, ``num_experts_per_tok`` active,
+ expert width ``moe_intermediate_size``).
+ * ``mlp_mot_gen`` - sparse MoE for the image generation path
+ (``gen_num_experts`` experts, ``gen_num_experts_per_tok``
+ active, expert width ``gen_moe_intermediate_size``).
+
+ Each gen-path knob falls back to its understanding-path counterpart when
+ unset, so vanilla single-MoE configs keep working without changes.
+ """
+
+ def __init__(
+ self,
+ rope_theta_hw=10000.0,
+ max_position_embeddings_hw=10000,
+ gen_num_experts=None,
+ gen_num_experts_per_tok=None,
+ gen_moe_intermediate_size=None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ _restore_legacy_rope_theta(self)
+ self.rope_theta_hw = rope_theta_hw
+ self.max_position_embeddings_hw = max_position_embeddings_hw
+
+ # Generation-path MoE knobs default to the understanding-path values
+ # so legacy single-MoE configs (where both branches share the same
+ # router width / expert count) keep working unchanged.
+ self.gen_num_experts = (
+ int(gen_num_experts) if gen_num_experts is not None else int(self.num_experts)
+ )
+ self.gen_num_experts_per_tok = (
+ int(gen_num_experts_per_tok)
+ if gen_num_experts_per_tok is not None
+ else int(self.num_experts_per_tok)
+ )
+ self.gen_moe_intermediate_size = (
+ int(gen_moe_intermediate_size)
+ if gen_moe_intermediate_size is not None
+ else int(self.moe_intermediate_size)
+ )
+
+ # ``Qwen3Attention`` (used by NEO-Unify MoE layers) reads
+ # ``config.layer_types[layer_idx]`` to decide between ``"full_attention"``
+ # and ``"sliding_attention"``. Older / vanilla ``Qwen3MoeConfig`` does
+ # not populate that field, so we backfill it here mirroring the dense
+ # ``Qwen3Config`` behaviour: sliding-attention layers start at
+ # ``max_window_layers`` when ``use_sliding_window`` is enabled.
+ existing = getattr(self, "layer_types", None)
+ if not existing or len(existing) != self.num_hidden_layers:
+ use_swa = bool(getattr(self, "use_sliding_window", False)) and getattr(
+ self, "sliding_window", None
+ ) is not None
+ max_window_layers = int(getattr(self, "max_window_layers", 0) or 0)
+ self.layer_types = [
+ "sliding_attention" if (use_swa and i >= max_window_layers) else "full_attention"
+ for i in range(self.num_hidden_layers)
+ ]
+
+
+def _is_moe_llm_config(llm_config) -> bool:
+ """Detect whether an ``llm_config`` (dict or object) targets a MoE backbone.
+
+ Order of checks: explicit ``model_type``, ``architectures`` entry that
+ contains ``MoE/MoeForCausalLM``, or presence of MoE-specific keys
+ (``num_experts``).
+ """
+ if isinstance(llm_config, dict):
+ model_type = llm_config.get("model_type", "")
+ archs = llm_config.get("architectures") or []
+ has_num_experts = "num_experts" in llm_config
+ else:
+ model_type = getattr(llm_config, "model_type", "")
+ archs = getattr(llm_config, "architectures", None) or []
+ has_num_experts = hasattr(llm_config, "num_experts")
+
+ if isinstance(model_type, str) and "moe" in model_type.lower():
+ return True
+ for arch in archs:
+ arch_str = str(arch)
+ if "Moe" in arch_str or "MoE" in arch_str:
+ return True
+ return bool(has_num_experts) and getattr(llm_config, "num_experts", 0) and int(getattr(llm_config, "num_experts", 0)) > 1
+
+
+def _build_llm_config(llm_config):
+ """Instantiate the right LLM config object from a dict or pre-built config."""
+ if isinstance(llm_config, dict):
+ if _is_moe_llm_config(llm_config):
+ return NEOMoELLMConfig(**llm_config)
+ return NEOLLMConfig(**llm_config)
+ return llm_config
+
+
+class NEOChatConfig(PretrainedConfig):
+ model_type = 'neo_chat'
+ is_composition = True
+
+ def __init__(
+ self,
+ vision_config=None,
+ llm_config=None,
+ use_backbone_lora=0,
+ use_llm_lora=0,
+ downsample_ratio=0.5,
+ template=None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+
+ if vision_config is None:
+ vision_config = {'architectures': ['NEOVisionModel']}
+ logger.info('vision_config is None. Initializing the NEOVisionConfig with default values.')
+
+ if llm_config is None:
+ llm_config = {'architectures': ['Qwen3ForCausalLM']}
+ logger.info('llm_config is None. Initializing the LlamaConfig config with default values (`LlamaConfig`).')
+ assert 'architectures' in llm_config, "Should specify architecture in llm_config"
+
+ if isinstance(vision_config, dict):
+ self.vision_config = NEOVisionConfig(**vision_config)
+ else:
+ self.vision_config = vision_config
+
+ self.llm_config = _build_llm_config(llm_config)
+
+ self.use_backbone_lora = use_backbone_lora
+ self.use_llm_lora = use_llm_lora
+ self.downsample_ratio = downsample_ratio
+ self.template = template
+ self.tie_word_embeddings = self.llm_config.tie_word_embeddings
+
+ @property
+ def is_moe_llm(self) -> bool:
+ """Convenience flag so callers can switch between dense / MoE LLM."""
+ return isinstance(self.llm_config, NEOMoELLMConfig)
+
+ def to_dict(self):
+ """
+ Serializes this instance to a Python dictionary. Override the default [`~PretrainedConfig.to_dict`].
+
+ Returns:
+ `Dict[str, any]`: Dictionary of all the attributes that make up this configuration instance,
+ """
+ output = copy.deepcopy(self.__dict__)
+ output['vision_config'] = self.vision_config.to_dict()
+ output['llm_config'] = self.llm_config.to_dict()
+ output['model_type'] = self.__class__.model_type
+ output['use_backbone_lora'] = self.use_backbone_lora
+ output['use_llm_lora'] = self.use_llm_lora
+ output['downsample_ratio'] = self.downsample_ratio
+ output['template'] = self.template
+
+ return output
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/configuration_neo_vit.py b/unirl/models/sensenova_u1/vendor/neo_unify/configuration_neo_vit.py
new file mode 100644
index 000000000..02837fea4
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/configuration_neo_vit.py
@@ -0,0 +1,52 @@
+import os
+from typing import Union
+
+from transformers.configuration_utils import PretrainedConfig
+from transformers.utils import logging
+
+logger = logging.get_logger(__name__)
+
+
+class NEOVisionConfig(PretrainedConfig):
+
+ model_type = 'neo_vision'
+
+ def __init__(
+ self,
+ num_channels=3,
+ patch_size=16,
+ hidden_size=1024,
+ llm_hidden_size=2048,
+ downsample_ratio=0.5,
+ rope_theta_vision=10000.0,
+ max_position_embeddings_vision=10000,
+ min_pixels=65536,
+ max_pixels=4194304,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+
+ self.hidden_size = hidden_size
+ self.llm_hidden_size = llm_hidden_size,
+ self.downsample_ratio = downsample_ratio,
+ self.rope_theta_vision = rope_theta_vision
+ self.max_position_embeddings_vision = max_position_embeddings_vision
+ self.num_channels = num_channels
+ self.patch_size = patch_size
+ self.min_pixels = min_pixels
+ self.max_pixels = max_pixels
+
+ @classmethod
+ def from_pretrained(cls, pretrained_model_name_or_path: Union[str, os.PathLike], **kwargs) -> 'PretrainedConfig':
+ config_dict, kwargs = cls.get_config_dict(pretrained_model_name_or_path, **kwargs)
+
+ if 'vision_config' in config_dict:
+ config_dict = config_dict['vision_config']
+
+ if 'model_type' in config_dict and hasattr(cls, 'model_type') and config_dict['model_type'] != cls.model_type:
+ logger.warning(
+ f"You are using a model of type {config_dict['model_type']} to instantiate a model of type "
+ f'{cls.model_type}. This is not supported for all configurations of models and can yield errors.'
+ )
+
+ return cls.from_dict(config_dict, **kwargs)
\ No newline at end of file
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/conversation.py b/unirl/models/sensenova_u1/vendor/neo_unify/conversation.py
new file mode 100644
index 000000000..73f312a2d
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/conversation.py
@@ -0,0 +1,397 @@
+"""
+Conversation prompt templates.
+
+We kindly request that you import fastchat instead of copying this file if you wish to use it.
+If you have changes in mind, please contribute back so the community can benefit collectively and continue to maintain these valuable templates.
+
+Modified from https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py
+"""
+
+import dataclasses
+from enum import IntEnum, auto
+from typing import Dict, List, Tuple, Union
+
+
+class SeparatorStyle(IntEnum):
+ """Separator styles."""
+
+ ADD_COLON_SINGLE = auto()
+ ADD_COLON_TWO = auto()
+ ADD_COLON_SPACE_SINGLE = auto()
+ NO_COLON_SINGLE = auto()
+ NO_COLON_TWO = auto()
+ ADD_NEW_LINE_SINGLE = auto()
+ LLAMA2 = auto()
+ CHATGLM = auto()
+ CHATML = auto()
+ CHATINTERN = auto()
+ DOLLY = auto()
+ RWKV = auto()
+ PHOENIX = auto()
+ ROBIN = auto()
+ FALCON_CHAT = auto()
+ CHATGLM3 = auto()
+ INTERNVL_ZH = auto()
+ MPT = auto()
+
+
+@dataclasses.dataclass
+class Conversation:
+ """A class that manages prompt templates and keeps all conversation history."""
+
+ # The name of this template
+ name: str
+ # The template of the system prompt
+ system_template: str = '{system_message}'
+ # The system message
+ system_message: str = ''
+ # The names of two roles
+ roles: Tuple[str] = ('USER', 'ASSISTANT')
+ # All messages. Each item is (role, message).
+ messages: List[List[str]] = ()
+ # The number of few shot examples
+ offset: int = 0
+ # The separator style and configurations
+ sep_style: SeparatorStyle = SeparatorStyle.ADD_COLON_SINGLE
+ sep: str = '\n'
+ sep2: str = None
+ # Stop criteria (the default one is EOS token)
+ stop_str: Union[str, List[str]] = None
+ # Stops generation if meeting any token in this list
+ stop_token_ids: List[int] = None
+
+ def get_prompt(self) -> str:
+ """Get the prompt for generation."""
+ if self.system_message is not None and self.system_message != '':
+ system_prompt = self.system_template.format(system_message=self.system_message)
+ else:
+ system_prompt = ''
+
+ if self.sep_style == SeparatorStyle.ADD_COLON_SINGLE:
+ ret = '' if system_prompt == '' else system_prompt + self.sep
+ for role, message in self.messages:
+ if message:
+ ret += role + ': ' + message + self.sep
+ else:
+ ret += role + ':'
+ return ret
+ elif self.sep_style == SeparatorStyle.ADD_COLON_TWO:
+ seps = [self.sep, self.sep2]
+ ret = '' if system_prompt == '' else system_prompt + seps[0]
+ for i, (role, message) in enumerate(self.messages):
+ if message:
+ ret += role + ': ' + message + seps[i % 2]
+ else:
+ ret += role + ':'
+ return ret
+ elif self.sep_style == SeparatorStyle.ADD_COLON_SPACE_SINGLE:
+ ret = '' if system_prompt == '' else system_prompt + self.sep
+ for role, message in self.messages:
+ if message:
+ ret += role + ': ' + message + self.sep
+ else:
+ ret += role + ': ' # must be end with a space
+ return ret
+ elif self.sep_style == SeparatorStyle.ADD_NEW_LINE_SINGLE:
+ ret = '' if system_prompt == '' else system_prompt + self.sep
+ for role, message in self.messages:
+ if message:
+ ret += role + '\n' + message + self.sep
+ else:
+ ret += role + '\n'
+ return ret
+ elif self.sep_style == SeparatorStyle.NO_COLON_SINGLE:
+ ret = system_prompt
+ for role, message in self.messages:
+ if message:
+ ret += role + message + self.sep
+ else:
+ ret += role
+ return ret
+ elif self.sep_style == SeparatorStyle.NO_COLON_TWO:
+ seps = [self.sep, self.sep2]
+ ret = system_prompt
+ for i, (role, message) in enumerate(self.messages):
+ if message:
+ ret += role + message + seps[i % 2]
+ else:
+ ret += role
+ return ret
+ elif self.sep_style == SeparatorStyle.RWKV:
+ ret = system_prompt
+ for i, (role, message) in enumerate(self.messages):
+ if message:
+ ret += (
+ role
+ + ': '
+ + message.replace('\r\n', '\n').replace('\n\n', '\n')
+ )
+ ret += '\n\n'
+ else:
+ ret += role + ':'
+ return ret
+ elif self.sep_style == SeparatorStyle.LLAMA2:
+ seps = [self.sep, self.sep2]
+ ret = system_prompt if system_prompt != '' else '[INST] '
+ for i, (role, message) in enumerate(self.messages):
+ tag = self.roles[i % 2]
+ if message:
+ if i == 0:
+ ret += message + ' '
+ else:
+ ret += tag + ' ' + message + seps[i % 2]
+ else:
+ ret += tag
+ return ret
+ elif self.sep_style == SeparatorStyle.CHATGLM:
+ # source: https://huggingface.co/THUDM/chatglm-6b/blob/1d240ba371910e9282298d4592532d7f0f3e9f3e/modeling_chatglm.py#L1302-L1308
+ # source2: https://huggingface.co/THUDM/chatglm2-6b/blob/e186c891cf64310ac66ef10a87e6635fa6c2a579/modeling_chatglm.py#L926
+ round_add_n = 1 if self.name == 'chatglm2' else 0
+ ret = '' if system_prompt == '' else system_prompt + self.sep
+
+ for i, (role, message) in enumerate(self.messages):
+ if i % 2 == 0:
+ ret += f'[Round {i//2 + round_add_n}]{self.sep}'
+
+ if message:
+ ret += f'{role}:{message}{self.sep}'
+ else:
+ ret += f'{role}:'
+ return ret
+ elif self.sep_style == SeparatorStyle.CHATML:
+ ret = '' if system_prompt == '' else system_prompt + self.sep + '\n'
+ for role, message in self.messages:
+ if message:
+ ret += role + '\n' + message + self.sep + '\n'
+ else:
+ ret += role + '\n'
+ return ret
+ elif self.sep_style == SeparatorStyle.CHATGLM3:
+ ret = system_prompt
+ for role, message in self.messages:
+ if message:
+ ret += role + '\n' + ' ' + message
+ else:
+ ret += role
+ return ret
+ elif self.sep_style == SeparatorStyle.CHATINTERN:
+ # source: https://huggingface.co/internlm/internlm-chat-7b-8k/blob/bd546fa984b4b0b86958f56bf37f94aa75ab8831/modeling_internlm.py#L771
+ seps = [self.sep, self.sep2]
+ ret = system_prompt
+ for i, (role, message) in enumerate(self.messages):
+ # if i % 2 == 0:
+ # ret += ""
+ if message:
+ ret += role + ':' + message + seps[i % 2] + '\n'
+ else:
+ ret += role + ':'
+ return ret
+ elif self.sep_style == SeparatorStyle.DOLLY:
+ seps = [self.sep, self.sep2]
+ ret = system_prompt
+ for i, (role, message) in enumerate(self.messages):
+ if message:
+ ret += role + ':\n' + message + seps[i % 2]
+ if i % 2 == 1:
+ ret += '\n\n'
+ else:
+ ret += role + ':\n'
+ return ret
+ elif self.sep_style == SeparatorStyle.PHOENIX:
+ ret = system_prompt
+ for role, message in self.messages:
+ if message:
+ ret += role + ': ' + '' + message + ''
+ else:
+ ret += role + ': ' + ''
+ return ret
+ elif self.sep_style == SeparatorStyle.ROBIN:
+ ret = '' if system_prompt == '' else system_prompt + self.sep
+ for role, message in self.messages:
+ if message:
+ ret += role + ':\n' + message + self.sep
+ else:
+ ret += role + ':\n'
+ return ret
+ elif self.sep_style == SeparatorStyle.FALCON_CHAT:
+ ret = '' if system_prompt == '' else system_prompt + self.sep
+ for role, message in self.messages:
+ if message:
+ ret += role + ': ' + message + self.sep
+ else:
+ ret += role + ':'
+
+ return ret
+ elif self.sep_style == SeparatorStyle.INTERNVL_ZH:
+ seps = [self.sep, self.sep2]
+ ret = '' if system_prompt == '' else self.system_message + seps[0]
+ for i, (role, message) in enumerate(self.messages):
+ if message:
+ ret += role + ': ' + message + seps[i % 2]
+ else:
+ ret += role + ':'
+ return ret
+ elif self.sep_style == SeparatorStyle.MPT:
+ ret = '' if system_prompt == '' else system_prompt + self.sep
+ for i, (role, message) in enumerate(self.messages):
+ if message:
+ if type(message) is tuple:
+ message, _, _ = message
+ ret += role + message + self.sep
+ else:
+ if i != len(self.messages) and message is not None:
+ ret += role + self.sep
+ else:
+ ret += role
+ return ret
+ else:
+ raise ValueError(f'Invalid style: {self.sep_style}')
+
+ def set_system_message(self, system_message: str):
+ """Set the system message."""
+ self.system_message = system_message
+
+ def append_message(self, role: str, message: str):
+ """Append a new message."""
+ self.messages.append([role, message])
+
+ def update_last_message(self, message: str):
+ """Update the last output.
+
+ The last message is typically set to be None when constructing the prompt,
+ so we need to update it in-place after getting the response from a model.
+ """
+ self.messages[-1][1] = message
+
+ def to_gradio_chatbot(self):
+ """Convert the conversation to gradio chatbot format."""
+ ret = []
+ for i, (role, msg) in enumerate(self.messages[self.offset :]):
+ if i % 2 == 0:
+ ret.append([msg, None])
+ else:
+ ret[-1][-1] = msg
+ return ret
+
+ def to_openai_api_messages(self):
+ """Convert the conversation to OpenAI chat completion format."""
+ ret = [{'role': 'system', 'content': self.system_message}]
+
+ for i, (_, msg) in enumerate(self.messages[self.offset :]):
+ if i % 2 == 0:
+ ret.append({'role': 'user', 'content': msg})
+ else:
+ if msg is not None:
+ ret.append({'role': 'assistant', 'content': msg})
+ return ret
+
+ def copy(self):
+ return Conversation(
+ name=self.name,
+ system_template=self.system_template,
+ system_message=self.system_message,
+ roles=self.roles,
+ messages=[[x, y] for x, y in self.messages],
+ offset=self.offset,
+ sep_style=self.sep_style,
+ sep=self.sep,
+ sep2=self.sep2,
+ stop_str=self.stop_str,
+ stop_token_ids=self.stop_token_ids,
+ )
+
+ def dict(self):
+ return {
+ 'template_name': self.name,
+ 'system_message': self.system_message,
+ 'roles': self.roles,
+ 'messages': self.messages,
+ 'offset': self.offset,
+ }
+
+
+# A global registry for all conversation templates
+conv_templates: Dict[str, Conversation] = {}
+
+
+def register_conv_template(template: Conversation, override: bool = False):
+ """Register a new conversation template."""
+ if not override:
+ assert (
+ template.name not in conv_templates
+ ), f'{template.name} has been registered.'
+
+ conv_templates[template.name] = template
+
+
+def get_conv_template(name: str) -> Conversation:
+ """Get a conversation template."""
+ return conv_templates[name].copy()
+
+
+# Both Hermes-2 and neo1_0-chat are chatml-format conversation templates. The difference
+# is that during training, the preprocessing function for the Hermes-2 template doesn't add
+# at the beginning of the tokenized sequence, while the neo1_0-chat template does.
+# Therefore, they are completely equivalent during inference.
+
+register_conv_template(
+ Conversation(
+ name='Hermes-2',
+ system_template='<|im_start|>system\n{system_message}',
+ # note: The new system prompt was not used here to avoid changes in benchmark performance.
+ # system_message='我是书生·万象,英文名是InternVL,是由上海人工智能实验室、清华大学及多家合作单位联合开发的多模态大语言模型。',
+ system_message='你是由上海人工智能实验室联合商汤科技开发的书生多模态大模型,英文名叫InternVL, 是一个有用无害的人工智能助手。',
+ roles=('<|im_start|>user\n', '<|im_start|>assistant\n'),
+ sep_style=SeparatorStyle.MPT,
+ sep='<|im_end|>',
+ stop_str='<|endoftext|>',
+ )
+)
+
+register_conv_template(
+ Conversation(
+ name='internlm2-chat',
+ system_template='<|im_start|>system\n{system_message}',
+ # note: The new system prompt was not used here to avoid changes in benchmark performance.
+ # system_message='我是书生·万象,英文名是InternVL,是由上海人工智能实验室、清华大学及多家合作单位联合开发的多模态大语言模型。',
+ system_message='你是由上海人工智能实验室联合商汤科技开发的书生多模态大模型,英文名叫InternVL, 是一个有用无害的人工智能助手。',
+ roles=('<|im_start|>user\n', '<|im_start|>assistant\n'),
+ sep_style=SeparatorStyle.MPT,
+ sep='<|im_end|>',
+ )
+)
+
+register_conv_template(
+ Conversation(
+ name='phi3-chat',
+ system_template='<|system|>\n{system_message}',
+ # note: The new system prompt was not used here to avoid changes in benchmark performance.
+ # system_message='我是书生·万象,英文名是InternVL,是由上海人工智能实验室、清华大学及多家合作单位联合开发的多模态大语言模型。',
+ system_message='你是由上海人工智能实验室联合商汤科技开发的书生多模态大模型,英文名叫InternVL, 是一个有用无害的人工智能助手。',
+ roles=('<|user|>\n', '<|assistant|>\n'),
+ sep_style=SeparatorStyle.MPT,
+ sep='<|end|>',
+ )
+)
+
+register_conv_template(
+ Conversation(
+ name='internvl2_5',
+ system_template='<|im_start|>system\n{system_message}',
+ system_message='你是书生·万象,英文名是InternVL,是由上海人工智能实验室、清华大学及多家合作单位联合开发的多模态大语言模型。',
+ roles=('<|im_start|>user\n', '<|im_start|>assistant\n'),
+ sep_style=SeparatorStyle.MPT,
+ sep='<|im_end|>\n',
+ )
+)
+
+register_conv_template(
+ Conversation(
+ name='neo1_0',
+ system_template='<|im_start|>system\n{system_message}',
+ system_message='',
+ roles=('<|im_start|>user\n', '<|im_start|>assistant\n'),
+ sep_style=SeparatorStyle.MPT,
+ sep='<|im_end|>\n',
+ )
+)
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/modeling_fm_modules.py b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_fm_modules.py
new file mode 100644
index 000000000..2435bb97f
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_fm_modules.py
@@ -0,0 +1,591 @@
+import numpy as np
+import torch
+import torch.nn as nn
+import math
+from functools import lru_cache
+
+from torch.utils.checkpoint import checkpoint
+def modulate(x, shift, scale=None):
+ if shift is None:
+ return x * (1 + scale)
+ return x * (1 + scale) + shift
+
+class RMSNorm(nn.Module):
+ def __init__(self, dim: int, eps: float = 1e-5):
+ super().__init__()
+ self.eps = eps
+ self.weight = nn.Parameter(torch.ones(dim))
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ output = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
+ return output * self.weight
+
+class TimestepEmbedder(nn.Module):
+ """
+ Embeds scalar timesteps into vector representations.
+ """
+
+ def __init__(self, hidden_size, frequency_embedding_size=256):
+ super().__init__()
+ self.mlp = nn.Sequential(
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
+ nn.SiLU(),
+ nn.Linear(hidden_size, hidden_size, bias=True),
+ )
+ self.frequency_embedding_size = frequency_embedding_size
+
+ @staticmethod
+ def timestep_embedding(t: torch.Tensor, dim: int, max_period: float = 10000.0):
+ """
+ Create sinusoidal timestep embeddings.
+ :param t: a 1-D Tensor of N indices, one per batch element. These may be fractional.
+ :param dim: the dimension of the output.
+ :param max_period: controls the minimum frequency of the embeddings.
+ :return: an (N, D) Tensor of positional embeddings.
+ """
+ # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
+ half = dim // 2
+ freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half).to(
+ device=t.device
+ )
+ args = t[:, None].float() * freqs[None]
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
+ if dim % 2:
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
+ return embedding
+
+ def forward(self, t):
+ t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
+ t_emb = self.mlp(t_freq.to(self.mlp[0].weight.dtype))
+ return t_emb
+
+class ResBlock(nn.Module):
+
+ def __init__(self, channels, mlp_ratio=1.0):
+ super().__init__()
+ self.channels = channels
+ self.intermediate_size = int(channels * mlp_ratio)
+
+ self.in_ln = nn.LayerNorm(self.channels, eps=1e-6)
+ self.mlp = nn.Sequential(
+ nn.Linear(self.channels, self.intermediate_size),
+ nn.SiLU(),
+ nn.Linear(self.intermediate_size, self.channels),
+ )
+
+ self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(channels, 3 * channels, bias=True))
+
+ def forward(self, x, y):
+ shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(y).chunk(3, dim=-1)
+ h = modulate(self.in_ln(x), shift_mlp, scale_mlp)
+ h = self.mlp(h)
+ return x + gate_mlp * h
+
+# class FinalLayer(nn.Module):
+
+# def __init__(self, model_channels, out_channels):
+# super().__init__()
+# self.norm_final = nn.LayerNorm(model_channels, elementwise_affine=False, eps=1e-6)
+# self.linear = nn.Linear(model_channels, out_channels, bias=True)
+# self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(model_channels, 2 * model_channels, bias=True))
+
+# def forward(self, x, c):
+# shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1)
+# x = modulate(self.norm_final(x), shift, scale)
+# x = self.linear(x)
+# return x
+
+# class SimpleMLPAdaLN(nn.Module):
+
+# def __init__(self, input_dim, out_dim, dim=1536, layers=12, mlp_ratio=1.0):
+# super().__init__()
+# self.input_dim = input_dim
+# self.out_dim = out_dim
+# self.dim = dim
+# self.layers = layers
+# self.mlp_ratio = mlp_ratio
+
+# self.time_embed = TimestepEmbedder(dim)
+# self.input_proj = nn.Linear(input_dim, dim)
+
+# res_blocks = []
+# for _ in range(layers):
+# res_blocks.append(ResBlock(dim, mlp_ratio))
+# self.res_blocks = nn.ModuleList(res_blocks)
+
+# self.final_layer = FinalLayer(dim, out_dim)
+
+# self.grad_checkpointing = False
+
+# self.initialize_weights()
+
+# def initialize_weights(self):
+# def _basic_init(module):
+# if isinstance(module, nn.Linear):
+# torch.nn.init.xavier_uniform_(module.weight)
+# if module.bias is not None:
+# nn.init.constant_(module.bias, 0)
+
+# self.apply(_basic_init)
+
+# # Initialize timestep embedding MLP
+# nn.init.normal_(self.time_embed.mlp[0].weight, std=0.02)
+# nn.init.normal_(self.time_embed.mlp[2].weight, std=0.02)
+
+# # Zero-out adaLN modulation layers
+# for block in self.res_blocks:
+# nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
+# nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
+
+# # Zero-out output layers
+# nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
+# nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0)
+# nn.init.constant_(self.final_layer.linear.weight, 0)
+# nn.init.constant_(self.final_layer.linear.bias, 0)
+
+# def forward(self, x, t):
+# """
+# x.shape = (bsz, input_dim)
+# t.shape = (bsz,)
+# """
+
+# x = self.input_proj(x)
+# t = self.time_embed(t)
+
+# y = t
+
+# for block in self.res_blocks:
+# if self.grad_checkpointing and self.training:
+# x = checkpoint(block, x, y, use_reentrant=True)
+# else:
+# x = block(x, y)
+
+# return self.final_layer(x, y)
+
+class FlowMatchingHead(nn.Module):
+
+ def __init__(self, input_dim, out_dim, dim=1536, layers=12, mlp_ratio=1.0):
+ super(FlowMatchingHead, self).__init__()
+ self.net = SimpleMLPAdaLN(input_dim=input_dim, out_dim=out_dim, dim=dim, layers=layers, mlp_ratio=mlp_ratio)
+
+ @property
+ def dtype(self):
+ return self.net.input_proj.weight.dtype
+
+ @property
+ def device(self):
+ return self.net.input_proj.weight.device
+
+ def forward(self, x, t):
+ x = self.net(x, t)
+ return x
+
+
+def precompute_freqs_cis_2d(dim: int, height: int, width:int, theta: float = 10000.0, scale=16.0):
+ # assert H * H == end
+ # flat_patch_pos = torch.linspace(-1, 1, end) # N = end
+ x_pos = torch.linspace(0, scale, width)
+ y_pos = torch.linspace(0, scale, height)
+ y_pos, x_pos = torch.meshgrid(y_pos, x_pos, indexing="ij")
+ y_pos = y_pos.reshape(-1)
+ x_pos = x_pos.reshape(-1)
+ freqs = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim)) # Hc/4
+ x_freqs = torch.outer(x_pos, freqs).float() # N Hc/4
+ y_freqs = torch.outer(y_pos, freqs).float() # N Hc/4
+ x_cis = torch.polar(torch.ones_like(x_freqs), x_freqs)
+ y_cis = torch.polar(torch.ones_like(y_freqs), y_freqs)
+ freqs_cis = torch.cat([x_cis.unsqueeze(dim=-1), y_cis.unsqueeze(dim=-1)], dim=-1) # N,Hc/4,2
+ freqs_cis = freqs_cis.reshape(height*width, -1)
+ return freqs_cis
+
+class NerfEmbedder(nn.Module):
+ def __init__(self, in_channels, hidden_size_input, max_freqs):
+ super().__init__()
+ self.max_freqs = max_freqs
+ self.hidden_size_input = hidden_size_input
+ self.embedder = nn.Sequential(
+ nn.Linear(in_channels+max_freqs**2, hidden_size_input, bias=True),
+ )
+
+ @lru_cache
+ def fetch_pos(self, patch_size, device, dtype):
+ pos = precompute_freqs_cis_2d(self.max_freqs ** 2 * 2, patch_size, patch_size).real
+ pos = pos[None, :, :].to(device=device, dtype=dtype)
+ return pos
+
+
+ def forward(self, inputs):
+ B, P2, C = inputs.shape
+ patch_size = int(P2 ** 0.5)
+ device = inputs.device
+ dtype = inputs.dtype
+ dct = self.fetch_pos(patch_size, device, dtype)
+ dct = dct.repeat(B, 1, 1)
+ inputs = torch.cat([inputs, dct], dim=-1)
+ inputs = self.embedder(inputs)
+ return inputs
+
+class SimpleMLPAdaLN(nn.Module):
+ """
+ The MLP for Diffusion Loss.
+ :param in_channels: channels in the input Tensor.
+ :param model_channels: base channel count for the model.
+ :param out_channels: channels in the output Tensor.
+ :param z_channels: channels in the condition.
+ :param num_res_blocks: number of residual blocks per downsample.
+ """
+
+ def __init__(
+ self,
+ in_channels,
+ model_channels,
+ out_channels,
+ z_channels,
+ num_res_blocks,
+ patch_size,
+ grad_checkpointing=False
+ ):
+ super().__init__()
+
+ self.in_channels = in_channels
+ self.model_channels = model_channels
+ self.out_channels = out_channels
+ self.num_res_blocks = num_res_blocks
+ self.grad_checkpointing = grad_checkpointing
+ self.patch_size = patch_size
+
+ self.cond_embed = nn.Linear(z_channels, patch_size**2*model_channels)
+
+ self.input_proj = nn.Linear(in_channels, model_channels)
+
+ res_blocks = []
+ for i in range(num_res_blocks):
+ res_blocks.append(ResBlock(
+ model_channels,
+ ))
+
+ self.res_blocks = nn.ModuleList(res_blocks)
+ self.final_layer = FinalLayer(model_channels, out_channels)
+
+ self.initialize_weights()
+
+ def initialize_weights(self):
+ def _basic_init(module):
+ if isinstance(module, nn.Linear):
+ torch.nn.init.xavier_uniform_(module.weight)
+ if module.bias is not None:
+ nn.init.constant_(module.bias, 0)
+ self.apply(_basic_init)
+
+ # Zero-out adaLN modulation layers
+ for block in self.res_blocks:
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
+
+ # Zero-out output layers
+ nn.init.constant_(self.final_layer.linear.weight, 0)
+ nn.init.constant_(self.final_layer.linear.bias, 0)
+
+ def forward(self, x, c):
+ """
+ Apply the model to an input batch.
+ :param x: an [N x C] Tensor of inputs.
+ :param t: a 1-D batch of timesteps.
+ :param c: conditioning from AR transformer.
+ :return: an [N x C] Tensor of outputs.
+ """
+ x = self.input_proj(x)
+ c = self.cond_embed(c)
+
+ y = c.reshape(-1, self.patch_size**2, self.model_channels)
+
+ for block in self.res_blocks:
+ x = block(x, y)
+
+ return self.final_layer(x)
+
+
+class FinalLayer(nn.Module):
+ """
+ The final layer adopted from DiT.
+ """
+ def __init__(self, model_channels, out_channels):
+ super().__init__()
+ self.norm_final = nn.LayerNorm(model_channels, elementwise_affine=False, eps=1e-6)
+ self.linear = nn.Linear(model_channels, out_channels, bias=True)
+
+ def forward(self, x):
+ x = self.norm_final(x)
+ x = self.linear(x)
+ return x
+
+#################################################################################
+# Sine/Cosine Positional Embedding Functions #
+#################################################################################
+# https://github.com/facebookresearch/mae/blob/main/util/pos_embed.py
+
+
+def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0, pe_interpolation=1.0):
+ """
+ grid_size: int of the grid height and width
+ return:
+ pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
+ """
+ grid_h = np.arange(grid_size, dtype=np.float32) / pe_interpolation
+ grid_w = np.arange(grid_size, dtype=np.float32) / pe_interpolation
+ grid = np.meshgrid(grid_w, grid_h) # here w goes first
+ grid = np.stack(grid, axis=0)
+
+ grid = grid.reshape([2, 1, grid_size, grid_size])
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
+ if cls_token and extra_tokens > 0:
+ pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0)
+ return pos_embed
+
+
+def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
+ assert embed_dim % 2 == 0
+
+ # use half of dimensions to encode grid_h
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
+
+ emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
+ return emb
+
+
+def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
+ """
+ embed_dim: output dimension for each position
+ pos: a list of positions to be encoded: size (M,)
+ out: (M, D)
+ """
+ assert embed_dim % 2 == 0
+ omega = np.arange(embed_dim // 2, dtype=np.float64)
+ omega /= embed_dim / 2.0
+ omega = 1.0 / 10000**omega # (D/2,)
+
+ pos = pos.reshape(-1) # (M,)
+ out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
+
+ emb_sin = np.sin(out) # (M, D/2)
+ emb_cos = np.cos(out) # (M, D/2)
+
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
+ return emb
+
+# --------------------------------------------------------
+# Interpolate position embeddings for high-resolution
+# References:
+# DeiT: https://github.com/facebookresearch/deit
+# --------------------------------------------------------
+def interpolate_pos_embed(model_path, pe_key: str = "gen_pos_embed", new_len: int = 4096):
+ state_dict = torch.load(model_path, map_location="cpu")
+
+ pos_embed_1d = state_dict[pe_key]
+ _, ori_len, embed_dim = pos_embed_1d.shape
+
+ ori_size = int(ori_len**0.5)
+ new_size = int(new_len**0.5)
+
+ if ori_size != new_size:
+ logger.info("Position interpolate from %dx%d to %dx%d" % (ori_size, ori_size, new_size, new_size))
+ pos_embed_2d = pos_embed_1d.reshape(-1, ori_size, ori_size, embed_dim).permute(0, 3, 1, 2)
+ pos_embed_2d = torch.nn.functional.interpolate(
+ pos_embed_2d, size=(new_size, new_size), mode="bicubic", align_corners=False
+ )
+ pos_embed_1d = pos_embed_2d.permute(0, 2, 3, 1).flatten(1, 2)
+ state_dict[pe_key] = pos_embed_1d
+
+ torch.save(state_dict, model_path)
+
+class PositionEmbedding(nn.Module):
+ def __init__(self, max_num_patch_per_side, hidden_size):
+ super().__init__()
+ self.max_num_patch_per_side = max_num_patch_per_side
+ self.hidden_size = hidden_size
+ self.pos_embed = nn.Parameter(
+ torch.zeros(max_num_patch_per_side ** 2, hidden_size),
+ requires_grad=False
+ )
+ self._init_weights()
+
+ def _init_weights(self):
+ # Initialize (and freeze) pos_embed by sin-cos embedding:
+ pos_embed = get_2d_sincos_pos_embed(self.hidden_size, self.max_num_patch_per_side)
+ self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float())
+
+ def forward(self, position_ids):
+ return self.pos_embed[position_ids]
+
+
+class ResidualConvBlock(nn.Module):
+ def __init__(self, channels: int):
+ super().__init__()
+ self.block = nn.Sequential(
+ nn.Conv2d(channels, channels, kernel_size=3, padding=1),
+ nn.SiLU(),
+ nn.Conv2d(channels, channels, kernel_size=3, padding=1),
+ )
+ nn.init.zeros_(self.block[2].weight)
+ nn.init.zeros_(self.block[2].bias)
+
+ def forward(self, x):
+ return x + self.block(x)
+
+
+class PostConvSmoother(nn.Module):
+ def __init__(self, in_channels=3, hidden_channels=64, num_blocks=3):
+ super().__init__()
+ self.in_proj = nn.Conv2d(in_channels, hidden_channels, kernel_size=3, padding=1)
+ self.blocks = nn.Sequential(*[ResidualConvBlock(hidden_channels) for _ in range(num_blocks)])
+ self.out_proj = nn.Conv2d(hidden_channels, in_channels, kernel_size=1)
+
+ nn.init.zeros_(self.out_proj.weight)
+ nn.init.zeros_(self.out_proj.bias)
+
+ def forward(self, x):
+ h = self.in_proj(x)
+ h = self.blocks(h)
+ return x + self.out_proj(h)
+
+
+class ProgressiveConvDecoder(nn.Module):
+ def __init__(self, hidden_dim=4096, out_channels=3):
+ super().__init__()
+
+ # self.proj = nn.Linear(hidden_dim, 1024)
+ # self.act = nn.SiLU()
+
+ self.up_blocks = nn.ModuleList([
+ nn.Sequential(
+ nn.Upsample(scale_factor=2, mode='nearest'),
+ nn.Conv2d(hidden_dim, 512, kernel_size=3, padding=1),
+ nn.GroupNorm(32, 512),
+ nn.SiLU()
+ ),
+ nn.Sequential(
+ nn.Upsample(scale_factor=2, mode='nearest'),
+ nn.Conv2d(512, 256, kernel_size=3, padding=1),
+ nn.GroupNorm(32, 256),
+ nn.SiLU()
+ ),
+ nn.Sequential(
+ nn.Upsample(scale_factor=2, mode='nearest'),
+ nn.Conv2d(256, 64, kernel_size=3, padding=1),
+ nn.GroupNorm(32, 64),
+ nn.SiLU()
+ ),
+ nn.Sequential(
+ nn.Upsample(scale_factor=2, mode='nearest'),
+ nn.Conv2d(64, 32, kernel_size=3, padding=1),
+ nn.GroupNorm(16, 32),
+ nn.SiLU()
+ ),
+ nn.Sequential(
+ nn.Upsample(scale_factor=2, mode='nearest'),
+ nn.Conv2d(32, 16, kernel_size=3, padding=1),
+ nn.SiLU()
+ )
+ ])
+
+ self.out_conv = nn.Conv2d(16, out_channels, kernel_size=3, padding=1)
+
+ def forward(self, x_2d):
+ # B, C, H, W = x_2d.shape
+ # x = x_2d.permute(0, 2, 3, 1).contiguous() # (B, H, W, C)
+ # x = self.proj(x)
+ # x = self.act(x)
+ # x = x.permute(0, 3, 1, 2).contiguous() # (B, 512, H, W)
+ x = x_2d
+ for block in self.up_blocks:
+ x = block(x)
+
+ out = self.out_conv(x)
+ return out
+
+
+class PatchDecoder_postps(nn.Module):
+ def __init__(self):
+ super().__init__()
+ # layer 1: H/32 -> H/8 (4x upscale)
+
+ self.conv1 = nn.Conv2d(4096, 4096, kernel_size=3, padding=1)
+ self.ps1 = nn.PixelShuffle(4)
+ self.act1 = nn.GELU()
+
+ # layer 2: H/8 -> H (8x upscale)
+ self.conv2 = nn.Conv2d(256, 192, kernel_size=3, padding=1)
+ self.ps2 = nn.PixelShuffle(8)
+
+ def forward(self, x):
+ # x shape: [B, 4096, H/32, W/32]
+ x = self.ps1(self.act1(self.conv1(x))) # -> [B, 256, H/8, W/8]
+ x = self.ps2(self.conv2(x)) # -> [B, 3, H, W]
+ return x
+
+
+class PatchDecoder_preps(nn.Module):
+ def __init__(self):
+ super().__init__()
+ # layer 1: H/32 -> H/16 (2x upscale)
+ self.ps1 = nn.PixelShuffle(2)
+ self.conv1 = nn.Conv2d(1024, 1024, kernel_size=3, padding=1)
+ self.act1 = nn.GELU()
+
+ # layer 2: H/16 -> H/8 (2x upscale)
+ self.ps2 = nn.PixelShuffle(2)
+ self.conv2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
+ self.act2 = nn.GELU()
+
+ # layer 3: H/8 -> H (8x upscale)
+ self.ps3 = nn.PixelShuffle(8)
+ self.conv3 = nn.Conv2d(4, 3, kernel_size=3, padding=1)
+
+ def forward(self, x):
+ # x shape: [B, 4096, H/32, W/32]
+ x = self.act1(self.conv1(self.ps1((x)))) # -> [B, 256, H/16, W/16]
+ x = self.act2(self.conv2(self.ps2((x)))) # -> [B, 256, H/8, W/8]
+ x = self.conv3(self.ps3((x))) # -> [B, 3, H, W]
+ return x
+
+class PatchDecoder_preps1(nn.Module):
+ def __init__(self):
+ super().__init__()
+ # layer 1: H/32 -> H/16 (2x upscale)
+ self.ps1 = nn.PixelShuffle(2)
+ self.conv1 = nn.Conv2d(1024, 1024, kernel_size=3, padding=1)
+ self.act1 = nn.GELU()
+
+ # layer 2: H/16 -> H/8 (2x upscale)
+ self.ps2 = nn.PixelShuffle(2)
+ self.conv2 = nn.Conv2d(256, 192, kernel_size=3, padding=1)
+
+ # layer 3: H/8 -> H (8x upscale)
+ self.ps3 = nn.PixelShuffle(8)
+
+ def forward(self, x):
+ # x shape: [B, 4096, H/32, W/32]
+ x = self.act1(self.conv1(self.ps1((x)))) # -> [B, 256, H/16, W/16]
+ x = self.ps3(self.conv2(self.ps2((x)))) # -> [B, 256, H/8, W/8]
+ return x
+
+class ConvDecoder(nn.Module):
+ def __init__(self, input_dim=4096, hidden_dim=1024):
+ super().__init__()
+ # layer 1: H/32 -> H/16 (2x upscale)
+ self.ps1 = nn.PixelShuffle(2)
+ self.conv1 = nn.Conv2d(input_dim // 4, hidden_dim, kernel_size=3, padding=1)
+ self.act1 = nn.GELU()
+
+ # layer 2: H/16 -> H/8 (2x upscale)
+ self.ps2 = nn.PixelShuffle(2)
+ self.conv2 = nn.Conv2d(hidden_dim // 4, 192, kernel_size=3, padding=1)
+
+ # layer 3: H/8 -> H (8x upscale)
+ self.ps3 = nn.PixelShuffle(8)
+
+ def forward(self, x):
+ x = self.act1(self.conv1(self.ps1((x))))
+ x = self.ps3(self.conv2(self.ps2((x))))
+ return x
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/modeling_neo_chat.py b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_neo_chat.py
new file mode 100644
index 000000000..b4420a1a5
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_neo_chat.py
@@ -0,0 +1,1992 @@
+from typing import List, Optional, Tuple, Union
+import math
+import os
+import torch.utils.checkpoint
+from torch import nn
+import transformers
+from torch.nn import CrossEntropyLoss
+from transformers import GenerationConfig
+from transformers.modeling_outputs import CausalLMOutputWithPast
+from transformers.modeling_utils import PreTrainedModel
+from transformers.utils import logging
+
+from .configuration_neo_chat import NEOChatConfig, NEOMoELLMConfig
+from .conversation import get_conv_template
+from .modeling_neo_vit import NEOVisionModel
+from .modeling_qwen3 import Qwen3ForCausalLM, create_block_causal_mask
+from .modeling_qwen3_moe import Qwen3MoeForCausalLM
+from .modeling_fm_modules import PositionEmbedding, TimestepEmbedder, FlowMatchingHead, RMSNorm, NerfEmbedder, SimpleMLPAdaLN, ConvDecoder
+from .utils import load_image_native, SYSTEM_MESSAGE_FOR_GEN
+
+logger = logging.get_logger(__name__)
+
+
+def version_cmp(v1, v2, op='eq'):
+ import operator
+
+ from packaging import version
+ op_func = getattr(operator, op)
+ return op_func(version.parse(v1), version.parse(v2))
+
+def prepare_flash_kv_cache(
+ past_key_values,
+ current_len: int,
+ batch_size: int,
+):
+ """
+ Convert prefix cache from [B, H, S, D] to flash-attn friendly [B, S, H, D],
+ and preallocate full KV buffer for [prefix + current].
+
+ This is done once before denoising loop.
+ """
+ if past_key_values is None:
+ return
+
+ for layer in past_key_values.layers:
+ past_k = layer.keys
+ past_v = layer.values
+
+ if past_k is None or past_v is None:
+ layer.flash_prefix_len = 0
+ layer.flash_total_len = current_len
+ layer.flash_k_cache = None
+ layer.flash_v_cache = None
+ continue
+
+ # original cache layout assumed: [B, H, S, D]
+ past_k_flash = past_k.transpose(1, 2).contiguous() # [B, S, H, D]
+ past_v_flash = past_v.transpose(1, 2).contiguous() # [B, S, H, D]
+
+ prefix_len = past_k_flash.shape[1]
+ total_len = prefix_len + current_len
+
+ k_cache = torch.empty(
+ (batch_size, total_len, past_k_flash.shape[2], past_k_flash.shape[3]),
+ device=past_k_flash.device,
+ dtype=past_k_flash.dtype,
+ )
+ v_cache = torch.empty(
+ (batch_size, total_len, past_v_flash.shape[2], past_v_flash.shape[3]),
+ device=past_v_flash.device,
+ dtype=past_v_flash.dtype,
+ )
+
+ k_cache[:, :prefix_len].copy_(past_k_flash)
+ v_cache[:, :prefix_len].copy_(past_v_flash)
+
+ layer.flash_prefix_len = prefix_len
+ layer.flash_total_len = total_len
+ layer.flash_k_cache = k_cache
+ layer.flash_v_cache = v_cache
+
+def clear_flash_kv_cache(past_key_values):
+ if past_key_values is None:
+ return
+ for layer in past_key_values.layers:
+ if hasattr(layer, "flash_prefix_len"):
+ delattr(layer, "flash_prefix_len")
+ if hasattr(layer, "flash_total_len"):
+ delattr(layer, "flash_total_len")
+ if hasattr(layer, "flash_k_cache"):
+ delattr(layer, "flash_k_cache")
+ if hasattr(layer, "flash_v_cache"):
+ delattr(layer, "flash_v_cache")
+
+def optimized_scale(positive_flat, negative_flat):
+ # Force the divisor computation to float32 regardless of the surrounding
+ # autocast (the squared-norm/division is what we don't want in fp16/bf16).
+ # ``device_type`` is taken from the input so this runs equally on CUDA and
+ # XPU; ``mps`` is rerouted to ``cpu`` because torch.autocast rejects it.
+ device_type = positive_flat.device.type
+ if device_type == "mps":
+ device_type = "cpu"
+ with torch.autocast(device_type=device_type, enabled=False):
+ positive_flat = positive_flat.float()
+ negative_flat = negative_flat.float()
+
+ # Calculate dot production
+ dot_product = torch.sum(positive_flat * negative_flat, dim=1, keepdim=True)
+
+ # Squared norm of uncondition
+ squared_norm = torch.sum(negative_flat ** 2, dim=1, keepdim=True) + 1e-8
+
+ # st_star = v_cond^T * v_uncond / ||v_uncond||^2
+ st_star = dot_product / squared_norm
+
+ return st_star
+
+def build_abs_positions_from_grid_hw(grid_hw: torch.Tensor, device=None):
+ """
+ Compute patch coordinates (x, y)
+
+ Args:
+ grid_hw: (B, 2) tensor representing (H, W) per image
+ """
+ device = grid_hw.device
+ B = grid_hw.shape[0]
+
+ # Get the number of patches per image
+ H = grid_hw[:, 0]
+ W = grid_hw[:, 1]
+ N = H * W
+ N_total = N.sum()
+
+ # Create the batch index for each patch (B x patch count)
+ patch_to_sample = torch.repeat_interleave(torch.arange(B, device=device), N) # (N_total,)
+
+ # Generate intra-image patch index (row-major order)
+ patch_id_within_image = torch.arange(N_total, device=device)
+ patch_id_within_image = patch_id_within_image - torch.cumsum(
+ torch.cat([torch.tensor([0], device=device), N[:-1]]), dim=0
+ )[patch_to_sample]
+
+ # Get H/W for each patch according to its image
+ W_per_patch = W[patch_to_sample]
+ abs_x = patch_id_within_image % W_per_patch
+ abs_y = patch_id_within_image // W_per_patch
+
+ return abs_x, abs_y
+
+
+class NEOChatModel(PreTrainedModel):
+ config_class = NEOChatConfig
+ main_input_name = 'pixel_values'
+ base_model_prefix = 'language_model'
+ _supports_flash_attn_2 = True
+ supports_gradient_checkpointing = True
+ _no_split_modules = [
+ "NEOVisionModel",
+ "Qwen3DecoderLayer",
+ "Qwen3MoeDecoderLayer",
+ ]
+ _denoise_offload_module_paths = (
+ "language_model.model.embed_tokens",
+ "language_model.lm_head",
+ )
+
+ # support transformers 4.51.+
+ _tp_plan = ''
+
+ def __init__(self, config: NEOChatConfig, vision_model=None, language_model=None, use_flash_attn=True):
+ super().__init__(config)
+
+ assert version_cmp(transformers.__version__, '4.37.0', 'ge')
+ patch_size = config.vision_config.patch_size
+ self.patch_size = patch_size
+ self.template = config.template
+ self.downsample_ratio = config.downsample_ratio
+ config.llm_config._attn_implementation = 'eager'
+
+ if vision_model is not None:
+ self.vision_model = vision_model
+ else:
+ self.vision_model = NEOVisionModel(config.vision_config)
+ vision_model_mot_gen = NEOVisionModel(config.vision_config)
+ if language_model is not None:
+ self.language_model = language_model
+ else:
+ # Pick the right backbone class based on the LLM config: dense
+ # Qwen3 (DANCE family) or Qwen3-MoE (A3B family). The two share
+ # the same NEO-Unify two-branch attention/norm layout, so the
+ # rest of this class works against either.
+ if isinstance(config.llm_config, NEOMoELLMConfig):
+ self.language_model = Qwen3MoeForCausalLM(config.llm_config)
+ else:
+ self.language_model = Qwen3ForCausalLM(config.llm_config)
+
+ merge_size = int(1 / self.downsample_ratio)
+ output_dim = 3*(patch_size*merge_size)**2
+ llm_hidden_size = self.config.llm_config.hidden_size
+ self.use_deep_fm_head = self.config.fm_head_layers > 2
+ self.use_pixel_head = self.config.use_pixel_head
+ if self.use_deep_fm_head:
+ fm_head = FlowMatchingHead(llm_hidden_size, output_dim, dim=self.config.fm_head_dim, layers=self.config.fm_head_layers, mlp_ratio=self.config.fm_head_mlp_ratio)
+ else:
+ fm_head = nn.Sequential(
+ nn.Linear(llm_hidden_size, 4096, bias=True),
+ nn.GELU(),
+ nn.Linear(4096, output_dim, bias=True),
+ )
+
+ timestep_embedder = TimestepEmbedder(llm_hidden_size)
+ self.fm_modules = nn.ModuleDict(
+ {
+ "vision_model_mot_gen": vision_model_mot_gen,
+ "timestep_embedder": timestep_embedder,
+ "fm_head": fm_head
+ }
+ )
+
+ if self.use_pixel_head:
+ self.fm_modules["fm_head"] = ConvDecoder(llm_hidden_size)
+
+ self.concat_time_token_num = config.concat_time_token_num
+ self.noise_scale = config.noise_scale
+ self.noise_scale_mode = config.noise_scale_mode
+ self.noise_scale_base_image_seq_len = config.noise_scale_base_image_seq_len
+ self.add_noise_scale_embedding = config.add_noise_scale_embedding
+ self.noise_scale_max_value = config.noise_scale_max_value
+ self.time_schedule = config.time_schedule
+ self.time_shift_type = config.time_shift_type
+ self.base_shift = config.base_shift
+ self.max_shift = config.max_shift
+ self.base_image_seq_len = config.base_image_seq_len
+ self.max_image_seq_len = config.max_image_seq_len
+
+ if self.add_noise_scale_embedding:
+ noise_scale_embedder = TimestepEmbedder(llm_hidden_size)
+ self.fm_modules['noise_scale_embedder'] = noise_scale_embedder
+
+
+ self.img_context_token_id = None
+ self.img_start_token_id = 151670
+ self.last_think_content = ""
+ self.conv_template = get_conv_template(self.template)
+ self.system_message = self.conv_template.system_message
+
+ # Transformers 5 builds composite-model metadata (including
+ # ``all_tied_weights_keys``) in ``post_init``. Without this call a
+ # real checkpoint reaches the final loading pass with that metadata
+ # missing, even though the nested language model initialized it.
+ self.post_init()
+
+ def _notify_layer_offload_phase(self, phase: str) -> None:
+ callback = getattr(self, "_layer_offload_phase_callback", None)
+ if callback is not None:
+ callback(phase)
+
+ def forward(
+ self,
+ pixel_values: torch.FloatTensor,
+ input_ids: torch.LongTensor = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ image_flags: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[List[torch.FloatTensor]] = None,
+ labels: Optional[torch.LongTensor] = None,
+ use_cache: Optional[bool] = None,
+ output_attentions: Optional[bool] = None,
+ output_hidden_states: Optional[bool] = None,
+ return_dict: Optional[bool] = None,
+ ) -> Union[Tuple, CausalLMOutputWithPast]:
+ raise NotImplementedError('forward')
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
+
+ image_flags = image_flags.squeeze(-1)
+ input_embeds = self.language_model.get_input_embeddings()(input_ids).clone()
+
+ vit_embeds = self.extract_feature(pixel_values)
+ vit_embeds = vit_embeds[image_flags == 1]
+
+ B, N, C = input_embeds.shape
+ input_embeds = input_embeds.reshape(B * N, C)
+
+ # if torch.distributed.is_initialized() and torch.distributed.get_rank() == 0:
+ # print(f'dynamic ViT batch size: {vit_batch_size}, images per sample: {vit_batch_size / B}, dynamic token length: {N}')
+
+ input_ids = input_ids.reshape(B * N)
+ selected = (input_ids == self.img_context_token_id)
+ try:
+ input_embeds[selected] = input_embeds[selected] * 0.0 + vit_embeds.reshape(-1, C)
+ except Exception as e:
+ vit_embeds = vit_embeds.reshape(-1, C)
+ print(f'warning: {e}, input_embeds[selected].shape={input_embeds[selected].shape}, '
+ f'vit_embeds.shape={vit_embeds.shape}')
+ n_token = min(selected.sum(), vit_embeds.size(0))
+ input_embeds[selected][:n_token] = input_embeds[selected][:n_token] * 0.0 + vit_embeds[:n_token]
+
+ input_embeds = input_embeds.reshape(B, N, C)
+
+ outputs = self.language_model(
+ inputs_embeds=input_embeds,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ output_attentions=output_attentions,
+ output_hidden_states=output_hidden_states,
+ return_dict=return_dict,
+ )
+ logits = outputs.logits
+
+ loss = None
+ if labels is not None:
+ # Shift so that tokens < n predict n
+ shift_logits = logits[..., :-1, :].contiguous()
+ shift_labels = labels[..., 1:].contiguous()
+ # Flatten the tokens
+ loss_fct = CrossEntropyLoss()
+ shift_logits = shift_logits.view(-1, self.language_model.config.vocab_size)
+ shift_labels = shift_labels.view(-1)
+ # Enable model parallelism
+ shift_labels = shift_labels.to(shift_logits.device)
+ loss = loss_fct(shift_logits, shift_labels)
+
+ if not return_dict:
+ output = (logits,) + outputs[1:]
+ return (loss,) + output if loss is not None else output
+
+ return CausalLMOutputWithPast(
+ loss=loss,
+ logits=logits,
+ past_key_values=outputs.past_key_values,
+ hidden_states=outputs.hidden_states,
+ attentions=outputs.attentions,
+ )
+
+ def extract_feature(self, pixel_values, gen_model=False, grid_hw=None):
+ if gen_model:
+ return self.fm_modules['vision_model_mot_gen'](pixel_values=pixel_values,
+ output_hidden_states=False,
+ return_dict=True,
+ grid_hw=grid_hw).last_hidden_state
+ else:
+ return self.vision_model(pixel_values=pixel_values,
+ output_hidden_states=False,
+ return_dict=True,
+ grid_hw=grid_hw).last_hidden_state
+
+ def batch_chat(self, tokenizer, pixel_values, questions, generation_config, num_patches_list=None,
+ history=None, return_history=False, IMG_START_TOKEN='
', IMG_END_TOKEN='',
+ IMG_CONTEXT_TOKEN='', verbose=False, image_counts=None):
+ raise NotImplementedError('batch_chat')
+ if history is not None or return_history:
+ print('Now multi-turn chat is not supported in batch_chat.')
+ raise NotImplementedError
+
+ if image_counts is not None:
+ num_patches_list = image_counts
+ print('Warning: `image_counts` is deprecated. Please use `num_patches_list` instead.')
+
+ img_context_token_id = tokenizer.convert_tokens_to_ids(IMG_CONTEXT_TOKEN)
+ self.img_context_token_id = img_context_token_id
+
+ if verbose and pixel_values is not None:
+ image_bs = pixel_values.shape[0]
+ print(f'dynamic ViT batch size: {image_bs}')
+
+ queries = []
+ for idx, num_patches in enumerate(num_patches_list):
+ question = questions[idx]
+ if pixel_values is not None and '' not in question:
+ question = '\n' + question
+ template = get_conv_template(self.template)
+ template.system_message = self.system_message
+ template.append_message(template.roles[0], question)
+ template.append_message(template.roles[1], None)
+ query = template.get_prompt()
+
+ image_tokens = IMG_START_TOKEN + IMG_CONTEXT_TOKEN + IMG_END_TOKEN
+ query = query.replace('', image_tokens, 1)
+ queries.append(query)
+
+ tokenizer.padding_side = 'left'
+ model_inputs = tokenizer(queries, return_tensors='pt', padding=True)
+ input_ids = model_inputs['input_ids'].to(self.device)
+ attention_mask = model_inputs['attention_mask'].to(self.device)
+ eos_token_id = tokenizer.convert_tokens_to_ids(template.sep.strip())
+ generation_config['eos_token_id'] = eos_token_id
+ generation_output = self.generate(
+ pixel_values=pixel_values,
+ input_ids=input_ids,
+ attention_mask=attention_mask,
+ **generation_config
+ )
+ responses = tokenizer.batch_decode(generation_output, skip_special_tokens=True)
+ responses = [response.split(template.sep.strip())[0].strip() for response in responses]
+ return responses
+
+ def patchify(self, images, patch_size, channel_first=False):
+ """
+ images: (N, 3, H, W)
+ x: (N, L, patch_size**2 *3)
+ """
+ h, w = images.shape[2] // patch_size, images.shape[3] // patch_size
+ x = images.reshape(shape=(images.shape[0], 3, h, patch_size, w, patch_size))
+
+ if channel_first:
+ x = torch.einsum('nchpwq->nhwcpq', x)
+ else:
+ x = torch.einsum('nchpwq->nhwpqc', x)
+
+ x = x.reshape(shape=(images.shape[0], h * w, patch_size**2 * 3))
+ return x
+
+ def unpatchify(sle, x, patch_size, h=None, w=None):
+ """
+ x: (N, L, patch_size**2 *3)
+ images: (N, 3, H, W)
+ """
+ if h is None or w is None:
+ h = w = int(x.shape[1]**.5)
+ else:
+ h = h // patch_size
+ w = w // patch_size
+ x = x.reshape(shape=(x.shape[0], h, w, patch_size, patch_size, 3))
+ x = torch.einsum('nhwpqc->nchpwq', x)
+ images = x.reshape(shape=(x.shape[0], 3, h * patch_size, w * patch_size))
+ return images
+
+ def _euler_step(self, v_pred, z, t, t_next):
+ z_next = z + (t_next - t) * v_pred
+ return z_next
+
+ def _calculate_dynamic_mu(self, image_seq_len: int) -> float:
+ denom = self.max_image_seq_len - self.base_image_seq_len
+ if denom == 0:
+ return float(self.base_shift)
+ m = (self.max_shift - self.base_shift) / denom
+ b = self.base_shift - m * self.base_image_seq_len
+ return float(image_seq_len) * m + b
+
+ def _apply_time_schedule(self, t: torch.Tensor, image_seq_len: int, timestep_shift: float) -> torch.Tensor:
+ self.time_schedule = "standard"
+ sigma = 1 - t
+ if timestep_shift != 1:
+ self.time_schedule = "standard"
+ if self.time_schedule == "standard":
+ shift = timestep_shift
+ sigma = shift * sigma / (1 + (shift - 1) * sigma)
+ elif self.time_schedule == "dynamic":
+ mu = self._calculate_dynamic_mu(image_seq_len)
+ mu_t = t.new_tensor(mu)
+ if self.time_shift_type == "exponential":
+ shift = torch.exp(mu_t)
+ sigma = shift * sigma / (1 + (shift - 1) * sigma)
+ elif self.time_shift_type == "linear":
+ sigma = mu_t / (mu_t + (1 / sigma - 1))
+ else:
+ raise ValueError(f"Unsupported time_shift_type: {self.time_shift_type}")
+ else:
+ raise ValueError(f"Unsupported time_schedule: {self.time_schedule}")
+ return 1 - sigma
+
+ def _build_t2i_query(self, prompt_text, system_message=None, append_text=None):
+ template = get_conv_template(self.template)
+ template.system_message = self.system_message if system_message is None else system_message
+ template.append_message(template.roles[0], prompt_text)
+ template.append_message(template.roles[1], None)
+ if append_text is not None:
+ return template.get_prompt() + append_text
+ return template.get_prompt()
+
+ def _build_t2i_text_inputs(self, tokenizer, query: str):
+ model_inputs = tokenizer(query, return_tensors="pt")
+ input_ids = model_inputs["input_ids"].to(self.device)
+
+ t_idx = torch.arange(0, input_ids.shape[1], dtype=torch.long, device=input_ids.device)
+ h_idx = torch.zeros_like(t_idx)
+ w_idx = torch.zeros_like(t_idx)
+ indexes = torch.stack([t_idx, h_idx, w_idx], dim=0)
+
+ attention_mask = {"full_attention": create_block_causal_mask(indexes[0])}
+ return input_ids, indexes, attention_mask
+
+ def _build_t2i_image_indexes(self, token_h, token_w, text_len, device):
+ t_image = torch.full((token_h * token_w,), text_len, dtype=torch.long, device=device)
+ idx = torch.arange(token_h * token_w, device=device, dtype=torch.long)
+ h_image = idx // token_w
+ w_image = idx % token_w
+ return torch.stack([t_image, h_image, w_image], dim=0)
+
+ def _t2i_prefix_forward(self, input_ids, indexes, attention_mask):
+ out = self.language_model.model(
+ input_ids=input_ids,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ use_cache=True,
+ )
+ return out.past_key_values, out.last_hidden_state
+
+ def _it2i_prefix_forward(self, input_imbeds, indexes, attention_mask, gen_indicators=None):
+ out = self.language_model.model(
+ inputs_embeds=input_imbeds,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ use_cache=True,
+ image_gen_indicators=gen_indicators.view(1, -1) if gen_indicators is not None else None
+ )
+ return out.past_key_values, out.last_hidden_state
+
+ def _think_prefix_forward(self, **kwargs):
+ """Build the Think prefix cache without materialising per-token logits."""
+ return self.language_model(use_cache=True, logits_to_keep=1, **kwargs)
+
+ def _append_text_tokens_to_cache(self, cache, t_idx, input_ids):
+ if input_ids.shape[1] == 0:
+ return t_idx
+
+ device = input_ids.device
+ seq_len = input_ids.shape[1]
+ inputs_embeds = self.language_model.get_input_embeddings()(input_ids)
+
+ t_indexes = torch.arange(t_idx + 1, t_idx + 1 + seq_len, dtype=torch.long, device=device)
+ h_indexes = torch.zeros(seq_len, dtype=torch.long, device=device)
+ w_indexes = torch.zeros(seq_len, dtype=torch.long, device=device)
+ indexes = torch.stack([t_indexes, h_indexes, w_indexes], dim=0)
+
+ past_len = cache.get_seq_length()
+ mask = torch.zeros(1, 1, seq_len, past_len + seq_len, device=device)
+ causal_mask = torch.tril(torch.ones(seq_len, seq_len, device=device))
+ causal_mask = torch.where(causal_mask == 1, 0.0, float('-inf'))
+ mask[:, :, :, past_len:] = causal_mask
+ attention_mask_dict = {"full_attention": mask}
+
+ self.language_model.model(
+ inputs_embeds=inputs_embeds,
+ indexes=indexes,
+ attention_mask=attention_mask_dict,
+ past_key_values=cache,
+ use_cache=True
+ )
+ return t_idx + seq_len
+
+ def _generate_think(
+ self,
+ tokenizer,
+ prefix_outputs,
+ past_key_values,
+ t_idx,
+ IMG_START_TOKEN,
+ max_think_tokens=1024,
+ ):
+ template = get_conv_template(self.template)
+ eos_token_id = tokenizer.convert_tokens_to_ids(template.sep.strip())
+ think_end_token_id = tokenizer.convert_tokens_to_ids('')
+ think_token_ids = []
+ next_token = torch.argmax(prefix_outputs.logits[:, -1, :], dim=-1)
+
+ for _ in range(max_think_tokens):
+ token_item = next_token.item()
+ if token_item == eos_token_id:
+ break
+ if token_item == think_end_token_id:
+ self.language_model.model.current_index = t_idx
+ outputs = self.language_model(
+ input_ids=next_token.unsqueeze(0),
+ past_key_values=past_key_values,
+ use_cache=True
+ )
+ past_key_values = outputs.past_key_values
+ t_idx += 1
+ think_token_ids.append(token_item)
+ break
+
+ think_token_ids.append(token_item)
+
+ self.language_model.model.current_index = t_idx
+ outputs = self.language_model(
+ input_ids=next_token.unsqueeze(0),
+ past_key_values=past_key_values,
+ use_cache=True
+ )
+ past_key_values = outputs.past_key_values
+ t_idx += 1
+
+ next_token = torch.argmax(outputs.logits[:, -1, :], dim=-1)
+
+ append_ids = tokenizer(
+ '\n\n' + IMG_START_TOKEN,
+ return_tensors='pt',
+ add_special_tokens=False,
+ )['input_ids'].to(self.device)
+ t_idx = self._append_text_tokens_to_cache(past_key_values, t_idx, append_ids)
+
+ think_text = tokenizer.decode(think_token_ids, skip_special_tokens=False)
+
+ return past_key_values, t_idx, think_text
+
+ def _t2i_predict_v(self, input_embeds, indexes_image, attn_mask, past_key_values, t, z, image_token_num, timestep_embeddings=None, image_size=None):
+ B, L = z.shape[0], z.shape[1]
+
+ outputs = self.language_model.model(
+ inputs_embeds=input_embeds,
+ image_gen_indicators=torch.ones((input_embeds.shape[0], input_embeds.shape[1]), dtype=torch.bool, device=input_embeds.device),
+ indexes=indexes_image,
+ attention_mask=attn_mask,
+ past_key_values=past_key_values,
+ update_cache=False,
+ use_cache=True,
+ )
+
+ if self.use_pixel_head:
+ merge_size = int(1 / self.downsample_ratio)
+ token_h = image_size[1] // (self.patch_size * merge_size)
+ token_w = image_size[0] // (self.patch_size * merge_size)
+
+ img_reshaped = outputs.last_hidden_state[:, -image_token_num:].view(B, token_h, token_w, -1)
+ img_2d = torch.einsum("b h w c -> b c h w", img_reshaped)
+ img_2d = img_2d.contiguous().view(B, -1, token_h, token_w)
+
+ smoothed_img_2d = self.fm_modules['fm_head'](img_2d)
+
+ smoothed_reshaped = smoothed_img_2d.view(B, 3, token_h, self.patch_size * merge_size, token_w, self.patch_size * merge_size)
+ smoothed_reshaped = torch.einsum("b c h p w q -> b h w p q c", smoothed_reshaped)
+ out_1d = smoothed_reshaped.contiguous().view(B, L, self.patch_size * merge_size * self.patch_size * merge_size * 3)
+ x_pred = out_1d
+ else:
+ if self.use_deep_fm_head:
+ x_pred = self.fm_modules["fm_head"](
+ outputs.last_hidden_state[:, -image_token_num:].view(B*L, -1), t.repeat(B*L)
+ ).view(B, L, -1)
+ else:
+ x_pred = self.fm_modules["fm_head"](
+ outputs.last_hidden_state[:, -image_token_num:].view(B, L, -1)
+ ).view(B, L, -1)
+
+
+ v_pred = (x_pred - z) / (1 - t).clamp_min(self.config.t_eps)
+ return v_pred
+
+ def _build_it2i_inputs(self, tokenizer, query, pixel_values=None, grid_hw=None):
+ model_inputs = tokenizer(query, return_tensors="pt")
+ input_ids = model_inputs["input_ids"].to(self.device)
+
+ indexes = self.get_thw_indexes(input_ids[0], grid_hw)
+
+ attention_mask = {"full_attention": create_block_causal_mask(indexes[0])}
+
+ input_embeds = self.language_model.get_input_embeddings()(input_ids)
+ B, N, C = input_embeds.shape
+ if pixel_values is not None:
+ vit_embeds = self.extract_feature(pixel_values, grid_hw=grid_hw)
+ input_embeds = input_embeds.reshape(B * N, C)
+ input_ids = input_ids.reshape(B * N)
+ selected = (input_ids == self.img_context_token_id)
+ assert selected.sum() != 0
+ input_embeds[selected] = vit_embeds.reshape(-1, C).to(input_embeds.device)
+ input_embeds = input_embeds.reshape(B, N, C)
+
+ return input_embeds, indexes, attention_mask
+
+ @torch.no_grad()
+ def interleave_gen_image_only(
+ self,
+ tokenizer,
+ prompt,
+ gt_text,
+ images=None,
+ gt_images=None,
+ cfg_scale=1.0,
+ img_cfg_scale=1.0,
+ cfg_norm='none',
+ max_images=10,
+ enable_timestep_shift=True,
+ timestep_shift=1.0,
+ image_size=(256, 256),
+ num_steps=30,
+ IMG_START_TOKEN='
',
+ IMG_END_TOKEN='',
+ IMG_CONTEXT_TOKEN='',
+ method='euler',
+ cfg_interval=(0, 1),
+ t_eps=0.02,
+ verbose=False,
+ system_message='',
+ ):
+ self.img_context_token_id = tokenizer.convert_tokens_to_ids(IMG_CONTEXT_TOKEN)
+ self.img_start_token_id = tokenizer.convert_tokens_to_ids(IMG_START_TOKEN)
+ self.config.t_eps = t_eps
+
+ if isinstance(image_size, tuple):
+ image_size_list = [image_size] * max_images
+ elif isinstance(image_size, list) and isinstance(image_size[0], tuple):
+ image_size_list = image_size
+ if len(image_size) < max_images:
+ image_size_list += [image_size_list[-1]] * (max_images - len(image_size_list))
+ else:
+ assert False, "image size should be a tuple or a list of tuple"
+
+ if images is None:
+ images =[]
+
+ image_token_count = prompt.count('')
+ assert len(images) >= image_token_count
+ if len(images) > image_token_count:
+ prompt = "\n" * (len(images) - image_token_count) + prompt
+
+ pixel_values =[]
+ grid_hw =[]
+ for image in images:
+ cur_pixel_values, cur_grid_hw = load_image_native(image, self.patch_size, self.downsample_ratio, min_pixels=512*512, max_pixels=min(2048*2048, (4096*4096)//max(1, len(images))), upscale=False)
+ grid_hw.append(cur_grid_hw.to(self.device))
+ pixel_values.append(cur_pixel_values.to(self.device).to(torch.bfloat16))
+
+ merge_size = int(1 / self.downsample_ratio)
+ pv_tensor = torch.cat(pixel_values) if pixel_values else None
+ ghw_tensor = torch.cat(grid_hw) if grid_hw else None
+
+ # Condition Initial Cache
+ template_cond = get_conv_template(self.template)
+ template_cond.system_message = system_message
+ template_cond.append_message(template_cond.roles[0], prompt)
+ template_cond.append_message(template_cond.roles[1], None)
+ query_cond = template_cond.get_prompt() + '\n\n\n\n'
+
+ def replace_image_tokens(query, grid_hw_list):
+ for i in range(len(grid_hw_list)):
+ num_patch_token = int(grid_hw_list[i][0, 0] * grid_hw_list[i][0, 1] * self.downsample_ratio**2)
+ image_tokens = IMG_START_TOKEN + IMG_CONTEXT_TOKEN * num_patch_token + IMG_END_TOKEN
+ query = query.replace('', image_tokens, 1)
+ return query
+
+ query_cond = replace_image_tokens(query_cond, grid_hw)
+ input_embeds_cond, indexes_cond, attention_mask_cond = self._build_it2i_inputs(tokenizer, query_cond, pv_tensor, ghw_tensor)
+
+ outputs_cond = self.language_model(inputs_embeds=input_embeds_cond, indexes=indexes_cond, attention_mask=attention_mask_cond, use_cache=True)
+ past_key_values_cond = outputs_cond.past_key_values
+ t_index_cond = indexes_cond[0].max().item()
+
+ # Text Uncondition Cache Initial
+ question_text_uncondition = '' * len(images)
+ template_tu = get_conv_template(self.template)
+ template_tu.system_message = self.system_message
+ template_tu.append_message(template_tu.roles[0], question_text_uncondition)
+ template_tu.append_message(template_tu.roles[1], None)
+ query_text_uncond = template_tu.get_prompt()
+ query_text_uncond = replace_image_tokens(query_text_uncond, grid_hw)
+
+ input_embeds_tu, indexes_tu, attention_mask_tu = self._build_it2i_inputs(tokenizer, query_text_uncond, pv_tensor, ghw_tensor)
+ outputs_tu = self.language_model(inputs_embeds=input_embeds_tu, indexes=indexes_tu, attention_mask=attention_mask_tu, use_cache=True)
+ past_key_values_tu = outputs_tu.past_key_values
+ t_index_tu = indexes_tu[0].max().item()
+
+ # Img Uncondition Cache Initial
+ query_img_uncond = self._build_t2i_query("", append_text=IMG_START_TOKEN)
+ input_embeds_iu, indexes_iu, attention_mask_iu = self._build_it2i_inputs(tokenizer, query_img_uncond)
+ outputs_iu = self.language_model(inputs_embeds=input_embeds_iu, indexes=indexes_iu, attention_mask=attention_mask_iu, use_cache=True)
+ past_key_values_iu = outputs_iu.past_key_values
+
+
+ generated_images =[]
+ img_count = 0
+ device = self.device
+
+ def append_ids_to_cache(cache, t_idx, input_ids):
+ if input_ids.shape[1] == 0:
+ return t_idx
+ seq_len = input_ids.shape[1]
+ inputs_embeds = self.language_model.get_input_embeddings()(input_ids)
+
+ t_indexes = torch.arange(t_idx + 1, t_idx + 1 + seq_len, dtype=torch.long, device=device)
+ h_indexes = torch.zeros(seq_len, dtype=torch.long, device=device)
+ w_indexes = torch.zeros(seq_len, dtype=torch.long, device=device)
+ indexes = torch.stack([t_indexes, h_indexes, w_indexes], dim=0)
+
+ past_len = cache.get_seq_length()
+ mask = torch.zeros(1, 1, seq_len, past_len + seq_len, device=device)
+ causal_mask = torch.tril(torch.ones(seq_len, seq_len, device=device))
+ causal_mask = torch.where(causal_mask == 1, 0.0, float('-inf'))
+ mask[:, :, :, past_len:] = causal_mask
+ attention_mask_dict = {"full_attention": mask}
+
+ self.language_model(
+ inputs_embeds=inputs_embeds,
+ indexes=indexes,
+ attention_mask=attention_mask_dict,
+ past_key_values=cache,
+ use_cache=True
+ )
+ return t_idx + seq_len
+
+ def append_image_to_cache(cache, t_idx, inputs_embeds_img, N_img_tokens, abs_pos_w, abs_pos_h):
+ past_len = cache.get_seq_length()
+ tgt_len = N_img_tokens + 1
+
+ t_indexes = torch.zeros(tgt_len, dtype=torch.long, device=device)
+ t_indexes[:N_img_tokens] = t_idx + 1
+ t_indexes[N_img_tokens] = t_idx + 2
+
+ h_indexes = torch.zeros(tgt_len, dtype=torch.long, device=device)
+ w_indexes = torch.zeros(tgt_len, dtype=torch.long, device=device)
+ h_indexes[:N_img_tokens] = abs_pos_h
+ w_indexes[:N_img_tokens] = abs_pos_w
+
+ indexes = torch.stack([t_indexes, h_indexes, w_indexes], dim=0)
+
+ mask = torch.zeros(1, 1, tgt_len, past_len + tgt_len, device=device)
+ mask[0, 0, :N_img_tokens, past_len + N_img_tokens] = float('-inf')
+ attention_mask_dict = {"full_attention": mask}
+
+ self.language_model(
+ inputs_embeds=inputs_embeds_img,
+ indexes=indexes,
+ attention_mask=attention_mask_dict,
+ past_key_values=cache,
+ use_cache=True
+ )
+ return t_idx + 2
+
+ parts = gt_text.split('')
+ img_start_id_tensor = torch.tensor([[self.img_start_token_id]], device=device)
+
+ for i, part in enumerate(parts):
+ if len(part) > 0:
+ if verbose:
+ print(part, end='', flush=True)
+ part_ids = tokenizer(part, return_tensors='pt', add_special_tokens=False)['input_ids'].to(device)
+ t_index_cond = append_ids_to_cache(past_key_values_cond, t_index_cond, part_ids)
+
+ if i < len(parts) - 1:
+ if img_count >= max_images:
+ break
+
+ if verbose:
+ print("", end='', flush=True)
+
+ t_index_cond = append_ids_to_cache(past_key_values_cond, t_index_cond, img_start_id_tensor)
+ t_index_tu = append_ids_to_cache(past_key_values_tu, t_index_tu, img_start_id_tensor)
+
+ cur_image_size = image_size_list[img_count]
+ token_h = cur_image_size[1] // (self.patch_size * merge_size)
+ token_w = cur_image_size[0] // (self.patch_size * merge_size)
+
+ indexes_image_condition = self._build_t2i_image_indexes(token_h, token_w, t_index_cond + 1, device=device)
+ indexes_image_text_uncondition = self._build_t2i_image_indexes(token_h, token_w, t_index_tu + 1, device=device)
+ indexes_image_img_uncondition = self._build_t2i_image_indexes(token_h, token_w, indexes_iu[0].max() + 1, device=device)
+
+ grid_h = cur_image_size[1] // self.patch_size
+ grid_w = cur_image_size[0] // self.patch_size
+ gen_grid_hw = torch.tensor([[grid_h, grid_w]], device=device)
+
+ noise_scale = self.noise_scale
+ if self.noise_scale_mode in ("resolution", "dynamic", 'dynamic_sqrt'):
+ noise_scale = math.sqrt((grid_h*grid_w)/(merge_size**2) / self.noise_scale_base_image_seq_len)
+ base = float(self.noise_scale_base_image_seq_len)
+ noise_scale = math.sqrt((grid_h*grid_w)/(merge_size**2)/base) * float(self.noise_scale)
+ if self.noise_scale_mode == 'dynamic_sqrt':
+ noise_scale = math.sqrt(noise_scale)
+ noise_scale = min(noise_scale, self.noise_scale_max_value)
+ image_prediction = noise_scale * torch.randn((1, 3, cur_image_size[1], cur_image_size[0]), device=device, dtype=outputs_cond.logits.dtype)
+
+ past_key_values_cond_cfg = past_key_values_cond
+ past_key_values_tu_cfg = past_key_values_tu
+ past_key_values_iu_cfg = past_key_values_iu
+
+ # attention_mask_condition = {"full_attention": torch.zeros(1, 1, token_h*token_w, past_key_values_cond.get_seq_length() + token_h*token_w, device=device)}
+ # attention_mask_text_uncondition = {"full_attention": torch.zeros(1, 1, token_h*token_w, past_key_values_tu.get_seq_length() + token_h*token_w, device=device)}
+ # attention_mask_img_uncondition = {"full_attention": torch.zeros(1, 1, token_h*token_w, past_key_values_iu.get_seq_length() + token_h*token_w, device=device)}
+ attention_mask_condition = {"full_attention": None}
+ attention_mask_text_uncondition = {"full_attention": None}
+ attention_mask_img_uncondition = {"full_attention": None}
+
+ prepare_flash_kv_cache(
+ past_key_values_cond_cfg,
+ current_len=token_h * token_w,
+ batch_size=1,
+ )
+ prepare_flash_kv_cache(
+ past_key_values_tu_cfg,
+ current_len=token_h * token_w,
+ batch_size=1,
+ )
+ prepare_flash_kv_cache(
+ past_key_values_iu_cfg,
+ current_len=token_h * token_w,
+ batch_size=1,
+ )
+
+ timesteps = torch.linspace(0.0, 1.0, num_steps+1, device=device)
+ if enable_timestep_shift:
+ timesteps = self._apply_time_schedule(timesteps, token_h*token_w, timestep_shift)
+
+ step_iter = range(num_steps)
+ if verbose:
+ try:
+ from tqdm import tqdm as _tqdm
+ step_iter = _tqdm(
+ step_iter,
+ desc=f"image {img_count + 1} ({image_size[0]}x{image_size[1]})",
+ total=num_steps,
+ leave=False,
+ )
+ except ImportError:
+ pass
+ for step_i in step_iter:
+ t = timesteps[step_i]
+ t_next = timesteps[step_i + 1]
+
+ z = self.patchify(image_prediction, self.patch_size * merge_size)
+ image_input = self.patchify(image_prediction, self.patch_size, channel_first=True)
+ image_embeds = self.extract_feature(image_input.view(1 * grid_h*grid_w, -1), gen_model=True, grid_hw=gen_grid_hw).view(1, token_h*token_w, -1)
+ t_expanded = t.expand(token_h*token_w)
+ timestep_embeddings = self.fm_modules['timestep_embedder'](t_expanded).view(1, token_h*token_w, -1)
+ if self.add_noise_scale_embedding:
+ noise_scale_tensor = torch.full_like(t_expanded, noise_scale/self.noise_scale_max_value)
+ noise_embeddings = self.fm_modules['noise_scale_embedder'](noise_scale_tensor).view(1, token_h*token_w, -1)
+ timestep_embeddings += noise_embeddings
+ image_embeds = image_embeds + timestep_embeddings
+
+ use_cfg = (t > cfg_interval[0] and t < cfg_interval[1]) or cfg_interval[0] == 0
+ out_cond = self._t2i_predict_v(image_embeds, indexes_image_condition, attention_mask_condition, past_key_values_cond_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ if not use_cfg:
+ v_pred = out_cond
+ elif cfg_scale == 1 and img_cfg_scale == 1:
+ v_pred = out_cond
+ elif img_cfg_scale == 1:
+ out_img_cond = self._t2i_predict_v(image_embeds, indexes_image_text_uncondition, attention_mask_text_uncondition, past_key_values_tu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ v_pred = out_img_cond + cfg_scale * (out_cond - out_img_cond)
+ elif cfg_scale == img_cfg_scale:
+ out_uncond = self._t2i_predict_v(image_embeds, indexes_image_img_uncondition, attention_mask_img_uncondition, past_key_values_iu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ v_pred = out_uncond + cfg_scale * (out_cond - out_uncond)
+ else:
+ out_img_cond = self._t2i_predict_v(image_embeds, indexes_image_text_uncondition, attention_mask_text_uncondition, past_key_values_tu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ out_uncond = self._t2i_predict_v(image_embeds, indexes_image_img_uncondition, attention_mask_img_uncondition, past_key_values_iu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ v_pred = (
+ out_uncond
+ + cfg_scale * (out_cond - out_img_cond)
+ + img_cfg_scale * (out_img_cond - out_uncond)
+ )
+ if (cfg_scale > 1 or img_cfg_scale > 1) and use_cfg:
+ if cfg_norm == 'global':
+ norm_v_condition = torch.norm(out_cond, dim=(1,2), keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=(1,2), keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+ elif cfg_norm == 'channel':
+ norm_v_condition = torch.norm(out_cond, dim=-1, keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=-1, keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+
+ z = z + (t_next - t) * v_pred
+ image_prediction = self.unpatchify(z, self.patch_size * merge_size, cur_image_size[1], cur_image_size[0])
+
+ generated_images.append(image_prediction)
+
+ clear_flash_kv_cache(past_key_values_cond_cfg)
+ clear_flash_kv_cache(past_key_values_tu_cfg)
+ clear_flash_kv_cache(past_key_values_iu_cfg)
+
+ if gt_images is not None and img_count < len(gt_images):
+ gt_img_pil = gt_images[img_count]
+ gt_pixel_values, gt_grid_hw = load_image_native(gt_img_pil, self.patch_size, self.downsample_ratio, min_pixels=512*512, max_pixels=(2048*2048), upscale=False)
+ gt_pixel_values = gt_pixel_values.to(device).to(torch.bfloat16)
+
+ flatten_pixel_values = gt_pixel_values
+ gen_grid_hw_und = gt_grid_hw
+ else:
+ pred_img = image_prediction[0].unsqueeze(0).to(torch.bfloat16)
+ raw_img = pred_img * 0.5 + 0.5
+ img_mean = torch.tensor([0.485, 0.456, 0.406], dtype=raw_img.dtype, device=device).view(1, 3, 1, 1)
+ img_std = torch.tensor([0.229, 0.224, 0.225], dtype=raw_img.dtype, device=device).view(1, 3, 1, 1)
+ und_img = (raw_img - img_mean) / img_std
+
+ c, h, w = und_img[0].shape
+ ps = self.patch_size
+ p_grid_h = h // ps
+ p_grid_w = w // ps
+ flatten_pixel_values = (
+ und_img[0].view(c, p_grid_h, ps, p_grid_w, ps)
+ .permute(1, 3, 0, 2, 4)
+ .reshape(p_grid_h * p_grid_w, c * ps ** 2)
+ )
+ gen_grid_hw_und = torch.tensor([[p_grid_h, p_grid_w]], device=device)
+
+ vit_embeds = self.extract_feature(flatten_pixel_values, grid_hw=gen_grid_hw_und[:1]).unsqueeze(0)
+
+ img_end_id = tokenizer.convert_tokens_to_ids(IMG_END_TOKEN)
+ img_end_embed = self.language_model.get_input_embeddings()(torch.tensor([[img_end_id]], device=device))
+ inputs_embeds_img = torch.cat([vit_embeds, img_end_embed], dim=1) # (1, N + 1, C)
+
+ N_img_tokens = vit_embeds.shape[1]
+ abs_pos_w, abs_pos_h = build_abs_positions_from_grid_hw(gen_grid_hw_und[:1] // int(1 / self.downsample_ratio), device=device)
+
+ t_index_cond = append_image_to_cache(past_key_values_cond, t_index_cond, inputs_embeds_img, N_img_tokens, abs_pos_w, abs_pos_h)
+ t_index_tu = append_image_to_cache(past_key_values_tu, t_index_tu, inputs_embeds_img, N_img_tokens, abs_pos_w, abs_pos_h)
+
+ img_count += 1
+
+ return generated_images
+
+ @torch.no_grad()
+ def interleave_gen(
+ self,
+ tokenizer,
+ prompt,
+ images=None,
+ generation_config=None,
+ cfg_scale=1.0,
+ img_cfg_scale=1.0,
+ cfg_norm='none',
+ max_images=10,
+ enable_timestep_shift=True,
+ timestep_shift=1.0,
+ image_size=(256, 256),
+ num_steps=30,
+ IMG_START_TOKEN='
',
+ IMG_END_TOKEN='',
+ IMG_CONTEXT_TOKEN='',
+ method='euler',
+ cfg_interval=(0, 1),
+ t_eps=0.02,
+ verbose=False,
+ system_message='',
+ think_mode=False,
+ seed=0,
+ ):
+ self.img_context_token_id = tokenizer.convert_tokens_to_ids(IMG_CONTEXT_TOKEN)
+ self.img_start_token_id = tokenizer.convert_tokens_to_ids(IMG_START_TOKEN)
+ self.config.t_eps = t_eps
+
+ if isinstance(image_size, tuple):
+ image_size_list = [image_size] * max_images
+ elif isinstance(image_size, list) and isinstance(image_size[0], tuple):
+ image_size_list = image_size
+ if len(image_size) < max_images:
+ image_size_list += [image_size_list[-1]] * (max_images - len(image_size_list))
+ else:
+ assert False, "image size should be a tuple or a list of tuple"
+
+ if generation_config and hasattr(generation_config, 'max_new_tokens') and generation_config.max_new_tokens is not None:
+ max_new_tokens = generation_config.max_new_tokens
+ else:
+ max_new_tokens = 8192
+
+ current_generated_tokens = 0
+
+ if images is None:
+ images = []
+
+ template = get_conv_template(self.template)
+ template.system_message = self.system_message
+ eos_token_id = tokenizer.convert_tokens_to_ids(template.sep.strip())
+
+ image_token_count = prompt.count('')
+ assert len(images) >= image_token_count
+ if len(images) > image_token_count:
+ prompt = "\n" * (len(images) - image_token_count) + prompt
+
+ pixel_values =[]
+ grid_hw =[]
+ for image in images:
+ cur_pixel_values, cur_grid_hw = load_image_native(image, self.patch_size, self.downsample_ratio, min_pixels=512*512, max_pixels=min(2048*2048, (4096*4096)//max(1, len(images))), upscale=False)
+ grid_hw.append(cur_grid_hw.to(self.device))
+ pixel_values.append(cur_pixel_values.to(self.device).to(torch.bfloat16))
+
+ merge_size = int(1 / self.downsample_ratio)
+ pv_tensor = torch.cat(pixel_values) if pixel_values else None
+ ghw_tensor = torch.cat(grid_hw) if grid_hw else None
+
+ # Condition
+ template_cond = get_conv_template(self.template)
+ template_cond.system_message = system_message
+ template_cond.append_message(template_cond.roles[0], prompt)
+ template_cond.append_message(template_cond.roles[1], None)
+ query_cond = template_cond.get_prompt()
+
+ if not think_mode:
+ query_cond = query_cond + '\n\n\n\n'
+
+ def replace_image_tokens(query, grid_hw_list):
+ for i in range(len(grid_hw_list)):
+ num_patch_token = int(grid_hw_list[i][0, 0] * grid_hw_list[i][0, 1] * self.downsample_ratio**2)
+ image_tokens = IMG_START_TOKEN + IMG_CONTEXT_TOKEN * num_patch_token + IMG_END_TOKEN
+ query = query.replace('', image_tokens, 1)
+ return query
+
+ query_cond = replace_image_tokens(query_cond, grid_hw)
+ input_embeds_cond, indexes_cond, attention_mask_cond = self._build_it2i_inputs(tokenizer, query_cond, pv_tensor, ghw_tensor)
+
+ outputs_cond = self.language_model(inputs_embeds=input_embeds_cond, indexes=indexes_cond, attention_mask=attention_mask_cond, use_cache=True)
+ past_key_values_cond = outputs_cond.past_key_values
+ t_index_cond = indexes_cond[0].max().item()
+
+ # Initialize Text Uncondition Cache
+ question_text_uncondition = '' * len(images)
+ template_tu = get_conv_template(self.template)
+ template_tu.system_message = self.system_message
+ template_tu.append_message(template_tu.roles[0], question_text_uncondition)
+ template_tu.append_message(template_tu.roles[1], None)
+ query_text_uncond = template_tu.get_prompt()
+ query_text_uncond = replace_image_tokens(query_text_uncond, grid_hw)
+
+ input_embeds_tu, indexes_tu, attention_mask_tu = self._build_it2i_inputs(tokenizer, query_text_uncond, pv_tensor, ghw_tensor)
+ outputs_tu = self.language_model(inputs_embeds=input_embeds_tu, indexes=indexes_tu, attention_mask=attention_mask_tu, use_cache=True)
+ past_key_values_tu = outputs_tu.past_key_values
+ t_index_tu = indexes_tu[0].max().item()
+
+ # Initialize Img (ALL) Uncondition Cache
+ query_img_uncond = self._build_t2i_query("", append_text=IMG_START_TOKEN)
+ input_embeds_iu, indexes_iu, attention_mask_iu = self._build_it2i_inputs(tokenizer, query_img_uncond)
+ outputs_iu = self.language_model(inputs_embeds=input_embeds_iu, indexes=indexes_iu, attention_mask=attention_mask_iu, use_cache=True)
+ past_key_values_iu = outputs_iu.past_key_values
+
+
+ generated_text = ""
+ generated_images =[]
+ max_images = 10
+ img_count = 0
+
+ next_token = torch.argmax(outputs_cond.logits[:, -1, :], dim=-1)
+
+ generator = torch.Generator(self.device).manual_seed(seed)
+ while True:
+ # text generation
+ gen_tokens = []
+ hit_max_tokens = False
+ last_decoded = 0
+ while True:
+ token_item = next_token.item()
+ if token_item == eos_token_id or token_item == self.img_start_token_id:
+ break
+ gen_tokens.append(token_item)
+ current_generated_tokens += 1
+
+ self.language_model.model.current_index = t_index_cond
+ outputs_cond = self.language_model(
+ input_ids=next_token.unsqueeze(0),
+ past_key_values=past_key_values_cond,
+ use_cache=True
+ )
+ past_key_values_cond = outputs_cond.past_key_values
+ t_index_cond += 1
+ next_token = torch.argmax(outputs_cond.logits[:, -1, :], dim=-1)
+
+ # Stream partial text so users see liveness during long runs
+ # (e.g. low VRAM offload). Decode in 16-token chunks.
+ if verbose and len(gen_tokens) - last_decoded >= 16:
+ partial = tokenizer.decode(gen_tokens[last_decoded:], skip_special_tokens=True)
+ print(partial, end='', flush=True)
+ last_decoded = len(gen_tokens)
+
+ if current_generated_tokens >= max_new_tokens:
+ hit_max_tokens = True
+ break
+
+ if len(gen_tokens) > 0:
+ chunk_text = tokenizer.decode(gen_tokens, skip_special_tokens=True)
+ generated_text += chunk_text
+ if verbose:
+ remaining = tokenizer.decode(gen_tokens[last_decoded:], skip_special_tokens=True)
+ if remaining:
+ print(remaining, end='', flush=True)
+
+ if next_token.item() == eos_token_id or hit_max_tokens:
+ break
+
+ if next_token.item() == self.img_start_token_id:
+ if img_count >= max_images:
+ break
+
+ generated_text += ""
+ if verbose:
+ print(f"\n[image {img_count + 1}] preparing diffusion...", flush=True)
+
+ # Add the img_start_token for condition and text_uncondition branch
+ self.language_model.model.current_index = t_index_cond
+ outputs_cond = self.language_model(input_ids=next_token.unsqueeze(0), past_key_values=past_key_values_cond, use_cache=True)
+ past_key_values_cond = outputs_cond.past_key_values
+ t_index_cond += 1
+
+ self.language_model.model.current_index = t_index_tu
+ outputs_tu = self.language_model(input_ids=next_token.unsqueeze(0), past_key_values=past_key_values_tu, use_cache=True)
+ past_key_values_tu = outputs_tu.past_key_values
+ t_index_tu += 1
+
+ image_size = image_size_list[img_count]
+ # Image Generation
+ token_h = image_size[1] // (self.patch_size * merge_size)
+ token_w = image_size[0] // (self.patch_size * merge_size)
+ device = self.device
+
+ indexes_image_condition = self._build_t2i_image_indexes(token_h, token_w, t_index_cond + 1, device=device)
+ indexes_image_text_uncondition = self._build_t2i_image_indexes(token_h, token_w, t_index_tu + 1, device=device)
+ indexes_image_img_uncondition = self._build_t2i_image_indexes(token_h, token_w, indexes_iu[0].max() + 1, device=device)
+
+ grid_h = image_size[1] // self.patch_size
+ grid_w = image_size[0] // self.patch_size
+ gen_grid_hw = torch.tensor([[grid_h, grid_w]], device=device)
+
+ noise_scale = self.noise_scale
+ if self.noise_scale_mode in ("resolution", "dynamic", 'dynamic_sqrt'):
+ base = float(self.noise_scale_base_image_seq_len)
+ noise_scale = math.sqrt((grid_h*grid_w)/(merge_size**2)/base) * float(self.noise_scale)
+ if self.noise_scale_mode == 'dynamic_sqrt':
+ noise_scale = math.sqrt(noise_scale)
+ noise_scale = min(noise_scale, self.noise_scale_max_value)
+ image_prediction = noise_scale * torch.randn((1, 3, image_size[1], image_size[0]), device=device, dtype=outputs_cond.logits.dtype, generator=generator)
+
+ past_key_values_cond_cfg = past_key_values_cond
+ past_key_values_tu_cfg = past_key_values_tu
+ past_key_values_iu_cfg = past_key_values_iu
+
+ attention_mask_condition = {"full_attention": None}
+ attention_mask_text_uncondition = {"full_attention": None}
+ attention_mask_img_uncondition = {"full_attention": None}
+
+ prepare_flash_kv_cache(
+ past_key_values_cond_cfg,
+ current_len=token_h * token_w,
+ batch_size=1,
+ )
+ prepare_flash_kv_cache(
+ past_key_values_tu_cfg,
+ current_len=token_h * token_w,
+ batch_size=1,
+ )
+ prepare_flash_kv_cache(
+ past_key_values_iu_cfg,
+ current_len=token_h * token_w,
+ batch_size=1,
+ )
+
+ timesteps = torch.linspace(0.0, 1.0, num_steps+1, device=device)
+ if enable_timestep_shift:
+ timesteps = self._apply_time_schedule(timesteps, token_h*token_w, timestep_shift)
+
+ step_iter = range(num_steps)
+ if verbose:
+ try:
+ from tqdm import tqdm as _tqdm
+ step_iter = _tqdm(
+ step_iter,
+ desc=f"image {img_count + 1} ({image_size[0]}x{image_size[1]})",
+ total=num_steps,
+ leave=False,
+ )
+ except ImportError:
+ pass
+ for step_i in step_iter:
+ t = timesteps[step_i]
+ t_next = timesteps[step_i + 1]
+
+ z = self.patchify(image_prediction, self.patch_size * merge_size)
+ image_input = self.patchify(image_prediction, self.patch_size, channel_first=True)
+ image_embeds = self.extract_feature(image_input.view(1 * grid_h*grid_w, -1), gen_model=True, grid_hw=gen_grid_hw).view(1, token_h*token_w, -1)
+ t_expanded = t.expand(token_h*token_w)
+ timestep_embeddings = self.fm_modules['timestep_embedder'](t_expanded).view(1, token_h*token_w, -1)
+ if self.add_noise_scale_embedding:
+ noise_scale_tensor = torch.full_like(t_expanded, noise_scale/self.noise_scale_max_value)
+ noise_embeddings = self.fm_modules['noise_scale_embedder'](noise_scale_tensor).view(1, token_h*token_w, -1)
+ timestep_embeddings += noise_embeddings
+ image_embeds = image_embeds + timestep_embeddings
+
+ use_cfg = (t > cfg_interval[0] and t < cfg_interval[1]) or cfg_interval[0] == 0
+ out_cond = self._t2i_predict_v(image_embeds, indexes_image_condition, attention_mask_condition, past_key_values_cond_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ if not use_cfg:
+ v_pred = out_cond
+ elif cfg_scale == 1 and img_cfg_scale == 1:
+ v_pred = out_cond
+ elif img_cfg_scale == 1:
+ out_img_cond = self._t2i_predict_v(image_embeds, indexes_image_text_uncondition, attention_mask_text_uncondition, past_key_values_tu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ v_pred = out_img_cond + cfg_scale * (out_cond - out_img_cond)
+ elif cfg_scale == img_cfg_scale:
+ out_uncond = self._t2i_predict_v(image_embeds, indexes_image_img_uncondition, attention_mask_img_uncondition, past_key_values_iu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ v_pred = out_uncond + cfg_scale * (out_cond - out_uncond)
+ else:
+ out_img_cond = self._t2i_predict_v(image_embeds, indexes_image_text_uncondition, attention_mask_text_uncondition, past_key_values_tu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ out_uncond = self._t2i_predict_v(image_embeds, indexes_image_img_uncondition, attention_mask_img_uncondition, past_key_values_iu_cfg, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings)
+ v_pred = (
+ out_uncond
+ + cfg_scale * (out_cond - out_img_cond)
+ + img_cfg_scale * (out_img_cond - out_uncond)
+ )
+ if (cfg_scale > 1 or img_cfg_scale > 1 and use_cfg):
+ if cfg_norm == 'global':
+ norm_v_condition = torch.norm(out_cond, dim=(1,2), keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=(1,2), keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+ elif cfg_norm == 'channel':
+ norm_v_condition = torch.norm(out_cond, dim=-1, keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=-1, keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+
+ z = z + (t_next - t) * v_pred
+ image_prediction = self.unpatchify(z, self.patch_size * merge_size, image_size[1], image_size[0])
+
+ generated_images.append(image_prediction)
+
+ clear_flash_kv_cache(past_key_values_cond_cfg)
+ clear_flash_kv_cache(past_key_values_tu_cfg)
+ clear_flash_kv_cache(past_key_values_iu_cfg)
+
+ img_count += 1
+
+ # re-encode the generated image using the und-branch
+ pred_img = image_prediction[0].unsqueeze(0).to(torch.bfloat16)
+ # re-normalize the image
+ raw_img = pred_img * 0.5 + 0.5
+ img_mean = torch.tensor([0.485, 0.456, 0.406], dtype=raw_img.dtype, device=device).view(1, 3, 1, 1)
+ img_std = torch.tensor([0.229, 0.224, 0.225], dtype=raw_img.dtype, device=device).view(1, 3, 1, 1)
+ und_img = (raw_img - img_mean) / img_std
+ c, h, w = und_img[0].shape
+ ps = self.patch_size
+ p_grid_h = h // ps
+ p_grid_w = w // ps
+ flatten_pixel_values = (
+ und_img[0].view(c, p_grid_h, ps, p_grid_w, ps)
+ .permute(1, 3, 0, 2, 4) # [grid_h, grid_w, c, patch_size, patch_size]
+ .reshape(p_grid_h * p_grid_w, c * ps ** 2)
+ )
+ vit_embeds = self.extract_feature(flatten_pixel_values, grid_hw=gen_grid_hw[:1]).unsqueeze(0)
+
+ img_end_id = tokenizer.convert_tokens_to_ids(IMG_END_TOKEN)
+ img_end_embed = self.language_model.get_input_embeddings()(torch.tensor([[img_end_id]], device=device))
+ inputs_embeds_img = torch.cat([vit_embeds, img_end_embed], dim=1) # (1, N + 1, C)
+
+ N_img_tokens = vit_embeds.shape[1]
+ abs_pos_w, abs_pos_h = build_abs_positions_from_grid_hw(gen_grid_hw[:1] // int(1 / self.downsample_ratio), device=device)
+
+ def append_image_to_cache(cache, t_idx):
+ past_len = cache.get_seq_length()
+ tgt_len = N_img_tokens + 1
+
+ t_indexes = torch.zeros(tgt_len, dtype=torch.long, device=device)
+ t_indexes[:N_img_tokens] = t_idx + 1
+ t_indexes[N_img_tokens] = t_idx + 2
+
+ h_indexes = torch.zeros(tgt_len, dtype=torch.long, device=device)
+ w_indexes = torch.zeros(tgt_len, dtype=torch.long, device=device)
+ h_indexes[:N_img_tokens] = abs_pos_h
+ w_indexes[:N_img_tokens] = abs_pos_w
+
+ indexes = torch.stack([t_indexes, h_indexes, w_indexes], dim=0)
+
+ mask = torch.zeros(1, 1, tgt_len, past_len + tgt_len, device=device)
+ mask[0, 0, :N_img_tokens, past_len + N_img_tokens] = float('-inf')
+ attention_mask_dict = {"full_attention": mask}
+
+ outputs = self.language_model(
+ inputs_embeds=inputs_embeds_img,
+ indexes=indexes,
+ attention_mask=attention_mask_dict,
+ past_key_values=cache,
+ use_cache=True
+ )
+ return outputs, t_idx + 2
+
+ outputs_cond, t_index_cond = append_image_to_cache(past_key_values_cond, t_index_cond)
+ outputs_tu, t_index_tu = append_image_to_cache(past_key_values_tu, t_index_tu)
+
+ next_token = torch.argmax(outputs_cond.logits[:, -1, :], dim=-1)
+
+ return generated_text, generated_images
+
+ @torch.no_grad()
+ def it2i_generate(self, tokenizer, prompt, images, cfg_scale=1, img_cfg_scale=1, cfg_norm='none', enable_timestep_shift=True, timestep_shift=1, image_size=(256, 256), num_steps=30, IMG_START_TOKEN='
', IMG_END_TOKEN='', IMG_CONTEXT_TOKEN='', method='euler', cfg_interval=(0, 1), batch_size=1, t_eps=0.02, think_mode=False, seed=0):
+ assert cfg_norm in ['none', 'global', 'channel']
+ self._notify_layer_offload_phase("prefix")
+
+ self.img_context_token_id = tokenizer.convert_tokens_to_ids(IMG_CONTEXT_TOKEN)
+ self.config.t_eps = t_eps
+
+ image_token_count = prompt.count('')
+ assert len(images) >= image_token_count
+ if len(images) > image_token_count:
+ if image_token_count == 0 and len(images) > 1:
+ prompt = "".join(f"Image-{i + 1}:\n" for i in range(len(images))) + prompt
+ else:
+ prompt = "\n" * (len(images) - image_token_count) + prompt
+
+ pixel_values = []
+ grid_hw = []
+ for image in images:
+ cur_pixel_values, cur_grid_hw = load_image_native(
+ image,
+ self.patch_size,
+ self.downsample_ratio,
+ min_pixels=512 * 512,
+ max_pixels=min(2048*2048, (4096 * 4096) // len(images)),
+ upscale=False,
+ )
+ cur_grid_hw = cur_grid_hw.to(self.device)
+ cur_pixel_values = cur_pixel_values.to(self.device).to(torch.bfloat16)
+ pixel_values.append(cur_pixel_values)
+ grid_hw.append(cur_grid_hw)
+ pixel_values = torch.cat(pixel_values)
+ grid_hw = torch.cat(grid_hw)
+
+ merge_size = int(1 / self.downsample_ratio)
+ question_condition = f"{prompt}"
+ think_text = ""
+ needs_cfg = not (cfg_scale == 1 and img_cfg_scale == 1)
+ needs_img_condition = needs_cfg and (img_cfg_scale == 1 or cfg_scale != img_cfg_scale)
+ needs_uncondition = needs_cfg and img_cfg_scale != 1
+
+ think_content = '\n' if think_mode else '\n\n\n\n' + IMG_START_TOKEN
+ query_condition = self._build_t2i_query(question_condition, system_message=SYSTEM_MESSAGE_FOR_GEN, append_text=think_content)
+ query_img_condition = (
+ self._build_t2i_query('' * len(images), append_text=IMG_START_TOKEN)
+ if needs_img_condition
+ else None
+ )
+ query_uncondition = self._build_t2i_query("", append_text=IMG_START_TOKEN) if needs_uncondition else None
+
+ for i in range(grid_hw.shape[0]):
+ num_patch_token = int(grid_hw[i, 0] * grid_hw[i, 1] * self.downsample_ratio**2)
+ image_tokens = IMG_START_TOKEN + IMG_CONTEXT_TOKEN * num_patch_token + IMG_END_TOKEN
+ query_condition = query_condition.replace('', image_tokens, 1)
+ if query_img_condition is not None:
+ query_img_condition = query_img_condition.replace('', image_tokens, 1)
+
+ input_embeds_condition, indexes_condition, attention_mask_condition_prefix = self._build_it2i_inputs(
+ tokenizer, query_condition, pixel_values, grid_hw
+ )
+ if query_img_condition is not None:
+ input_embeds_img_condition, indexes_img_condition, attention_mask_img_condition_prefix = self._build_it2i_inputs(
+ tokenizer, query_img_condition, pixel_values, grid_hw
+ )
+ else:
+ input_embeds_img_condition = indexes_img_condition = attention_mask_img_condition_prefix = None
+ if query_uncondition is not None:
+ input_embeds_uncondition, indexes_uncondition, attention_mask_uncondition_prefix = self._build_it2i_inputs(
+ tokenizer, query_uncondition
+ )
+ else:
+ input_embeds_uncondition = indexes_uncondition = attention_mask_uncondition_prefix = None
+
+ token_h = image_size[1] // (self.patch_size * merge_size)
+ token_w = image_size[0] // (self.patch_size * merge_size)
+
+ indexes_image_condition = self._build_t2i_image_indexes(
+ token_h, token_w, indexes_condition[0].max() + 1, device=input_embeds_condition.device
+ )
+ indexes_image_img_condition = (
+ self._build_t2i_image_indexes(
+ token_h, token_w, indexes_img_condition[0].max() + 1, device=input_embeds_img_condition.device
+ )
+ if indexes_img_condition is not None
+ else None
+ )
+ indexes_image_uncondition = (
+ self._build_t2i_image_indexes(
+ token_h, token_w, indexes_uncondition[0].max() + 1, device=input_embeds_uncondition.device
+ )
+ if indexes_uncondition is not None
+ else None
+ )
+
+ if think_mode:
+ outputs_condition = self._think_prefix_forward(
+ inputs_embeds=input_embeds_condition,
+ indexes=indexes_condition,
+ attention_mask=attention_mask_condition_prefix,
+ )
+ past_key_values_condition = outputs_condition.past_key_values
+ device = outputs_condition.logits.device
+ dtype = outputs_condition.logits.dtype
+ t_index_condition = indexes_condition[0].max().item()
+ past_key_values_condition, t_index_condition, think_text = self._generate_think(
+ tokenizer,
+ outputs_condition,
+ past_key_values_condition,
+ t_index_condition,
+ IMG_START_TOKEN,
+ )
+ indexes_image_condition = self._build_t2i_image_indexes(
+ token_h, token_w, t_index_condition + 1, device=input_embeds_condition.device
+ )
+ del outputs_condition
+ else:
+ past_key_values_condition, prefix_hidden_states = self._it2i_prefix_forward(
+ input_embeds_condition, indexes_condition, attention_mask_condition_prefix
+ )
+ device = prefix_hidden_states.device
+ dtype = prefix_hidden_states.dtype
+ del prefix_hidden_states
+ past_key_values_img_condition = None
+ if input_embeds_img_condition is not None:
+ past_key_values_img_condition, _ = self._it2i_prefix_forward(
+ input_embeds_img_condition, indexes_img_condition, attention_mask_img_condition_prefix
+ )
+ past_key_values_uncondition = None
+ if input_embeds_uncondition is not None:
+ past_key_values_uncondition, _ = self._it2i_prefix_forward(
+ input_embeds_uncondition, indexes_uncondition, attention_mask_uncondition_prefix
+ )
+
+ del pixel_values, grid_hw
+ del input_embeds_condition, indexes_condition, attention_mask_condition_prefix
+ if input_embeds_img_condition is not None:
+ del input_embeds_img_condition, indexes_img_condition, attention_mask_img_condition_prefix
+ if input_embeds_uncondition is not None:
+ del input_embeds_uncondition, indexes_uncondition, attention_mask_uncondition_prefix
+ self._notify_layer_offload_phase("denoise")
+
+ for layer_idx in range(len(past_key_values_condition.layers)):
+ past_key_values_condition.layers[layer_idx].keys = past_key_values_condition.layers[layer_idx].keys.expand(
+ batch_size, *past_key_values_condition.layers[layer_idx].keys.shape[1:]
+ )
+ past_key_values_condition.layers[layer_idx].values = past_key_values_condition.layers[layer_idx].values.expand(
+ batch_size, *past_key_values_condition.layers[layer_idx].values.shape[1:]
+ )
+ if past_key_values_img_condition is not None:
+ past_key_values_img_condition.layers[layer_idx].keys = past_key_values_img_condition.layers[layer_idx].keys.expand(
+ batch_size, *past_key_values_img_condition.layers[layer_idx].keys.shape[1:]
+ )
+ past_key_values_img_condition.layers[layer_idx].values = past_key_values_img_condition.layers[layer_idx].values.expand(
+ batch_size, *past_key_values_img_condition.layers[layer_idx].values.shape[1:]
+ )
+ if past_key_values_uncondition is not None:
+ past_key_values_uncondition.layers[layer_idx].keys = past_key_values_uncondition.layers[layer_idx].keys.expand(
+ batch_size, *past_key_values_uncondition.layers[layer_idx].keys.shape[1:]
+ )
+ past_key_values_uncondition.layers[layer_idx].values = past_key_values_uncondition.layers[layer_idx].values.expand(
+ batch_size, *past_key_values_uncondition.layers[layer_idx].values.shape[1:]
+ )
+
+ prepare_flash_kv_cache(
+ past_key_values_condition,
+ current_len=token_h * token_w,
+ batch_size=batch_size,
+ )
+ if past_key_values_img_condition is not None:
+ prepare_flash_kv_cache(
+ past_key_values_img_condition,
+ current_len=token_h * token_w,
+ batch_size=batch_size,
+ )
+ if past_key_values_uncondition is not None:
+ prepare_flash_kv_cache(
+ past_key_values_uncondition,
+ current_len=token_h * token_w,
+ batch_size=batch_size,
+ )
+
+ grid_h = image_size[1] // self.patch_size
+ grid_w = image_size[0] // self.patch_size
+ grid_hw = torch.tensor([[grid_h, grid_w]] * batch_size, device=device)
+
+ noise_scale = self.noise_scale
+ if self.noise_scale_mode in ("resolution", "dynamic", "dynamic_sqrt"):
+ base = float(self.noise_scale_base_image_seq_len)
+ scale = math.sqrt((grid_h * grid_w) / (merge_size**2) / base)
+ noise_scale = scale * float(self.noise_scale)
+ if self.noise_scale_mode == 'dynamic_sqrt':
+ noise_scale = math.sqrt(noise_scale)
+ noise_scale = min(noise_scale, self.noise_scale_max_value)
+ generator = torch.Generator(device).manual_seed(seed)
+ image_prediction = noise_scale * torch.randn(
+ (batch_size, 3, image_size[1], image_size[0]), device=device, dtype=dtype, generator=generator
+ )
+
+ attention_mask_condition = {"full_attention": None}
+ attention_mask_img_condition = {"full_attention": None}
+ attention_mask_uncondition = {"full_attention": None}
+
+ timesteps = torch.linspace(0.0, 1.0, num_steps + 1, device=device)
+ if enable_timestep_shift:
+ timesteps = self._apply_time_schedule(timesteps, token_h * token_w, timestep_shift)
+
+ for step_i in range(num_steps):
+ t = timesteps[step_i]
+ t_next = timesteps[step_i + 1]
+ use_cfg = (t > cfg_interval[0] and t < cfg_interval[1]) or cfg_interval[0] == 0
+
+ z = self.patchify(image_prediction, self.patch_size * merge_size)
+ image_input = self.patchify(image_prediction, self.patch_size, channel_first=True)
+ image_embeds = self.extract_feature(
+ image_input.view(batch_size * grid_h * grid_w, -1),
+ gen_model=True,
+ grid_hw=grid_hw,
+ ).view(batch_size, token_h * token_w, -1)
+ t_expanded = t.expand(batch_size * token_h * token_w)
+ timestep_embeddings = self.fm_modules['timestep_embedder'](t_expanded).view(batch_size, token_h * token_w, -1)
+ if self.add_noise_scale_embedding:
+ noise_scale_tensor = torch.full_like(t_expanded, noise_scale / self.noise_scale_max_value)
+ noise_embeddings = self.fm_modules['noise_scale_embedder'](noise_scale_tensor).view(batch_size, token_h * token_w, -1)
+ timestep_embeddings += noise_embeddings
+ image_embeds = image_embeds + timestep_embeddings
+
+ out_cond = self._t2i_predict_v(
+ image_embeds,
+ indexes_image_condition,
+ attention_mask_condition,
+ past_key_values_condition,
+ t,
+ z,
+ image_token_num=token_h * token_w,
+ timestep_embeddings=timestep_embeddings,
+ image_size=image_size,
+ )
+
+ if not use_cfg:
+ v_pred = out_cond
+ elif cfg_scale == 1 and img_cfg_scale == 1:
+ v_pred = out_cond
+ elif img_cfg_scale == 1:
+ out_img_cond = self._t2i_predict_v(
+ image_embeds,
+ indexes_image_img_condition,
+ attention_mask_img_condition,
+ past_key_values_img_condition,
+ t,
+ z,
+ image_token_num=token_h * token_w,
+ timestep_embeddings=timestep_embeddings,
+ image_size=image_size,
+ )
+ v_pred = out_img_cond + cfg_scale * (out_cond - out_img_cond)
+ elif cfg_scale == img_cfg_scale:
+ out_uncond = self._t2i_predict_v(
+ image_embeds,
+ indexes_image_uncondition,
+ attention_mask_uncondition,
+ past_key_values_uncondition,
+ t,
+ z,
+ image_token_num=token_h * token_w,
+ timestep_embeddings=timestep_embeddings,
+ image_size=image_size,
+ )
+ v_pred = out_uncond + cfg_scale * (out_cond - out_uncond)
+ else:
+ out_img_cond = self._t2i_predict_v(
+ image_embeds,
+ indexes_image_img_condition,
+ attention_mask_img_condition,
+ past_key_values_img_condition,
+ t,
+ z,
+ image_token_num=token_h * token_w,
+ timestep_embeddings=timestep_embeddings,
+ image_size=image_size,
+ )
+ out_uncond = self._t2i_predict_v(
+ image_embeds,
+ indexes_image_uncondition,
+ attention_mask_uncondition,
+ past_key_values_uncondition,
+ t,
+ z,
+ image_token_num=token_h * token_w,
+ timestep_embeddings=timestep_embeddings,
+ image_size=image_size,
+ )
+ v_pred = (
+ out_uncond
+ + cfg_scale * (out_cond - out_img_cond)
+ + img_cfg_scale * (out_img_cond - out_uncond)
+ )
+ if (cfg_scale > 1 or img_cfg_scale > 1) and use_cfg:
+ if cfg_norm == 'global':
+ norm_v_condition = torch.norm(out_cond, dim=(1, 2), keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=(1, 2), keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+ elif cfg_norm == 'channel':
+ norm_v_condition = torch.norm(out_cond, dim=-1, keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=-1, keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+
+ z = z + (t_next - t) * v_pred
+ image_prediction = self.unpatchify(z, self.patch_size * merge_size, image_size[1], image_size[0])
+
+ clear_flash_kv_cache(past_key_values_condition)
+ if past_key_values_img_condition is not None:
+ clear_flash_kv_cache(past_key_values_img_condition)
+ if past_key_values_uncondition is not None:
+ clear_flash_kv_cache(past_key_values_uncondition)
+
+ self.last_think_content = think_text
+ if think_mode:
+ return image_prediction, think_text
+ return image_prediction
+
+ @torch.no_grad()
+ def t2i_generate(self, tokenizer, prompt, cfg_scale=1, timestep_shift=1, enable_timestep_shift=True, cfg_norm='none', image_size=(256, 256), num_steps=30, IMG_START_TOKEN='
', IMG_END_TOKEN='', IMG_CONTEXT_TOKEN='', method='euler', cfg_interval=(0, 1), batch_size=1, t_eps=0.02, think_mode=False, seed=0):
+ assert self.concat_time_token_num == 0
+ assert cfg_norm in ['cfg_zero_star', 'global', 'none', 'channel']
+ self._notify_layer_offload_phase("prefix")
+ merge_size = int(1 / self.downsample_ratio)
+
+ self.config.t_eps = t_eps
+ # question_condition = f"Please generate an image based on the following description: {prompt}"
+ question_condition = f"{prompt}"
+ # question_condition += f"\nThe resolution of the image should be {image_size}"
+
+ think_text = ""
+ needs_cfg = cfg_scale > 1
+
+ think_content = '\n' if think_mode else '\n\n\n\n' + IMG_START_TOKEN
+ query_condition = self._build_t2i_query(question_condition, system_message=SYSTEM_MESSAGE_FOR_GEN, append_text=think_content)
+ query_uncondition = self._build_t2i_query("", append_text=IMG_START_TOKEN) if needs_cfg else None
+
+ input_ids_condition, indexes_condition, attention_mask_condition_prefix = self._build_t2i_text_inputs(tokenizer, query_condition)
+ if query_uncondition is not None:
+ input_ids_uncondition, indexes_uncondition, attention_mask_uncondition_prefix = self._build_t2i_text_inputs(tokenizer, query_uncondition)
+ else:
+ input_ids_uncondition = indexes_uncondition = attention_mask_uncondition_prefix = None
+
+ token_h = image_size[1] // (self.patch_size * merge_size)
+ token_w = image_size[0] // (self.patch_size * merge_size)
+
+ indexes_image_condition = self._build_t2i_image_indexes(token_h, token_w, indexes_condition.shape[1], device=input_ids_condition.device)
+ indexes_image_uncondition = (
+ self._build_t2i_image_indexes(token_h, token_w, indexes_uncondition.shape[1], device=input_ids_uncondition.device)
+ if indexes_uncondition is not None
+ else None
+ )
+
+ if think_mode:
+ outputs_condition = self._think_prefix_forward(
+ input_ids=input_ids_condition,
+ indexes=indexes_condition,
+ attention_mask=attention_mask_condition_prefix,
+ )
+ past_key_values_condition = outputs_condition.past_key_values
+ device = outputs_condition.logits.device
+ dtype = outputs_condition.logits.dtype
+ t_index_condition = indexes_condition[0].max().item()
+ past_key_values_condition, t_index_condition, think_text = self._generate_think(
+ tokenizer,
+ outputs_condition,
+ past_key_values_condition,
+ t_index_condition,
+ IMG_START_TOKEN,
+ )
+ indexes_image_condition = self._build_t2i_image_indexes(
+ token_h, token_w, t_index_condition + 1, device=input_ids_condition.device
+ )
+ del outputs_condition
+ else:
+ past_key_values_condition, prefix_hidden_states = self._t2i_prefix_forward(input_ids_condition, indexes_condition, attention_mask_condition_prefix)
+ device = prefix_hidden_states.device
+ dtype = prefix_hidden_states.dtype
+ del prefix_hidden_states
+ past_key_values_uncondition = None
+ if input_ids_uncondition is not None:
+ past_key_values_uncondition, _ = self._t2i_prefix_forward(input_ids_uncondition, indexes_uncondition, attention_mask_uncondition_prefix)
+
+ del input_ids_condition, indexes_condition, attention_mask_condition_prefix
+ if input_ids_uncondition is not None:
+ del input_ids_uncondition, indexes_uncondition, attention_mask_uncondition_prefix
+ self._notify_layer_offload_phase("denoise")
+
+ for layer_idx in range(len(past_key_values_condition.layers)):
+ past_key_values_condition.layers[layer_idx].keys = past_key_values_condition.layers[layer_idx].keys.expand(batch_size, *past_key_values_condition.layers[layer_idx].keys.shape[1:])
+ past_key_values_condition.layers[layer_idx].values = past_key_values_condition.layers[layer_idx].values.expand(batch_size, *past_key_values_condition.layers[layer_idx].values.shape[1:])
+ if past_key_values_uncondition is not None:
+ past_key_values_uncondition.layers[layer_idx].keys = past_key_values_uncondition.layers[layer_idx].keys.expand(batch_size, *past_key_values_uncondition.layers[layer_idx].keys.shape[1:])
+ past_key_values_uncondition.layers[layer_idx].values = past_key_values_uncondition.layers[layer_idx].values.expand(batch_size, *past_key_values_uncondition.layers[layer_idx].values.shape[1:])
+
+ # prepare flash cache once
+ prepare_flash_kv_cache(
+ past_key_values_condition,
+ current_len=token_h * token_w,
+ batch_size=batch_size,
+ )
+ if past_key_values_uncondition is not None:
+ prepare_flash_kv_cache(
+ past_key_values_uncondition,
+ current_len=token_h * token_w,
+ batch_size=batch_size,
+ )
+
+ # init noise image tokens
+ grid_h = image_size[1] // self.patch_size
+ grid_w = image_size[0] // self.patch_size
+ grid_hw = torch.tensor([[grid_h, grid_w]]*batch_size, device=device)
+
+ noise_scale = self.noise_scale
+ if self.noise_scale_mode in ("resolution", "dynamic", 'dynamic_sqrt'):
+ base = float(self.noise_scale_base_image_seq_len)
+ scale = math.sqrt((grid_h*grid_w)/(merge_size**2)/base)
+ noise_scale = scale * float(self.noise_scale)
+ if self.noise_scale_mode == 'dynamic_sqrt':
+ noise_scale = math.sqrt(noise_scale)
+ noise_scale = min(noise_scale, self.noise_scale_max_value)
+ generator = torch.Generator(device).manual_seed(seed)
+ image_prediction = noise_scale * torch.randn((batch_size, 3, image_size[1], image_size[0]), device=device, dtype=dtype, generator=generator)
+
+ attention_mask_condition = {"full_attention": None}
+ attention_mask_uncondition = {"full_attention": None}
+
+ timesteps = torch.linspace(0.0, 1.0, num_steps+1, device=device)
+ if enable_timestep_shift:
+ timesteps = self._apply_time_schedule(timesteps, token_h*token_w, timestep_shift)
+
+ for step_i in range(num_steps):
+ t = timesteps[step_i]
+ t_next = timesteps[step_i + 1]
+
+ z = self.patchify(image_prediction, self.patch_size * merge_size)
+ image_input = self.patchify(image_prediction, self.patch_size, channel_first=True)
+ image_embeds = self.extract_feature(image_input.view(batch_size * grid_h*grid_w, -1), gen_model=True, grid_hw=grid_hw).view(batch_size, token_h*token_w, -1)
+ t_expanded = t.expand(batch_size*token_h*token_w)
+ timestep_embeddings = self.fm_modules['timestep_embedder'](t_expanded).view(batch_size, token_h*token_w, -1)
+ if self.add_noise_scale_embedding:
+ noise_scale_tensor = torch.full_like(t_expanded, noise_scale / self.noise_scale_max_value)
+ noise_embeddings = self.fm_modules['noise_scale_embedder'](noise_scale_tensor).view(batch_size, token_h*token_w, -1)
+ timestep_embeddings += noise_embeddings
+ image_embeds = image_embeds + timestep_embeddings
+
+ v_pred_condition = self._t2i_predict_v(image_embeds, indexes_image_condition, attention_mask_condition, past_key_values_condition, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings, image_size=image_size)
+
+ if t >= cfg_interval[0] and t <= cfg_interval[1] and cfg_scale > 1:
+ v_pred_uncondition = self._t2i_predict_v(image_embeds, indexes_image_uncondition, attention_mask_uncondition, past_key_values_uncondition, t, z, image_token_num=token_h*token_w, timestep_embeddings=timestep_embeddings, image_size=image_size)
+ if cfg_norm == 'cfg_zero_star':
+ positive_flat = v_pred_condition.view(batch_size, -1)
+ negative_flat = v_pred_uncondition.view(batch_size, -1)
+
+ alpha = optimized_scale(positive_flat,negative_flat)
+ alpha = alpha.view(batch_size, *([1] * (len(v_pred_condition.shape) - 1)))
+ alpha = alpha.to(positive_flat.dtype)
+
+ if (step_i <= 0):
+ v_pred = v_pred_condition*0.
+ else:
+ v_pred = v_pred_uncondition * alpha + cfg_scale * (v_pred_condition - v_pred_uncondition * alpha)
+ else:
+ v_pred = v_pred_uncondition + cfg_scale * (v_pred_condition - v_pred_uncondition)
+ if cfg_norm == 'global':
+ norm_v_condition = torch.norm(v_pred_condition, dim=(1,2), keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=(1,2), keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+ elif cfg_norm == 'channel':
+ norm_v_condition = torch.norm(v_pred_condition, dim=-1, keepdim=True)
+ norm_v_cfg = torch.norm(v_pred, dim=-1, keepdim=True)
+ scale = (norm_v_condition / (norm_v_cfg + 1e-8)).clamp(min=0, max=1.0)
+ v_pred = v_pred * scale
+ else:
+ v_pred = v_pred_condition
+
+ z = z + (t_next - t) * v_pred
+
+ image_prediction = self.unpatchify(z, self.patch_size * merge_size, image_size[1], image_size[0])
+
+ clear_flash_kv_cache(past_key_values_condition)
+ if past_key_values_uncondition is not None:
+ clear_flash_kv_cache(past_key_values_uncondition)
+
+ self.last_think_content = think_text
+ if think_mode:
+ return image_prediction, think_text
+ return image_prediction
+
+ def chat(self, tokenizer, pixel_values, question, generation_config, history=None, return_history=False, grid_hw=None,
+ IMG_START_TOKEN='
', IMG_END_TOKEN='', IMG_CONTEXT_TOKEN='', verbose=False):
+
+ if history is None and pixel_values is not None and '' not in question:
+ question = '\n' + question
+
+ img_context_token_id = tokenizer.convert_tokens_to_ids(IMG_CONTEXT_TOKEN)
+ self.img_context_token_id = img_context_token_id
+ self.img_start_token_id = tokenizer.convert_tokens_to_ids(IMG_START_TOKEN)
+
+ template = get_conv_template(self.template)
+ template.system_message = self.system_message
+ eos_token_id = tokenizer.convert_tokens_to_ids(template.sep.strip())
+
+ history = [] if history is None else history
+ for (old_question, old_answer) in history:
+ template.append_message(template.roles[0], old_question)
+ template.append_message(template.roles[1], old_answer)
+ template.append_message(template.roles[0], question)
+ template.append_message(template.roles[1], None)
+ query = template.get_prompt()
+
+ if verbose and pixel_values is not None:
+ print(f'dynamic image size: {grid_hw[0] * self.patch_size}')
+
+ for i in range(grid_hw.shape[0]):
+ num_patch_token = int(grid_hw[i, 0] * grid_hw[i, 1] * self.downsample_ratio**2)
+ image_tokens = IMG_START_TOKEN + IMG_CONTEXT_TOKEN * num_patch_token + IMG_END_TOKEN
+ query = query.replace('', image_tokens, 1)
+
+ model_inputs = tokenizer(query, return_tensors='pt')
+ input_ids = model_inputs['input_ids'].to(self.device)
+ attention_mask = model_inputs['attention_mask'].to(self.device)
+ generation_config['eos_token_id'] = eos_token_id
+ generation_output = self.generate(
+ pixel_values=pixel_values,
+ input_ids=input_ids,
+ grid_hw=grid_hw,
+ attention_mask=attention_mask,
+ **generation_config
+ )
+ response = tokenizer.batch_decode(generation_output, skip_special_tokens=True)[0]
+ response = response.split(template.sep.strip())[0].strip()
+ history.append((question, response))
+ if return_history:
+ return response, history
+ else:
+ query_to_print = query.replace(IMG_CONTEXT_TOKEN, '')
+ query_to_print = query_to_print.replace(f'{IMG_START_TOKEN}{IMG_END_TOKEN}', '')
+ if verbose:
+ print(query_to_print, response)
+ return response
+
+ @torch.no_grad()
+ def generate(
+ self,
+ pixel_values: Optional[torch.FloatTensor] = None,
+ input_ids: Optional[torch.FloatTensor] = None,
+ grid_hw: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.LongTensor] = None,
+ visual_features: Optional[torch.FloatTensor] = None,
+ generation_config: Optional[GenerationConfig] = None,
+ output_hidden_states: Optional[bool] = None,
+ **generate_kwargs,
+ ) -> torch.LongTensor:
+ assert input_ids.shape[0] == 1
+ assert self.img_context_token_id is not None
+ indexes = self.get_thw_indexes(input_ids[0], grid_hw)
+ if pixel_values is not None:
+ if visual_features is not None:
+ vit_embeds = visual_features
+ else:
+ vit_embeds = self.extract_feature(pixel_values, grid_hw=grid_hw)
+
+ input_embeds = self.language_model.get_input_embeddings()(input_ids)
+ B, N, C = input_embeds.shape
+ input_embeds = input_embeds.reshape(B * N, C)
+
+ input_ids = input_ids.reshape(B * N)
+ selected = (input_ids == self.img_context_token_id)
+ assert selected.sum() != 0
+ input_embeds[selected] = vit_embeds.reshape(-1, C).to(input_embeds.device)
+
+ input_embeds = input_embeds.reshape(B, N, C)
+ else:
+ input_embeds = self.language_model.get_input_embeddings()(input_ids)
+
+ outputs = self.language_model.generate(
+ inputs_embeds=input_embeds,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ generation_config=generation_config,
+ output_hidden_states=output_hidden_states,
+ use_cache=True,
+ **generate_kwargs,
+ )
+
+ return outputs
+
+ @property
+ def lm_head(self):
+ return self.language_model.get_output_embeddings()
+
+ def get_output_embeddings(self):
+ return self.language_model.get_output_embeddings()
+
+ def get_input_embeddings(self):
+ return self.language_model.get_input_embeddings()
+
+ def set_input_embeddings(self, value):
+ return self.language_model.set_input_embeddings(value)
+
+ def set_output_embeddings(self, value):
+ return self.language_model.set_output_embeddings(value)
+
+ def get_thw_indexes(self, input_ids, grid_hw=None):
+ img_start_shift = torch.cat([torch.zeros(1, dtype=torch.long).to(input_ids.device),
+ (input_ids == self.img_start_token_id).long()], dim=0)[:-1]
+ not_img_token = (input_ids != self.img_context_token_id).long()
+ t_indexes = ((img_start_shift + not_img_token).cumsum(0) - 1)
+ h_indexes = torch.zeros_like(t_indexes).to(t_indexes.device)
+ w_indexes = torch.zeros_like(t_indexes).to(t_indexes.device)
+
+ if grid_hw is not None:
+ selected = (input_ids == self.img_context_token_id)
+ if selected.long().sum() > 0:
+ abs_pos_w, abs_pos_h = build_abs_positions_from_grid_hw(
+ grid_hw // int(1 / self.downsample_ratio), device=t_indexes.device)
+ h_indexes[selected] = abs_pos_h.to(t_indexes.device, t_indexes.dtype)
+ w_indexes[selected] = abs_pos_w.to(t_indexes.device, t_indexes.dtype)
+ return torch.stack([t_indexes, h_indexes, w_indexes], dim=0)
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/modeling_neo_vit.py b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_neo_vit.py
new file mode 100644
index 000000000..80e60c5f1
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_neo_vit.py
@@ -0,0 +1,248 @@
+from typing import Optional, Tuple, Union
+
+import torch
+import torch.utils.checkpoint
+from torch import nn
+from transformers.modeling_outputs import BaseModelOutputWithPooling
+from transformers.modeling_utils import PreTrainedModel
+
+from .configuration_neo_vit import NEOVisionConfig
+
+
+def precompute_rope_freqs_sincos(
+ dim: int, max_position: int, base: float = 10000.0, device=None
+):
+ """预计算 RoPE 的 cos 和 sin 值 (1D)。"""
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, device=device).float() / dim))
+ t = torch.arange(max_position, device=device).type_as(inv_freq)
+ freqs = torch.outer(t, inv_freq)
+ return torch.cos(freqs), torch.sin(freqs)
+
+
+def build_abs_positions_from_grid_hw(grid_hw: torch.Tensor, device=None):
+ """
+ Compute patch coordinates (x, y)
+
+ Args:
+ grid_hw: (B, 2) tensor representing (H, W) per image
+ """
+ device = grid_hw.device
+ B = grid_hw.shape[0]
+
+ # Get the number of patches per image
+ H = grid_hw[:, 0]
+ W = grid_hw[:, 1]
+ N = H * W
+ N_total = N.sum()
+
+ # Create the batch index for each patch (B x patch count)
+ patch_to_sample = torch.repeat_interleave(torch.arange(B, device=device), N) # (N_total,)
+
+ # Generate intra-image patch index (row-major order)
+ patch_id_within_image = torch.arange(N_total, device=device)
+ patch_id_within_image = patch_id_within_image - torch.cumsum(
+ torch.cat([torch.tensor([0], device=device), N[:-1]]), dim=0
+ )[patch_to_sample]
+
+ # Get H/W for each patch according to its image
+ W_per_patch = W[patch_to_sample]
+ abs_x = patch_id_within_image % W_per_patch
+ abs_y = patch_id_within_image // W_per_patch
+
+ return abs_x, abs_y
+
+
+def apply_rotary_emb_1d(
+ x: torch.Tensor,
+ cos_cached: torch.Tensor,
+ sin_cached: torch.Tensor,
+ positions: torch.Tensor,
+):
+ """对输入张量的一部分应用1D RoPE。"""
+ # x: (..., seq_len, dim_part)
+ # positions: (..., seq_len)
+ # cos_cached: (max_pos, dim_part / 2)
+
+ cos = cos_cached[positions] # Shape: (positions.shape, dim_part / 2)
+ sin = sin_cached[positions] # Shape: (positions.shape, dim_part / 2)
+
+ x1 = x[..., 0::2]
+ x2 = x[..., 1::2]
+
+ rotated_x1 = x1 * cos - x2 * sin
+ rotated_x2 = x1 * sin + x2 * cos
+
+ x_rotated = torch.empty_like(x)
+ x_rotated[..., 0::2] = rotated_x1
+ x_rotated[..., 1::2] = rotated_x2
+ return x_rotated
+
+
+def apply_2d_rotary_pos_emb(
+ x: torch.Tensor,
+ cos_cached_x: torch.Tensor,
+ sin_cached_x: torch.Tensor,
+ cos_cached_y: torch.Tensor,
+ sin_cached_y: torch.Tensor,
+ abs_positions_x: torch.Tensor,
+ abs_positions_y: torch.Tensor
+):
+ """应用2D RoPE到输入张量x。"""
+ dim = x.shape[-1]
+ dim_half = dim // 2
+
+ # 假设我们将embedding的前半部分用于一个方向的RoPE,后半部分用于另一个方向
+ # 例如,前一半给X坐标,后一半给Y坐标 (或者反过来,但要保持一致)
+ x_part_1 = x[..., :dim_half]
+ x_part_2 = x[..., dim_half:]
+
+ # 将与 abs_positions_x 相关的旋转应用于 x_part_1
+ rotated_part_1 = apply_rotary_emb_1d(
+ x_part_1, cos_cached_x, sin_cached_x, abs_positions_x
+ )
+ # 将与 abs_positions_y 相关的旋转应用于 x_part_2
+ rotated_part_2 = apply_rotary_emb_1d(
+ x_part_2, cos_cached_y, sin_cached_y, abs_positions_y
+ )
+
+ # 将它们重新拼接起来。确保顺序与你分割时一致。
+ return torch.cat((rotated_part_1, rotated_part_2), dim=-1)
+
+
+class NEOVisionEmbeddings(nn.Module):
+ """
+ Embedding Module for Vision.
+ """
+
+ def __init__(self, config: NEOVisionConfig):
+ super().__init__()
+ self.config = config
+ self.embed_dim = config.hidden_size
+ self.llm_embed_dim = config.llm_hidden_size[0]
+ self.downsample_factor = int(1 / config.downsample_ratio[0])
+ self.patch_size = config.patch_size
+
+ self.patch_embedding = nn.Conv2d(
+ in_channels=config.num_channels, out_channels=self.embed_dim, kernel_size=self.patch_size, stride=self.patch_size
+ )
+ self.dense_embedding = nn.Conv2d(
+ in_channels=self.embed_dim, out_channels=self.llm_embed_dim, kernel_size=self.downsample_factor, stride=self.downsample_factor
+ )
+ self.gelu = nn.GELU()
+
+ self.rope_dim_part = self.embed_dim // 2
+ self.max_position_embeddings_vision = config.max_position_embeddings_vision
+ self.rope_theta_vision = config.rope_theta_vision
+
+ # These deterministic caches are not checkpoint state. In
+ # Transformers 5, ``from_pretrained`` constructs models on the meta
+ # device; tensors computed here would later be materialized as
+ # uninitialized memory because persistent=False buffers are absent
+ # from the checkpoint. Build them lazily on the first real device.
+ self.register_buffer("cos_cached_x", None, persistent=False)
+ self.register_buffer("sin_cached_x", None, persistent=False)
+ self.register_buffer("cos_cached_y", None, persistent=False)
+ self.register_buffer("sin_cached_y", None, persistent=False)
+
+ def _ensure_rope_cache(self, device: torch.device) -> None:
+ if self.cos_cached_x is not None and self.cos_cached_x.device == device:
+ return
+
+ cos, sin = precompute_rope_freqs_sincos(
+ self.rope_dim_part,
+ self.max_position_embeddings_vision,
+ base=self.rope_theta_vision,
+ device=device,
+ )
+ self.cos_cached_x = cos
+ self.sin_cached_x = sin
+ self.cos_cached_y = cos.clone()
+ self.sin_cached_y = sin.clone()
+
+ def _apply_2d_rotary_pos_emb(self, patch_embeds, grid_hw):
+ """
+ Apply 2D Rotary Position Embedding to the patch embeddings.
+ """
+ abs_pos_x, abs_pos_y = build_abs_positions_from_grid_hw(grid_hw, device=patch_embeds.device)
+ embeddings = apply_2d_rotary_pos_emb(
+ patch_embeds.to(torch.float32), # RoPE calculations are often more stable in float32
+ self.cos_cached_x, self.sin_cached_x,
+ self.cos_cached_y, self.sin_cached_y,
+ abs_pos_x,
+ abs_pos_y
+ ).to(self.patch_embedding.weight.dtype)
+ return embeddings
+
+ def forward(self, pixel_values: torch.FloatTensor, grid_hw=None) -> torch.Tensor:
+
+ pixel_values = pixel_values.view( #
+ -1,
+ 3,
+ self.patch_size,
+ self.patch_size,
+ ) # [28072, 768] -> [28072, 3, 16, 16]
+ patch_embeds = self.gelu(self.patch_embedding(pixel_values)).view(-1, self.embed_dim)
+ self._ensure_rope_cache(patch_embeds.device)
+ patch_embeds = self._apply_2d_rotary_pos_emb(patch_embeds, grid_hw) # [28072, 1024]
+ assert (grid_hw[:,0] * grid_hw[:,1]).sum() == patch_embeds.shape[0]
+
+ patches_list = []
+ cur_position = 0
+ for i in range(grid_hw.shape[0]):
+ h, w = grid_hw[i]
+ patches_per_img = patch_embeds[cur_position : cur_position + h * w].view(h, w, -1).unsqueeze(0)
+ patches_per_img = self.dense_embedding(patches_per_img.permute(0, 3, 1, 2))
+ patches_per_img = patches_per_img.permute(0, 2, 3, 1)
+ patches_list.append(patches_per_img.view(-1, patches_per_img.shape[-1]))
+ cur_position += h * w
+
+ embeddings = torch.cat(patches_list, dim=0) # (N_total // downsample_factor**2, C)
+
+ assert cur_position == patch_embeds.shape[0]
+ assert embeddings.shape[0] == int(patch_embeds.shape[0] / self.downsample_factor**2)
+
+ return embeddings
+
+
+class NEOVisionModel(PreTrainedModel):
+ main_input_name = 'pixel_values'
+ _supports_flash_attn_2 = True
+ supports_gradient_checkpointing = True
+ config_class = NEOVisionConfig
+ # support transformers 4.51.+
+ _tp_plan = ''
+
+ def __init__(self, config: NEOVisionConfig):
+ super().__init__(config)
+ self.config = config
+
+ self.embeddings = NEOVisionEmbeddings(config)
+
+ def forward(
+ self,
+ pixel_values: Optional[torch.FloatTensor] = None,
+ output_hidden_states: Optional[bool] = None,
+ return_dict: Optional[bool] = None,
+ pixel_embeds: Optional[torch.FloatTensor] = None,
+ grid_hw: Optional[torch.Tensor] = None
+ ) -> Union[Tuple, BaseModelOutputWithPooling]:
+ output_hidden_states = (
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
+ )
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
+
+ if pixel_values is None and pixel_embeds is None:
+ raise ValueError('You have to specify pixel_values or pixel_embeds')
+
+ if pixel_embeds is not None:
+ hidden_states = pixel_embeds
+ else:
+ assert pixel_values.dim() == 2, f"pixel_values must be 2D for native resolution, got: {pixel_values.dim()}"
+ hidden_states = self.embeddings(pixel_values, grid_hw=grid_hw)
+
+ return BaseModelOutputWithPooling(
+ last_hidden_state=hidden_states,
+ pooler_output=None,
+ hidden_states=None,
+ attentions=None,
+ )
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/modeling_qwen3.py b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_qwen3.py
new file mode 100644
index 000000000..ebb961b3a
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_qwen3.py
@@ -0,0 +1,1298 @@
+from typing import Callable, Optional, Union
+
+import torch
+import torch._dynamo
+from torch import nn
+
+import copy
+import math
+from transformers.activations import ACT2FN
+from transformers.cache_utils import Cache, DynamicCache
+from transformers.generation import GenerationMixin
+from transformers.integrations import use_kernel_forward_from_hub
+from transformers.masking_utils import create_causal_mask
+from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
+from transformers.modeling_layers import (
+ GenericForQuestionAnswering,
+ GenericForSequenceClassification,
+ GenericForTokenClassification,
+ GradientCheckpointingLayer,
+)
+from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
+from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update
+from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
+from transformers.processing_utils import Unpack
+from transformers.utils import TransformersKwargs, can_return_tuple
+from transformers.utils.deprecation import deprecate_kwarg
+from transformers import Qwen3Config
+
+from .transformers_compat import causal_mask_kwargs, model_input_compat, tied_weights_keys
+
+try:
+ from flash_attn import flash_attn_func # type: ignore
+
+ _HAS_FLASH_ATTN = True
+except ImportError: # pragma: no cover - exercised only in CPU-only / no-flash envs
+ flash_attn_func = None # type: ignore
+ _HAS_FLASH_ATTN = False
+
+
+# Attention backend dispatch.
+#
+# Set via :func:`set_attn_backend`. Three modes are accepted:
+# * ``"auto"`` - use flash-attn if available, otherwise SDPA (default).
+# * ``"flash"`` - force flash-attn; raise if ``flash_attn`` is not installed.
+# * ``"sdpa"`` - force torch SDPA (useful for reproducibility tests and
+# debugging, even when flash-attn is available).
+_VALID_ATTN_BACKENDS = ("auto", "flash", "sdpa")
+_ATTN_BACKEND: str = "auto"
+
+
+def set_attn_backend(backend: str) -> str:
+ """Choose the attention kernel used by the Qwen3 layers at runtime.
+
+ Returns the backend string that was set. Raises ``ValueError`` for an
+ unknown name and ``RuntimeError`` if ``flash`` is requested but the
+ ``flash_attn`` package isn't importable.
+ """
+ global _ATTN_BACKEND
+ backend = backend.lower()
+ if backend not in _VALID_ATTN_BACKENDS:
+ raise ValueError(
+ f"Unknown attention backend {backend!r}. "
+ f"Expected one of {_VALID_ATTN_BACKENDS}."
+ )
+ if backend == "flash" and not _HAS_FLASH_ATTN:
+ raise RuntimeError(
+ "Requested attn_backend='flash' but `flash_attn` is not installed. "
+ "Install it (e.g. `uv pip install `) or use "
+ "'auto' / 'sdpa'."
+ )
+ _ATTN_BACKEND = backend
+ return _ATTN_BACKEND
+
+
+def get_attn_backend() -> str:
+ """Return the currently active attention backend name."""
+ return _ATTN_BACKEND
+
+
+def effective_attn_backend() -> str:
+ """Resolve ``'auto'`` to the kernel that will actually run."""
+ if _ATTN_BACKEND != "auto":
+ return _ATTN_BACKEND
+ return "flash" if _HAS_FLASH_ATTN else "sdpa"
+
+
+def _sdpa_attn_func(q, k, v, dropout_p: float = 0.0, softmax_scale=None, causal: bool = False):
+ """Drop-in SDPA fallback for ``flash_attn_func``.
+
+ ``flash_attn_func`` expects q/k/v in layout ``[B, S, H, D]`` and returns
+ ``[B, S_q, H_q, D]``. ``torch.nn.functional.scaled_dot_product_attention``
+ expects ``[B, H, S, D]``; we transpose in and out.
+
+ ``flash_attn_func`` natively handles Grouped-Query Attention (GQA) where
+ ``H_q > H_kv``. Plain ``scaled_dot_product_attention`` only supports that
+ via the ``enable_gqa=True`` kwarg (torch >= 2.5). For broader compatibility
+ we just materialize the repeat manually when needed.
+ """
+ q_bhsd = q.transpose(1, 2)
+ k_bhsd = k.transpose(1, 2)
+ v_bhsd = v.transpose(1, 2)
+
+ h_q = q_bhsd.shape[1]
+ h_kv = k_bhsd.shape[1]
+ if h_q != h_kv:
+ if h_q % h_kv != 0:
+ raise ValueError(
+ f"Cannot broadcast key/value heads ({h_kv}) to query heads ({h_q}): not divisible."
+ )
+ n_rep = h_q // h_kv
+ k_bhsd = k_bhsd.repeat_interleave(n_rep, dim=1)
+ v_bhsd = v_bhsd.repeat_interleave(n_rep, dim=1)
+
+ # SDPA does not support an explicit `scale` argument on older torch
+ # versions; fall back to the manual path in that case.
+ try:
+ out = torch.nn.functional.scaled_dot_product_attention(
+ q_bhsd,
+ k_bhsd,
+ v_bhsd,
+ dropout_p=dropout_p,
+ is_causal=causal,
+ scale=softmax_scale,
+ )
+ except TypeError:
+ if softmax_scale is not None:
+ q_bhsd = q_bhsd * softmax_scale
+ out = torch.nn.functional.scaled_dot_product_attention(
+ q_bhsd,
+ k_bhsd,
+ v_bhsd,
+ dropout_p=dropout_p,
+ is_causal=causal,
+ )
+ else:
+ out = torch.nn.functional.scaled_dot_product_attention(
+ q_bhsd,
+ k_bhsd,
+ v_bhsd,
+ dropout_p=dropout_p,
+ is_causal=causal,
+ )
+ return out.transpose(1, 2).contiguous()
+
+
+def _flash_or_sdpa(q, k, v, dropout_p: float = 0.0, softmax_scale=None, causal: bool = False):
+ backend = effective_attn_backend()
+ # flash-attn ships CUDA kernels only. On XPU / CPU we transparently fall
+ # back to SDPA even if the user asked for ``flash`` — the alternative
+ # (crashing on first forward) is worse, and ``set_attn_backend('flash')``
+ # already guarded against the "package missing" case.
+ if backend == "flash" and q.device.type == "cuda":
+ return flash_attn_func(q, k, v, dropout_p=dropout_p, softmax_scale=softmax_scale, causal=causal)
+ return _sdpa_attn_func(q, k, v, dropout_p=dropout_p, softmax_scale=softmax_scale, causal=causal)
+
+
+def create_block_causal_mask(index: torch.Tensor):
+ """
+ index: (L)
+ return: (1, 1, L, L) block-wise causal attention mask
+ """
+ L = index.size(0)
+ idx_i = index.unsqueeze(1).expand(L, L)
+ idx_j = index.unsqueeze(0).expand(L, L)
+
+ arange = torch.arange(L, device=index.device)
+ mask = (idx_j == idx_i) | (arange.unsqueeze(0) <= arange.unsqueeze(1))
+
+ return torch.where(mask[None, None, :, :] > 0, torch.tensor(0.0), torch.tensor(float('-inf')))
+
+
+def visualize_mask(mask: torch.Tensor, i: int = 0, j: int = 12):
+ """
+ mask: (1,1, L, L)
+ """
+ submask = torch.where(mask[0, 0, :, :] == 0, torch.tensor(1.0), torch.tensor(0.0))
+ submask = mask[i:j, i:j].int().cpu().numpy()
+ for row in submask:
+ print(" ".join(map(str, row)))
+
+
+@use_kernel_forward_from_hub("RMSNorm")
+class Qwen3RMSNorm(nn.Module):
+ def __init__(self, hidden_size, eps: float = 1e-6) -> None:
+ """
+ Qwen3RMSNorm is equivalent to T5LayerNorm
+ """
+ super().__init__()
+ self.weight = nn.Parameter(torch.ones(hidden_size))
+ self.variance_epsilon = eps
+
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
+ input_dtype = hidden_states.dtype
+ hidden_states = hidden_states.to(torch.float32)
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
+ return self.weight * hidden_states.to(input_dtype)
+
+ def extra_repr(self):
+ return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
+
+
+class Qwen3MLP(nn.Module):
+ def __init__(self, config):
+ super().__init__()
+ self.config = config
+ self.hidden_size = config.hidden_size
+ self.intermediate_size = config.intermediate_size
+ self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
+ self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
+ self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
+ self.act_fn = ACT2FN[config.hidden_act]
+
+ def forward(self, x):
+ down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
+ return down_proj
+
+
+def rotate_half(x):
+ """Rotates half the hidden dims of the input."""
+ x1 = x[..., : x.shape[-1] // 2]
+ x2 = x[..., x.shape[-1] // 2 :]
+ return torch.cat((-x2, x1), dim=-1)
+
+
+def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
+ """Applies Rotary Position Embedding to the query and key tensors.
+
+ Args:
+ q (`torch.Tensor`): The query tensor.
+ k (`torch.Tensor`): The key tensor.
+ cos (`torch.Tensor`): The cosine part of the rotary embedding.
+ sin (`torch.Tensor`): The sine part of the rotary embedding.
+ position_ids (`torch.Tensor`, *optional*):
+ Deprecated and unused.
+ unsqueeze_dim (`int`, *optional*, defaults to 1):
+ The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and
+ sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note
+ that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and
+ k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes
+ cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have
+ the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2.
+ Returns:
+ `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding.
+ """
+ cos = cos.unsqueeze(unsqueeze_dim)
+ sin = sin.unsqueeze(unsqueeze_dim)
+ q_embed = (q * cos) + (rotate_half(q) * sin)
+ k_embed = (k * cos) + (rotate_half(k) * sin)
+ return q_embed, k_embed
+
+
+def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
+ """
+ This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
+ num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
+ """
+ batch, num_key_value_heads, slen, head_dim = hidden_states.shape
+ if n_rep == 1:
+ return hidden_states
+ hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
+ return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
+
+
+def eager_attention_forward(
+ module: nn.Module,
+ query: torch.Tensor,
+ key: torch.Tensor,
+ value: torch.Tensor,
+ attention_mask: Optional[torch.Tensor],
+ scaling: float,
+ dropout: float = 0.0,
+ **kwargs: Unpack[TransformersKwargs],
+):
+ key_states = repeat_kv(key, module.num_key_value_groups)
+ value_states = repeat_kv(value, module.num_key_value_groups)
+
+ attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
+ if attention_mask is not None:
+ causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]
+ attn_weights = attn_weights + causal_mask
+
+ attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
+ attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
+ attn_output = torch.matmul(attn_weights, value_states)
+ attn_output = attn_output.transpose(1, 2).contiguous()
+
+ return attn_output, attn_weights
+
+
+def _compute_default_rope_parameters(config, device=None, **_kwargs):
+ """Default RoPE frequencies, inlined to avoid breakage across transformers versions.
+
+ transformers <=4.x exposes this as ``ROPE_INIT_FUNCTIONS["default"]``, but
+ 5.x dropped the ``"default"`` key from that table. Having a local copy keeps
+ ``Qwen3RotaryEmbedding`` working on both.
+ """
+ base = config.rope_theta
+ partial_rotary_factor = getattr(config, "partial_rotary_factor", 1.0)
+ head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
+ dim = int(head_dim * partial_rotary_factor)
+ attention_factor = 1.0
+ inv_freq = 1.0 / (
+ base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim)
+ )
+ return inv_freq, attention_factor
+
+
+class Qwen3RotaryEmbedding(nn.Module):
+ inv_freq: torch.Tensor # fix linting for `register_buffer`
+
+ compute_default_rope_parameters = staticmethod(_compute_default_rope_parameters)
+
+ def __init__(self, config: Qwen3Config, device=None):
+ super().__init__()
+ # BC: "rope_type" was originally "type"
+ if hasattr(config, "rope_scaling") and isinstance(config.rope_scaling, dict):
+ self.rope_type = config.rope_scaling.get("rope_type", config.rope_scaling.get("type"))
+ else:
+ self.rope_type = "default"
+ self.max_seq_len_cached = config.max_position_embeddings
+ self.original_max_seq_len = config.max_position_embeddings
+
+ self.config = config
+ if self.rope_type == "default" or self.rope_type is None:
+ base_rope_init_fn = _compute_default_rope_parameters
+ else:
+ base_rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
+
+ def _rope_init_fn_keep_freq_range(cfg: Qwen3Config, dev=None):
+ inv_freq, attention_scaling = base_rope_init_fn(cfg, dev)
+
+ cfg2 = copy.deepcopy(cfg)
+ head_dim = getattr(cfg2, "head_dim", None)
+ if head_dim is None:
+ head_dim = cfg2.hidden_size // cfg2.num_attention_heads
+ setattr(cfg2, "head_dim", head_dim)
+ cfg2.head_dim = int(head_dim) * 2
+
+ inv_freq_full, _ = base_rope_init_fn(cfg2, dev)
+ inv_freq = inv_freq_full[::2]
+
+ return inv_freq, attention_scaling
+
+ self.rope_init_fn = _rope_init_fn_keep_freq_range
+
+ inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device)
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
+ self.original_inv_freq = self.inv_freq
+
+ @torch.no_grad()
+ @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope)
+ def forward(self, x, position_ids):
+ inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
+ position_ids_expanded = position_ids[:, None, :].float()
+
+ device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu"
+ with torch.autocast(device_type=device_type, enabled=False): # Force float32
+ freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
+ emb = torch.cat((freqs, freqs), dim=-1)
+ cos = emb.cos() * self.attention_scaling
+ sin = emb.sin() * self.attention_scaling
+
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
+
+
+class Qwen3Attention(nn.Module):
+ """Multi-headed attention from 'Attention Is All You Need' paper"""
+
+ def __init__(self, config: Qwen3Config, layer_idx: int):
+ super().__init__()
+ self.config = config
+ self.layer_idx = layer_idx
+ self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
+ self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads
+ self.scaling = self.head_dim**-0.5
+ self.attention_dropout = config.attention_dropout
+ self.is_causal = True
+
+ self.q_proj = nn.Linear(
+ config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias
+ )
+ self.q_proj_mot_gen = nn.Linear(
+ config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias
+ )
+
+ self.k_proj = nn.Linear(
+ config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
+ )
+ self.k_proj_mot_gen = nn.Linear(
+ config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
+ )
+
+ self.v_proj = nn.Linear(
+ config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
+ )
+ self.v_proj_mot_gen = nn.Linear(
+ config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
+ )
+ self.o_proj = nn.Linear(
+ config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias
+ )
+ self.o_proj_mot_gen = nn.Linear(
+ config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias
+ )
+
+ self.q_norm = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps) # unlike olmo, only on the head dim!
+ self.q_norm_mot_gen = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps)
+ self.q_norm_hw = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps)
+ self.q_norm_hw_mot_gen = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps)
+
+ self.k_norm = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps) # thus post q_norm does not need reshape
+ self.k_norm_mot_gen = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps)
+ self.k_norm_hw = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps) # thus post q_norm does not need reshape
+ self.k_norm_hw_mot_gen = Qwen3RMSNorm(self.head_dim // 2, eps=config.rms_norm_eps)
+
+ self.sliding_window = config.sliding_window if config.layer_types[layer_idx] == "sliding_attention" else None
+
+ t_config = copy.deepcopy(config)
+ t_config.head_dim = config.head_dim // 2
+ self.rotary_emb = Qwen3RotaryEmbedding(config=t_config)
+
+ hw_config = copy.deepcopy(config)
+ hw_config.head_dim = config.head_dim // 4
+ hw_config.rope_theta = config.rope_theta_hw
+ if isinstance(getattr(hw_config, "rope_parameters", None), dict):
+ hw_config.rope_parameters = {**hw_config.rope_parameters, "rope_theta": config.rope_theta_hw}
+ hw_config.max_position_embeddings = config.max_position_embeddings_hw
+ self.rotary_emb_hw = Qwen3RotaryEmbedding(config=hw_config)
+
+ def forward_und(
+ self,
+ hidden_states: torch.Tensor,
+ indexes: Optional[torch.LongTensor],
+ attention_mask: Optional[torch.Tensor],
+ past_key_values: Optional[Cache] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[FlashAttentionKwargs],
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
+ assert self.config._attn_implementation == "eager"
+ input_shape = hidden_states.shape[:-1]
+ hidden_shape = (*input_shape, -1, self.head_dim)
+
+ query_states = self.q_proj(hidden_states).view(hidden_shape)
+ query_states_t, query_states_hw = query_states.chunk(2, dim=-1)
+ query_states_t = self.q_norm(query_states_t).transpose(1, 2)
+ query_states_hw = self.q_norm_hw(query_states_hw).transpose(1, 2)
+ query_states_h, query_states_w = query_states_hw.chunk(2, dim=-1)
+
+ key_states = self.k_proj(hidden_states).view(hidden_shape)
+ key_states_t, key_states_hw = key_states.chunk(2, dim=-1)
+ key_states_t = self.k_norm(key_states_t).transpose(1, 2)
+ key_states_hw = self.k_norm_hw(key_states_hw).transpose(1, 2)
+ key_states_h, key_states_w = key_states_hw.chunk(2, dim=-1)
+
+ value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
+
+ cos_t, sin_t = self.rotary_emb(hidden_states, indexes[0].unsqueeze(0))
+ query_states_t, key_states_t = apply_rotary_pos_emb(query_states_t, key_states_t, cos_t, sin_t)
+
+ cos_h, sin_h = self.rotary_emb_hw(hidden_states, indexes[1].unsqueeze(0))
+ query_states_h, key_states_h = apply_rotary_pos_emb(query_states_h, key_states_h, cos_h, sin_h)
+
+ cos_w, sin_w = self.rotary_emb_hw(hidden_states, indexes[2].unsqueeze(0))
+ query_states_w, key_states_w = apply_rotary_pos_emb(query_states_w, key_states_w, cos_w, sin_w)
+
+ query_states = torch.cat([query_states_t, query_states_h, query_states_w], dim=-1)
+ key_states = torch.cat([key_states_t, key_states_h, key_states_w], dim=-1)
+
+
+ if past_key_values is not None:
+ # sin and cos are specific to RoPE models; cache_position needed for the static cache
+ # cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
+ # key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs)
+ update_cache = kwargs.get("update_cache", True)
+ if update_cache:
+ key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs=None)
+ else:
+ # only use the past key values but do not append the current one
+ layer = past_key_values.layers[self.layer_idx]
+ past_k, past_v = layer.keys, layer.values
+
+ if past_k is not None:
+ key_states = torch.cat([past_k, key_states], dim=2) # concat on seq_len
+ value_states = torch.cat([past_v, value_states], dim=2)
+
+ attention_interface: Callable = eager_attention_forward
+ if self.config._attn_implementation != "eager":
+ attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
+
+ attn_output, attn_weights = attention_interface(
+ self,
+ query_states,
+ key_states,
+ value_states,
+ attention_mask,
+ dropout=0.0 if not self.training else self.attention_dropout,
+ scaling=self.scaling,
+ sliding_window=self.sliding_window, # diff with Llama
+ **kwargs,
+ )
+
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
+ attn_output = self.o_proj(attn_output)
+ return attn_output, attn_weights
+
+ # def forward_gen(
+ # self,
+ # hidden_states: torch.Tensor,
+ # indexes: Optional[torch.LongTensor],
+ # attention_mask: Optional[torch.Tensor],
+ # past_key_values: Optional[Cache] = None,
+ # cache_position: Optional[torch.LongTensor] = None,
+ # **kwargs: Unpack[FlashAttentionKwargs],
+ # ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
+ # assert self.config._attn_implementation == "eager"
+ # input_shape = hidden_states.shape[:-1]
+ # hidden_shape = (*input_shape, -1, self.head_dim)
+
+ # query_states = self.q_proj_mot_gen(hidden_states).view(hidden_shape)
+ # query_states_t, query_states_hw = query_states.chunk(2, dim=-1)
+ # query_states_t = self.q_norm_mot_gen(query_states_t).transpose(1, 2)
+ # query_states_hw = self.q_norm_hw_mot_gen(query_states_hw).transpose(1, 2)
+ # query_states_h, query_states_w = query_states_hw.chunk(2, dim=-1)
+
+ # key_states = self.k_proj_mot_gen(hidden_states).view(hidden_shape)
+ # key_states_t, key_states_hw = key_states.chunk(2, dim=-1)
+ # key_states_t = self.k_norm_mot_gen(key_states_t).transpose(1, 2)
+ # key_states_hw = self.k_norm_hw_mot_gen(key_states_hw).transpose(1, 2)
+ # key_states_h, key_states_w = key_states_hw.chunk(2, dim=-1)
+
+ # value_states = self.v_proj_mot_gen(hidden_states).view(hidden_shape).transpose(1, 2)
+
+ # cos_t, sin_t = self.rotary_emb(hidden_states, indexes[0].unsqueeze(0))
+ # query_states_t, key_states_t = apply_rotary_pos_emb(query_states_t, key_states_t, cos_t, sin_t)
+
+ # cos_h, sin_h = self.rotary_emb_hw(hidden_states, indexes[1].unsqueeze(0))
+ # query_states_h, key_states_h = apply_rotary_pos_emb(query_states_h, key_states_h, cos_h, sin_h)
+
+ # cos_w, sin_w = self.rotary_emb_hw(hidden_states, indexes[2].unsqueeze(0))
+ # query_states_w, key_states_w = apply_rotary_pos_emb(query_states_w, key_states_w, cos_w, sin_w)
+
+ # query_states = torch.cat([query_states_t, query_states_h, query_states_w], dim=-1)
+ # key_states = torch.cat([key_states_t, key_states_h, key_states_w], dim=-1)
+
+
+ # if past_key_values is not None:
+ # # sin and cos are specific to RoPE models; cache_position needed for the static cache
+ # # cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
+ # # key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs)
+ # update_cache = kwargs.get("update_cache", True)
+ # if update_cache:
+ # key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs=None)
+ # else:
+ # # only use the past key values but do not append the current one
+ # layer = past_key_values.layers[self.layer_idx]
+ # past_k, past_v = layer.keys, layer.values
+
+ # if past_k is not None:
+ # key_states = torch.cat([past_k, key_states], dim=2) # concat on seq_len
+ # value_states = torch.cat([past_v, value_states], dim=2)
+
+ # attention_interface: Callable = eager_attention_forward
+ # if self.config._attn_implementation != "eager":
+ # attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
+
+ # attn_output, attn_weights = attention_interface(
+ # self,
+ # query_states,
+ # key_states,
+ # value_states,
+ # attention_mask,
+ # dropout=0.0 if not self.training else self.attention_dropout,
+ # scaling=self.scaling,
+ # sliding_window=self.sliding_window, # diff with Llama
+ # **kwargs,
+ # )
+
+ # attn_output = attn_output.reshape(*input_shape, -1).contiguous()
+ # attn_output = self.o_proj_mot_gen(attn_output)
+ # return attn_output, attn_weights
+
+ def forward_gen(
+ self,
+ hidden_states: torch.Tensor,
+ indexes: Optional[torch.LongTensor],
+ attention_mask: Optional[torch.Tensor],
+ past_key_values: Optional[Cache] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[FlashAttentionKwargs],
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
+ input_shape = hidden_states.shape[:-1]
+ hidden_shape = (*input_shape, -1, self.head_dim)
+
+ # -----------------------------
+ # Build q / k / v for current tokens
+ # Internal layout before flash:
+ # q/k/v: [B, H, S, D]
+ # Flash layout:
+ # q/k/v: [B, S, H, D]
+ # -----------------------------
+ query_states = self.q_proj_mot_gen(hidden_states).view(hidden_shape)
+ query_states_t, query_states_hw = query_states.chunk(2, dim=-1)
+ query_states_t = self.q_norm_mot_gen(query_states_t).transpose(1, 2) # [B,H,S,D/2]
+ query_states_hw = self.q_norm_hw_mot_gen(query_states_hw).transpose(1, 2)
+ query_states_h, query_states_w = query_states_hw.chunk(2, dim=-1)
+
+ key_states = self.k_proj_mot_gen(hidden_states).view(hidden_shape)
+ key_states_t, key_states_hw = key_states.chunk(2, dim=-1)
+ key_states_t = self.k_norm_mot_gen(key_states_t).transpose(1, 2) # [B,H,S,D/2]
+ key_states_hw = self.k_norm_hw_mot_gen(key_states_hw).transpose(1, 2)
+ key_states_h, key_states_w = key_states_hw.chunk(2, dim=-1)
+
+ value_states = self.v_proj_mot_gen(hidden_states).view(hidden_shape).transpose(1, 2) # [B,H,S,D]
+
+ # RoPE
+ cos_t, sin_t = self.rotary_emb(hidden_states, indexes[0].unsqueeze(0))
+ query_states_t, key_states_t = apply_rotary_pos_emb(query_states_t, key_states_t, cos_t, sin_t)
+
+ cos_h, sin_h = self.rotary_emb_hw(hidden_states, indexes[1].unsqueeze(0))
+ query_states_h, key_states_h = apply_rotary_pos_emb(query_states_h, key_states_h, cos_h, sin_h)
+
+ cos_w, sin_w = self.rotary_emb_hw(hidden_states, indexes[2].unsqueeze(0))
+ query_states_w, key_states_w = apply_rotary_pos_emb(query_states_w, key_states_w, cos_w, sin_w)
+
+ # concat along head_dim
+ # query/key current layout: [B, H, S, D]
+ query_states = torch.cat([query_states_t, query_states_h, query_states_w], dim=-1)
+ key_states = torch.cat([key_states_t, key_states_h, key_states_w], dim=-1)
+
+ update_cache = kwargs.get("update_cache", True)
+
+ # ------------------------------------------------------------------
+ # Flash path:
+ # Only use when there is no explicit dense mask.
+ # This is exactly the t2i denoising use case:
+ # current image tokens attend to [prefix + current image tokens]
+ # fully bidirectional inside current block => causal=False
+ # ------------------------------------------------------------------
+ if attention_mask is None:
+ # Convert current q/k/v to flash layout [B, S, H, D]
+ q = query_states.transpose(1, 2).contiguous()
+ k_cur = key_states.transpose(1, 2).contiguous()
+ v_cur = value_states.transpose(1, 2).contiguous()
+
+ if past_key_values is not None:
+ if update_cache:
+ # Rare path, keep compatibility.
+ # past_key_values.update expects [B,H,S,D]
+ key_states, value_states = past_key_values.update(
+ key_states, value_states, self.layer_idx, cache_kwargs=None
+ )
+ k = key_states.transpose(1, 2).contiguous()
+ v = value_states.transpose(1, 2).contiguous()
+ else:
+ # Optimized path:
+ # use preallocated flash_k_cache / flash_v_cache
+ layer = past_key_values.layers[self.layer_idx]
+
+ if (
+ hasattr(layer, "flash_k_cache")
+ and layer.flash_k_cache is not None
+ and hasattr(layer, "flash_v_cache")
+ and layer.flash_v_cache is not None
+ ):
+ prefix_len = layer.flash_prefix_len
+ cur_len = k_cur.shape[1]
+
+ # overwrite current segment in-place
+ layer.flash_k_cache[:, prefix_len:prefix_len + cur_len].copy_(k_cur)
+ layer.flash_v_cache[:, prefix_len:prefix_len + cur_len].copy_(v_cur)
+
+ k = layer.flash_k_cache[:, :prefix_len + cur_len]
+ v = layer.flash_v_cache[:, :prefix_len + cur_len]
+ else:
+ # fallback if user forgot to prepare flash cache
+ layer = past_key_values.layers[self.layer_idx]
+ past_k, past_v = layer.keys, layer.values
+
+ if past_k is not None:
+ past_k = past_k.transpose(1, 2).contiguous()
+ past_v = past_v.transpose(1, 2).contiguous()
+ k = torch.cat([past_k, k_cur], dim=1)
+ v = torch.cat([past_v, v_cur], dim=1)
+ else:
+ k = k_cur
+ v = v_cur
+ else:
+ k = k_cur
+ v = v_cur
+
+ # sanity checks
+ assert q.ndim == 4 and k.ndim == 4 and v.ndim == 4
+ assert q.shape[0] == k.shape[0] == v.shape[0], (q.shape, k.shape, v.shape)
+ assert k.shape[1] == v.shape[1], (k.shape, v.shape)
+ assert k.shape[2] == v.shape[2], (k.shape, v.shape)
+ assert q.shape[3] == k.shape[3] == v.shape[3], (q.shape, k.shape, v.shape)
+
+ attn_output = _flash_or_sdpa(
+ q,
+ k,
+ v,
+ dropout_p=0.0 if not self.training else self.attention_dropout,
+ softmax_scale=self.scaling,
+ causal=False,
+ ) # [B, S_q, H_q, D]
+
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
+ attn_output = self.o_proj_mot_gen(attn_output)
+ return attn_output, None
+
+ # ------------------------------------------------------------------
+ # Original eager fallback path
+ # ------------------------------------------------------------------
+ if past_key_values is not None:
+ if update_cache:
+ key_states, value_states = past_key_values.update(
+ key_states, value_states, self.layer_idx, cache_kwargs=None
+ )
+ else:
+ layer = past_key_values.layers[self.layer_idx]
+ past_k, past_v = layer.keys, layer.values
+ if past_k is not None:
+ key_states = torch.cat([past_k, key_states], dim=2)
+ value_states = torch.cat([past_v, value_states], dim=2)
+
+ attention_interface: Callable = eager_attention_forward
+ if self.config._attn_implementation != "eager":
+ attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
+
+ attn_output, attn_weights = attention_interface(
+ self,
+ query_states,
+ key_states,
+ value_states,
+ attention_mask,
+ dropout=0.0 if not self.training else self.attention_dropout,
+ scaling=self.scaling,
+ sliding_window=self.sliding_window,
+ **kwargs,
+ )
+
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
+ attn_output = self.o_proj_mot_gen(attn_output)
+ return attn_output, attn_weights
+
+ @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ image_gen_indicators: torch.Tensor,
+ exist_non_image_gen_tokens: bool,
+ exist_image_gen_tokens: bool,
+ indexes: Optional[torch.LongTensor],
+ attention_mask: Optional[torch.Tensor],
+ past_key_values: Optional[Cache] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[FlashAttentionKwargs],
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
+ if exist_non_image_gen_tokens and not exist_image_gen_tokens:
+ return self.forward_und(hidden_states, indexes, attention_mask, past_key_values, cache_position, **kwargs)
+ if not exist_non_image_gen_tokens and exist_image_gen_tokens:
+ return self.forward_gen(hidden_states, indexes, attention_mask, past_key_values, cache_position, **kwargs)
+
+ # Mixed und/gen path: mirrors forward_und / forward_gen per token type.
+ # (Fixed per issue #207: the time-dim qk-norm, the `.view(hidden_shape)` before
+ # chunking, and the transpose on the time chunk were previously missing.)
+ # Note: Remove this raise once fully tested.
+ raise NotImplementedError(
+ "The mixed und/gen forward path is not yet validated (issue #207): known "
+ "issues are fixed, but it has no parity test and no production caller. "
+ "Split the sequence at token-type boundaries and use forward_und / forward_gen."
+ )
+
+ assert self.config._attn_implementation == "eager"
+ input_shape = hidden_states.shape[:-1]
+ hidden_shape = (*input_shape, -1, self.head_dim)
+
+ query_states = hidden_states.new_zeros((*input_shape, self.config.num_attention_heads*self.head_dim))
+ if exist_non_image_gen_tokens:
+ query_states[~image_gen_indicators] = self.q_proj(hidden_states[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ query_states[image_gen_indicators] = self.q_proj_mot_gen(hidden_states[image_gen_indicators])
+ query_states = query_states.view(hidden_shape) # [B, S, H, D]
+ query_states_t, query_states_hw = query_states.chunk(2, dim=-1)
+
+ _query_states_t = query_states_t.new_zeros(query_states_t.shape)
+ if exist_non_image_gen_tokens:
+ _query_states_t[~image_gen_indicators] = self.q_norm(query_states_t[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ _query_states_t[image_gen_indicators] = self.q_norm_mot_gen(query_states_t[image_gen_indicators])
+ query_states_t = _query_states_t.transpose(1, 2) # [B, H, S, D/2]
+
+ _query_states_hw = query_states_hw.new_zeros(query_states_hw.shape)
+ if exist_non_image_gen_tokens:
+ _query_states_hw[~image_gen_indicators] = self.q_norm_hw(query_states_hw[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ _query_states_hw[image_gen_indicators] = self.q_norm_hw_mot_gen(query_states_hw[image_gen_indicators])
+ query_states_hw = _query_states_hw.transpose(1, 2)
+ query_states_h, query_states_w = query_states_hw.chunk(2, dim=-1)
+
+ key_states = hidden_states.new_zeros((*input_shape, self.config.num_key_value_heads*self.head_dim))
+ if exist_non_image_gen_tokens:
+ key_states[~image_gen_indicators] = self.k_proj(hidden_states[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ key_states[image_gen_indicators] = self.k_proj_mot_gen(hidden_states[image_gen_indicators])
+ key_states = key_states.view(hidden_shape) # [B, S, H_kv, D]
+ key_states_t, key_states_hw = key_states.chunk(2, dim=-1)
+
+ _key_states_t = key_states_t.new_zeros(key_states_t.shape)
+ if exist_non_image_gen_tokens:
+ _key_states_t[~image_gen_indicators] = self.k_norm(key_states_t[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ _key_states_t[image_gen_indicators] = self.k_norm_mot_gen(key_states_t[image_gen_indicators])
+ key_states_t = _key_states_t.transpose(1, 2) # [B, H_kv, S, D/2]
+
+ _key_states_hw = key_states_hw.new_zeros(key_states_hw.shape)
+ if exist_non_image_gen_tokens:
+ _key_states_hw[~image_gen_indicators] = self.k_norm_hw(key_states_hw[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ _key_states_hw[image_gen_indicators] = self.k_norm_hw_mot_gen(key_states_hw[image_gen_indicators])
+ key_states_hw = _key_states_hw.transpose(1, 2)
+ key_states_h, key_states_w = key_states_hw.chunk(2, dim=-1)
+
+ value_states = hidden_states.new_zeros((*input_shape, self.config.num_key_value_heads*self.head_dim))
+ if exist_non_image_gen_tokens:
+ value_states[~image_gen_indicators] = self.v_proj(hidden_states[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ value_states[image_gen_indicators] = self.v_proj_mot_gen(hidden_states[image_gen_indicators])
+ value_states = value_states.view(hidden_shape).transpose(1, 2)
+
+ cos_t, sin_t = self.rotary_emb(hidden_states, indexes[0].unsqueeze(0))
+ query_states_t, key_states_t = apply_rotary_pos_emb(query_states_t, key_states_t, cos_t, sin_t)
+
+ cos_h, sin_h = self.rotary_emb_hw(hidden_states, indexes[1].unsqueeze(0))
+ query_states_h, key_states_h = apply_rotary_pos_emb(query_states_h, key_states_h, cos_h, sin_h)
+
+ cos_w, sin_w = self.rotary_emb_hw(hidden_states, indexes[2].unsqueeze(0))
+ query_states_w, key_states_w = apply_rotary_pos_emb(query_states_w, key_states_w, cos_w, sin_w)
+
+ query_states = torch.cat([query_states_t, query_states_h, query_states_w], dim=-1)
+ key_states = torch.cat([key_states_t, key_states_h, key_states_w], dim=-1)
+
+
+ if past_key_values is not None:
+ # sin and cos are specific to RoPE models; cache_position needed for the static cache
+ # cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
+ # key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs)
+ update_cache = kwargs.get("update_cache", True)
+ if update_cache:
+ key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs=None)
+ else:
+ # only use the past key values but do not append the current one
+ layer = past_key_values.layers[self.layer_idx]
+ past_k, past_v = layer.keys, layer.values
+
+ if past_k is not None:
+ key_states = torch.cat([past_k, key_states], dim=2) # concat on seq_len
+ value_states = torch.cat([past_v, value_states], dim=2)
+
+ attention_interface: Callable = eager_attention_forward
+ if self.config._attn_implementation != "eager":
+ attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
+
+ attn_output, attn_weights = attention_interface(
+ self,
+ query_states,
+ key_states,
+ value_states,
+ attention_mask,
+ dropout=0.0 if not self.training else self.attention_dropout,
+ scaling=self.scaling,
+ sliding_window=self.sliding_window, # diff with Llama
+ **kwargs,
+ )
+
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
+
+ _attn_output = attn_output.new_zeros((*input_shape, self.config.hidden_size))
+ if exist_non_image_gen_tokens:
+ _attn_output[~image_gen_indicators] = self.o_proj(attn_output[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ _attn_output[image_gen_indicators] = self.o_proj_mot_gen(attn_output[image_gen_indicators])
+
+ attn_output = _attn_output
+ return attn_output, attn_weights
+
+
+class Qwen3DecoderLayer(GradientCheckpointingLayer):
+ def __init__(self, config: Qwen3Config, layer_idx: int):
+ super().__init__()
+ self.hidden_size = config.hidden_size
+
+ self.self_attn = Qwen3Attention(config=config, layer_idx=layer_idx)
+
+ self.mlp = Qwen3MLP(config)
+ self.mlp_mot_gen = Qwen3MLP(config)
+ self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.input_layernorm_mot_gen = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.post_attention_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.post_attention_layernorm_mot_gen = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.attention_type = config.layer_types[layer_idx]
+
+ def forward_und(
+ self,
+ hidden_states: torch.Tensor,
+ image_gen_indicators: torch.Tensor,
+ exist_non_image_gen_tokens: bool,
+ exist_image_gen_tokens: bool,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ use_cache: Optional[bool] = False,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> torch.Tensor:
+ residual = hidden_states
+ hidden_states = self.input_layernorm(hidden_states)
+ # Self Attention
+ hidden_states, _ = self.self_attn(
+ hidden_states=hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ hidden_states = residual + hidden_states
+
+ # Fully Connected
+ residual = hidden_states
+ hidden_states = self.post_attention_layernorm(hidden_states)
+ hidden_states = self.mlp(hidden_states)
+ hidden_states = residual + hidden_states
+ return hidden_states
+
+ def forward_gen(
+ self,
+ hidden_states: torch.Tensor,
+ image_gen_indicators: torch.Tensor,
+ exist_non_image_gen_tokens: bool,
+ exist_image_gen_tokens: bool,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ use_cache: Optional[bool] = False,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> torch.Tensor:
+ residual = hidden_states
+ hidden_states = self.input_layernorm_mot_gen(hidden_states)
+ # Self Attention
+ hidden_states, _ = self.self_attn(
+ hidden_states=hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ hidden_states = residual + hidden_states
+
+ # Fully Connected
+ residual = hidden_states
+ hidden_states = self.post_attention_layernorm_mot_gen(hidden_states)
+ hidden_states = self.mlp_mot_gen(hidden_states)
+ hidden_states = residual + hidden_states
+ return hidden_states
+
+ @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ image_gen_indicators: torch.Tensor,
+ exist_non_image_gen_tokens: bool,
+ exist_image_gen_tokens: bool,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ use_cache: Optional[bool] = False,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> torch.Tensor:
+ if exist_non_image_gen_tokens and not exist_image_gen_tokens:
+ return self.forward_und(hidden_states, image_gen_indicators, exist_non_image_gen_tokens, exist_image_gen_tokens, indexes, attention_mask, position_ids, past_key_values, use_cache, cache_position, **kwargs)
+ if not exist_non_image_gen_tokens and exist_image_gen_tokens:
+ return self.forward_gen(hidden_states, image_gen_indicators, exist_non_image_gen_tokens, exist_image_gen_tokens, indexes, attention_mask, position_ids, past_key_values, use_cache, cache_position, **kwargs)
+
+ # Mixed und/gen path — see the NOTE in Qwen3Attention.forward for caveats.
+ raise NotImplementedError(
+ "Mixed und/gen decoder-layer forward is not yet validated (issue #207). "
+ "Split the sequence at token-type boundaries and use forward_und / forward_gen."
+ )
+
+ residual = hidden_states
+
+ _hidden_states = hidden_states.new_zeros(hidden_states.shape)
+ if exist_non_image_gen_tokens:
+ _hidden_states[~image_gen_indicators] = self.input_layernorm(hidden_states[~image_gen_indicators])
+ if exist_image_gen_tokens:
+ _hidden_states[image_gen_indicators] = self.input_layernorm_mot_gen(hidden_states[image_gen_indicators])
+ hidden_states = _hidden_states
+
+ # Self Attention
+ hidden_states, _ = self.self_attn(
+ hidden_states=hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ hidden_states = residual + hidden_states
+
+ # Fully Connected
+ residual = hidden_states
+
+ _hidden_states = hidden_states.new_zeros(hidden_states.shape)
+ if exist_non_image_gen_tokens:
+ _hidden_states[~image_gen_indicators] = self.mlp(self.post_attention_layernorm(hidden_states[~image_gen_indicators]))
+
+ if exist_image_gen_tokens:
+ _hidden_states[image_gen_indicators] = self.mlp_mot_gen(self.post_attention_layernorm_mot_gen(hidden_states[image_gen_indicators]))
+
+ hidden_states = _hidden_states
+ hidden_states = residual + hidden_states
+ return hidden_states
+
+
+class Qwen3PreTrainedModel(PreTrainedModel):
+ config: Qwen3Config
+ base_model_prefix = "model"
+ supports_gradient_checkpointing = True
+ _no_split_modules = ["Qwen3DecoderLayer"]
+ _skip_keys_device_placement = ["past_key_values"]
+ _supports_flash_attn = True
+ _supports_sdpa = True
+ _supports_flex_attn = True
+
+ _can_compile_fullgraph = True
+ _supports_attention_backend = True
+ _can_record_outputs = {
+ "hidden_states": Qwen3DecoderLayer,
+ "attentions": Qwen3Attention,
+ }
+
+
+class Qwen3Model(Qwen3PreTrainedModel):
+ def __init__(self, config: Qwen3Config):
+ super().__init__(config)
+ self.padding_idx = config.pad_token_id
+ self.vocab_size = config.vocab_size
+
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
+ self.layers = nn.ModuleList(
+ [Qwen3DecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
+ )
+ self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.norm_mot_gen = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+
+ self.gradient_checkpointing = False
+ self.has_sliding_layers = "sliding_attention" in self.config.layer_types
+ self.current_index = -1
+
+ # Initialize weights and apply final processing
+ self.post_init()
+
+ @model_input_compat
+ def forward(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ image_gen_indicators: Optional[torch.Tensor] = None,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ inputs_embeds: Optional[torch.FloatTensor] = None,
+ use_cache: Optional[bool] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> BaseModelOutputWithPast:
+
+ # assert position_ids is not None
+ # assert cache_position is not None
+ # assert past_key_values is not None
+
+ if image_gen_indicators is None:
+ exist_non_image_gen_tokens = True
+ exist_image_gen_tokens = False
+ else:
+ # Normalize once before the decoder loop. Leaving these as CUDA
+ # scalar tensors makes every layer's Python branch read them back
+ # to the host, serializing the compute stream and defeating weight
+ # prefetch overlap.
+ exist_non_image_gen_tokens = bool((~image_gen_indicators).any().item())
+ exist_image_gen_tokens = bool(image_gen_indicators.any().item())
+
+ if (input_ids is None) ^ (inputs_embeds is not None):
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
+
+ if inputs_embeds is None:
+ inputs_embeds = self.embed_tokens(input_ids)
+
+ if use_cache and past_key_values is None:
+ past_key_values = DynamicCache(config=self.config)
+
+ if cache_position is None:
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
+ cache_position = torch.arange(
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
+ )
+
+ if position_ids is None:
+ position_ids = cache_position.unsqueeze(0)
+
+ # It may already have been prepared by e.g. `generate`
+ if not isinstance(causal_mask_mapping := attention_mask, dict):
+ # Prepare mask arguments
+ if input_ids is not None:
+ mask_kwargs = causal_mask_kwargs(
+ create_causal_mask,
+ config=self.config,
+ inputs_embeds=inputs_embeds,
+ attention_mask=attention_mask,
+ cache_position=cache_position,
+ past_key_values=past_key_values,
+ position_ids=position_ids,
+ )
+ # Create the masks
+ causal_mask_mapping = {
+ "full_attention": create_causal_mask(**mask_kwargs),
+ }
+ self.current_index += 1
+ indexes = torch.LongTensor([[self.current_index], [0], [0]]).to(input_ids.device)
+ else:
+ causal_mask_mapping = {
+ "full_attention": create_block_causal_mask(indexes[0]),
+ }
+ self.current_index = indexes[0].max()
+ else:
+ self.current_index = indexes[0].max()
+ # raise NotImplementedError('not isinstance(causal_mask_mapping := attention_mask, dict)')
+
+ # The sliding window alternating layers are not always activated depending on the config
+ # if self.has_sliding_layers:
+ # causal_mask_mapping["sliding_attention"] = create_sliding_window_causal_mask(**mask_kwargs)
+
+ hidden_states = inputs_embeds
+
+ for decoder_layer in self.layers[: self.config.num_hidden_layers]:
+ hidden_states = decoder_layer(
+ hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=causal_mask_mapping[decoder_layer.attention_type],
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ if not exist_image_gen_tokens:
+ hidden_states = self.norm(hidden_states)
+ elif not exist_non_image_gen_tokens:
+ hidden_states = self.norm_mot_gen(hidden_states)
+ else:
+ _hidden_states = hidden_states.new_zeros(hidden_states.shape)
+ _hidden_states[~image_gen_indicators] = self.norm(hidden_states[~image_gen_indicators])
+ _hidden_states[image_gen_indicators] = self.norm_mot_gen(hidden_states[image_gen_indicators])
+ hidden_states = _hidden_states
+ return BaseModelOutputWithPast(
+ last_hidden_state=hidden_states,
+ past_key_values=past_key_values if use_cache else None,
+ )
+
+
+class Qwen3ForCausalLM(Qwen3PreTrainedModel, GenerationMixin):
+ _tied_weights_keys = tied_weights_keys("lm_head.weight", "model.embed_tokens.weight")
+ _tp_plan = {"lm_head": "colwise_rep"}
+ _pp_plan = {"lm_head": (["hidden_states"], ["logits"])}
+
+ def __init__(self, config):
+ super().__init__(config)
+ self.model = Qwen3Model(config)
+ self.vocab_size = config.vocab_size
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
+
+ # Initialize weights and apply final processing
+ self.post_init()
+
+ @can_return_tuple
+ def forward(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ inputs_embeds: Optional[torch.FloatTensor] = None,
+ labels: Optional[torch.LongTensor] = None,
+ use_cache: Optional[bool] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ logits_to_keep: Union[int, torch.Tensor] = 0,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> CausalLMOutputWithPast:
+ r"""
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
+ config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
+
+ Example:
+
+ ```python
+ >>> from transformers import AutoTokenizer, Qwen3ForCausalLM
+
+ >>> model = Qwen3ForCausalLM.from_pretrained("Qwen/Qwen3-8B")
+ >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
+
+ >>> prompt = "Hey, are you conscious? Can you talk to me?"
+ >>> inputs = tokenizer(prompt, return_tensors="pt")
+
+ >>> # Generate
+ >>> generate_ids = model.generate(inputs.input_ids, max_length=30)
+ >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
+ "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you."
+ ```"""
+
+ outputs: BaseModelOutputWithPast = self.model(
+ input_ids=input_ids,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ inputs_embeds=inputs_embeds,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+
+ hidden_states = outputs.last_hidden_state
+ # Only compute necessary logits, and do not upcast them to float if we are not computing the loss
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
+
+ loss = None
+ if labels is not None:
+ loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, **kwargs)
+
+ return CausalLMOutputWithPast(
+ loss=loss,
+ logits=logits,
+ past_key_values=outputs.past_key_values,
+ hidden_states=outputs.hidden_states,
+ attentions=outputs.attentions,
+ )
+
+
+class Qwen3ForSequenceClassification(GenericForSequenceClassification, Qwen3PreTrainedModel):
+ pass
+
+
+class Qwen3ForTokenClassification(GenericForTokenClassification, Qwen3PreTrainedModel):
+ pass
+
+
+class Qwen3ForQuestionAnswering(GenericForQuestionAnswering, Qwen3PreTrainedModel):
+ base_model_prefix = "transformer" # For BC, where `transformer` was used instead of `model`
+
+
+__all__ = [
+ "Qwen3ForCausalLM",
+ "Qwen3ForQuestionAnswering",
+ "Qwen3PreTrainedModel",
+ "Qwen3Model",
+ "Qwen3ForSequenceClassification",
+ "Qwen3ForTokenClassification",
+]
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/modeling_qwen3_moe.py b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_qwen3_moe.py
new file mode 100644
index 000000000..fbfe91422
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/modeling_qwen3_moe.py
@@ -0,0 +1,548 @@
+from typing import Callable, Optional, Union
+
+import torch
+import torch.nn.functional as F
+from torch import nn
+
+from transformers.cache_utils import Cache, DynamicCache
+from transformers.generation import GenerationMixin
+from transformers.masking_utils import create_causal_mask
+from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
+from transformers.modeling_layers import GradientCheckpointingLayer
+from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
+from transformers.modeling_utils import PreTrainedModel
+from transformers.processing_utils import Unpack
+from transformers.utils import TransformersKwargs, can_return_tuple
+from transformers.utils.deprecation import deprecate_kwarg
+
+from .configuration_neo_chat import NEOMoELLMConfig
+from .modeling_qwen3 import (
+ Qwen3Attention,
+ Qwen3RMSNorm,
+ create_block_causal_mask,
+)
+from .transformers_compat import causal_mask_kwargs, model_input_compat, tied_weights_keys
+
+
+class Qwen3MoeMLP(nn.Module):
+ """Single expert FFN. Same structure as :class:`Qwen3MLP` but the
+ intermediate size is parameterised so it can be ``moe_intermediate_size``
+ (per-expert) for experts and ``intermediate_size`` for any dense fallback.
+ """
+
+ def __init__(self, config, intermediate_size: Optional[int] = None):
+ super().__init__()
+ from transformers.activations import ACT2FN
+
+ self.config = config
+ self.hidden_size = config.hidden_size
+ self.intermediate_size = (
+ intermediate_size if intermediate_size is not None else config.intermediate_size
+ )
+ self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
+ self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
+ self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
+ self.act_fn = ACT2FN[config.hidden_act]
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
+
+
+class Qwen3MoeSparseMoeBlock(nn.Module):
+ """Top-k softmax-routed MoE block matching HuggingFace's Qwen3-MoE layout.
+
+ Parameter names (``gate.weight``, ``experts.{i}.gate_proj/up_proj/down_proj``)
+ are kept identical so converted A3B checkpoints load directly via the
+ ``mlp.*`` / ``mlp_mot_gen.*`` keys. The block is parameterised explicitly
+ so the same class can serve both the understanding branch (``num_experts``
+ experts, top-k = ``num_experts_per_tok``, width ``moe_intermediate_size``)
+ and the image-generation branch (``gen_num_experts`` etc.).
+ """
+
+ def __init__(
+ self,
+ config: NEOMoELLMConfig,
+ num_experts: Optional[int] = None,
+ num_experts_per_tok: Optional[int] = None,
+ moe_intermediate_size: Optional[int] = None,
+ ):
+ super().__init__()
+ self.num_experts = int(num_experts) if num_experts is not None else int(config.num_experts)
+ self.top_k = int(
+ num_experts_per_tok if num_experts_per_tok is not None else config.num_experts_per_tok
+ )
+ self.norm_topk_prob = bool(getattr(config, "norm_topk_prob", True))
+ self.hidden_size = config.hidden_size
+
+ expert_intermediate_size = int(
+ moe_intermediate_size
+ if moe_intermediate_size is not None
+ else config.moe_intermediate_size
+ )
+
+ self.gate = nn.Linear(config.hidden_size, self.num_experts, bias=False)
+ self.experts = nn.ModuleList(
+ [
+ Qwen3MoeMLP(config, intermediate_size=expert_intermediate_size)
+ for _ in range(self.num_experts)
+ ]
+ )
+
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
+ orig_shape = hidden_states.shape
+ hidden_dim = orig_shape[-1]
+ flat = hidden_states.view(-1, hidden_dim)
+ n_tokens = flat.shape[0]
+
+ router_logits = self.gate(flat)
+ routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float32)
+ routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1)
+ if self.norm_topk_prob:
+ routing_weights = routing_weights / routing_weights.sum(dim=-1, keepdim=True)
+ routing_weights = routing_weights.to(flat.dtype)
+
+ output = torch.zeros(
+ (n_tokens, hidden_dim), dtype=flat.dtype, device=flat.device
+ )
+ # (num_experts, top_k, num_tokens)
+ expert_mask = F.one_hot(selected_experts, num_classes=self.num_experts).permute(2, 1, 0)
+
+ for expert_idx in range(self.num_experts):
+ idx, top_x = torch.where(expert_mask[expert_idx])
+ if top_x.numel() == 0:
+ continue
+ expert_layer = self.experts[expert_idx]
+ current_state = flat.index_select(0, top_x)
+ current_out = expert_layer(current_state) * routing_weights[top_x, idx, None]
+ output.index_add_(0, top_x, current_out.to(flat.dtype))
+
+ return output.view(*orig_shape)
+
+
+class Qwen3MoeDecoderLayer(GradientCheckpointingLayer):
+ """A Qwen3-MoE decoder block with the NEO-Unify two-branch structure.
+
+ Mirrors ``Qwen3DecoderLayer`` from :mod:`modeling_qwen3` but uses sparse
+ MoE blocks on *both* branches:
+
+ * ``self.mlp`` - understanding-path MoE
+ (``num_experts`` / ``num_experts_per_tok`` /
+ ``moe_intermediate_size``)
+ * ``self.mlp_mot_gen`` - image-generation-path MoE
+ (``gen_num_experts`` / ``gen_num_experts_per_tok`` /
+ ``gen_moe_intermediate_size``)
+
+ Layers listed in ``mlp_only_layers`` or those not aligned with
+ ``decoder_sparse_step`` fall back to a dense :class:`Qwen3MoeMLP` on the
+ understanding branch (matching upstream Qwen3-MoE), while the
+ generation branch still uses a sparse MoE.
+ """
+
+ def __init__(self, config: NEOMoELLMConfig, layer_idx: int):
+ super().__init__()
+ self.hidden_size = config.hidden_size
+ self.self_attn = Qwen3Attention(config=config, layer_idx=layer_idx)
+
+ mlp_only_layers = list(getattr(config, "mlp_only_layers", []) or [])
+ decoder_sparse_step = int(getattr(config, "decoder_sparse_step", 1) or 1)
+ is_sparse = (
+ int(config.num_experts) > 0
+ and layer_idx not in mlp_only_layers
+ and (layer_idx + 1) % decoder_sparse_step == 0
+ )
+
+ if is_sparse:
+ self.mlp = Qwen3MoeSparseMoeBlock(
+ config,
+ num_experts=config.num_experts,
+ num_experts_per_tok=config.num_experts_per_tok,
+ moe_intermediate_size=config.moe_intermediate_size,
+ )
+ else:
+ self.mlp = Qwen3MoeMLP(config, intermediate_size=config.intermediate_size)
+
+ # Image-generation branch: in the A3B checkpoint this is *also* a sparse
+ # MoE block (``gen_num_experts`` experts, typically smaller than the und
+ # branch's ``num_experts``). ``NEOMoELLMConfig`` defaults the gen-path
+ # knobs to their und-path counterparts so legacy single-pool configs
+ # keep working.
+ self.mlp_mot_gen = Qwen3MoeSparseMoeBlock(
+ config,
+ num_experts=getattr(config, "gen_num_experts", config.num_experts),
+ num_experts_per_tok=getattr(
+ config, "gen_num_experts_per_tok", config.num_experts_per_tok
+ ),
+ moe_intermediate_size=getattr(
+ config, "gen_moe_intermediate_size", config.moe_intermediate_size
+ ),
+ )
+
+ self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.input_layernorm_mot_gen = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.post_attention_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.post_attention_layernorm_mot_gen = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.attention_type = config.layer_types[layer_idx]
+
+ def forward_und(
+ self,
+ hidden_states: torch.Tensor,
+ image_gen_indicators: torch.Tensor,
+ exist_non_image_gen_tokens: bool,
+ exist_image_gen_tokens: bool,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ use_cache: Optional[bool] = False,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> torch.Tensor:
+ residual = hidden_states
+ hidden_states = self.input_layernorm(hidden_states)
+ hidden_states, _ = self.self_attn(
+ hidden_states=hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ hidden_states = residual + hidden_states
+
+ residual = hidden_states
+ hidden_states = self.post_attention_layernorm(hidden_states)
+ hidden_states = self.mlp(hidden_states)
+ hidden_states = residual + hidden_states
+ return hidden_states
+
+ def forward_gen(
+ self,
+ hidden_states: torch.Tensor,
+ image_gen_indicators: torch.Tensor,
+ exist_non_image_gen_tokens: bool,
+ exist_image_gen_tokens: bool,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ use_cache: Optional[bool] = False,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> torch.Tensor:
+ residual = hidden_states
+ hidden_states = self.input_layernorm_mot_gen(hidden_states)
+ hidden_states, _ = self.self_attn(
+ hidden_states=hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ hidden_states = residual + hidden_states
+
+ residual = hidden_states
+ hidden_states = self.post_attention_layernorm_mot_gen(hidden_states)
+ hidden_states = self.mlp_mot_gen(hidden_states)
+ hidden_states = residual + hidden_states
+ return hidden_states
+
+ @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ image_gen_indicators: torch.Tensor,
+ exist_non_image_gen_tokens: bool,
+ exist_image_gen_tokens: bool,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ use_cache: Optional[bool] = False,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> torch.Tensor:
+ if exist_non_image_gen_tokens and not exist_image_gen_tokens:
+ return self.forward_und(
+ hidden_states, image_gen_indicators, exist_non_image_gen_tokens,
+ exist_image_gen_tokens, indexes, attention_mask, position_ids,
+ past_key_values, use_cache, cache_position, **kwargs,
+ )
+ if not exist_non_image_gen_tokens and exist_image_gen_tokens:
+ return self.forward_gen(
+ hidden_states, image_gen_indicators, exist_non_image_gen_tokens,
+ exist_image_gen_tokens, indexes, attention_mask, position_ids,
+ past_key_values, use_cache, cache_position, **kwargs,
+ )
+
+ # Mixed und/gen path — see the NOTE in Qwen3Attention.forward (modeling_qwen3.py).
+ raise NotImplementedError(
+ "Mixed und/gen decoder-layer forward is not yet validated (issue #207). "
+ "Split the sequence at token-type boundaries and use forward_und / forward_gen."
+ )
+
+ # Mixed batch: dispatch tokens per branch then merge back. Matches the
+ # dense ``Qwen3DecoderLayer.forward`` mixed-path implementation.
+ residual = hidden_states
+
+ _hidden_states = hidden_states.new_zeros(hidden_states.shape)
+ if exist_non_image_gen_tokens:
+ _hidden_states[~image_gen_indicators] = self.input_layernorm(
+ hidden_states[~image_gen_indicators]
+ )
+ if exist_image_gen_tokens:
+ _hidden_states[image_gen_indicators] = self.input_layernorm_mot_gen(
+ hidden_states[image_gen_indicators]
+ )
+ hidden_states = _hidden_states
+
+ hidden_states, _ = self.self_attn(
+ hidden_states=hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ hidden_states = residual + hidden_states
+
+ residual = hidden_states
+
+ _hidden_states = hidden_states.new_zeros(hidden_states.shape)
+ if exist_non_image_gen_tokens:
+ und_hidden = self.post_attention_layernorm(
+ hidden_states[~image_gen_indicators]
+ )
+ # MoE expects a 3D input (batch, seq, hidden); promote then squeeze.
+ if und_hidden.dim() == 2:
+ und_hidden = und_hidden.unsqueeze(0)
+ _hidden_states[~image_gen_indicators] = self.mlp(und_hidden).squeeze(0)
+ else:
+ _hidden_states[~image_gen_indicators] = self.mlp(und_hidden)
+ if exist_image_gen_tokens:
+ _hidden_states[image_gen_indicators] = self.mlp_mot_gen(
+ self.post_attention_layernorm_mot_gen(hidden_states[image_gen_indicators])
+ )
+
+ hidden_states = _hidden_states
+ hidden_states = residual + hidden_states
+ return hidden_states
+
+
+class Qwen3MoePreTrainedModel(PreTrainedModel):
+ config: NEOMoELLMConfig
+ base_model_prefix = "model"
+ supports_gradient_checkpointing = True
+ _no_split_modules = ["Qwen3MoeDecoderLayer"]
+ _skip_keys_device_placement = ["past_key_values"]
+ _supports_flash_attn = True
+ _supports_sdpa = True
+ _supports_flex_attn = True
+
+ _can_compile_fullgraph = False # MoE routing has data-dependent control flow.
+ _supports_attention_backend = True
+ _can_record_outputs = {
+ "hidden_states": Qwen3MoeDecoderLayer,
+ "attentions": Qwen3Attention,
+ }
+
+
+class Qwen3MoeModel(Qwen3MoePreTrainedModel):
+ def __init__(self, config: NEOMoELLMConfig):
+ super().__init__(config)
+ self.padding_idx = config.pad_token_id
+ self.vocab_size = config.vocab_size
+
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
+ self.layers = nn.ModuleList(
+ [Qwen3MoeDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
+ )
+ self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.norm_mot_gen = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+
+ self.gradient_checkpointing = False
+ self.has_sliding_layers = "sliding_attention" in self.config.layer_types
+ self.current_index = -1
+
+ self.post_init()
+
+ @model_input_compat
+ def forward(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ image_gen_indicators: Optional[torch.Tensor] = None,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ inputs_embeds: Optional[torch.FloatTensor] = None,
+ use_cache: Optional[bool] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> BaseModelOutputWithPast:
+ if image_gen_indicators is None:
+ exist_non_image_gen_tokens = True
+ exist_image_gen_tokens = False
+ else:
+ # Convert the CUDA reductions once before the decoder loop. If the
+ # scalar tensors reach every layer, each Python branch can force a
+ # host-device synchronization and collapse async weight prefetch.
+ exist_non_image_gen_tokens = bool((~image_gen_indicators).any().item())
+ exist_image_gen_tokens = bool(image_gen_indicators.any().item())
+
+ if (input_ids is None) ^ (inputs_embeds is not None):
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
+
+ if inputs_embeds is None:
+ inputs_embeds = self.embed_tokens(input_ids)
+
+ if use_cache and past_key_values is None:
+ past_key_values = DynamicCache(config=self.config)
+
+ if cache_position is None:
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
+ cache_position = torch.arange(
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
+ )
+
+ if position_ids is None:
+ position_ids = cache_position.unsqueeze(0)
+
+ if not isinstance(causal_mask_mapping := attention_mask, dict):
+ if input_ids is not None:
+ mask_kwargs = causal_mask_kwargs(
+ create_causal_mask,
+ config=self.config,
+ inputs_embeds=inputs_embeds,
+ attention_mask=attention_mask,
+ cache_position=cache_position,
+ past_key_values=past_key_values,
+ position_ids=position_ids,
+ )
+ causal_mask_mapping = {
+ "full_attention": create_causal_mask(**mask_kwargs),
+ }
+ self.current_index += 1
+ indexes = torch.LongTensor([[self.current_index], [0], [0]]).to(input_ids.device)
+ else:
+ causal_mask_mapping = {
+ "full_attention": create_block_causal_mask(indexes[0]),
+ }
+ self.current_index = indexes[0].max()
+ else:
+ self.current_index = indexes[0].max()
+
+ hidden_states = inputs_embeds
+
+ for decoder_layer in self.layers[: self.config.num_hidden_layers]:
+ hidden_states = decoder_layer(
+ hidden_states,
+ image_gen_indicators=image_gen_indicators,
+ exist_non_image_gen_tokens=exist_non_image_gen_tokens,
+ exist_image_gen_tokens=exist_image_gen_tokens,
+ indexes=indexes,
+ attention_mask=causal_mask_mapping[decoder_layer.attention_type],
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+
+ if not exist_image_gen_tokens:
+ hidden_states = self.norm(hidden_states)
+ elif not exist_non_image_gen_tokens:
+ hidden_states = self.norm_mot_gen(hidden_states)
+ else:
+ _hidden_states = hidden_states.new_zeros(hidden_states.shape)
+ _hidden_states[~image_gen_indicators] = self.norm(hidden_states[~image_gen_indicators])
+ _hidden_states[image_gen_indicators] = self.norm_mot_gen(hidden_states[image_gen_indicators])
+ hidden_states = _hidden_states
+
+ return BaseModelOutputWithPast(
+ last_hidden_state=hidden_states,
+ past_key_values=past_key_values if use_cache else None,
+ )
+
+
+class Qwen3MoeForCausalLM(Qwen3MoePreTrainedModel, GenerationMixin):
+ _tied_weights_keys = tied_weights_keys("lm_head.weight", "model.embed_tokens.weight")
+ _tp_plan = {"lm_head": "colwise_rep"}
+ _pp_plan = {"lm_head": (["hidden_states"], ["logits"])}
+
+ def __init__(self, config: NEOMoELLMConfig):
+ super().__init__(config)
+ self.model = Qwen3MoeModel(config)
+ self.vocab_size = config.vocab_size
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
+ self.post_init()
+
+ @can_return_tuple
+ def forward(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ indexes: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ inputs_embeds: Optional[torch.FloatTensor] = None,
+ labels: Optional[torch.LongTensor] = None,
+ use_cache: Optional[bool] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ logits_to_keep: Union[int, torch.Tensor] = 0,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> CausalLMOutputWithPast:
+ outputs: BaseModelOutputWithPast = self.model(
+ input_ids=input_ids,
+ indexes=indexes,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ inputs_embeds=inputs_embeds,
+ use_cache=use_cache,
+ cache_position=cache_position,
+ **kwargs,
+ )
+
+ hidden_states = outputs.last_hidden_state
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
+
+ loss = None
+ if labels is not None:
+ loss = self.loss_function(
+ logits=logits, labels=labels, vocab_size=self.config.vocab_size, **kwargs
+ )
+
+ return CausalLMOutputWithPast(
+ loss=loss,
+ logits=logits,
+ past_key_values=outputs.past_key_values,
+ hidden_states=outputs.hidden_states,
+ attentions=outputs.attentions,
+ )
+
+
+__all__ = [
+ "Qwen3MoeForCausalLM",
+ "Qwen3MoeModel",
+ "Qwen3MoePreTrainedModel",
+ "Qwen3MoeDecoderLayer",
+ "Qwen3MoeSparseMoeBlock",
+ "Qwen3MoeMLP",
+]
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/transformers_compat.py b/unirl/models/sensenova_u1/vendor/neo_unify/transformers_compat.py
new file mode 100644
index 000000000..2a531cd37
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/transformers_compat.py
@@ -0,0 +1,67 @@
+"""Small compatibility seams for the supported Transformers 4/5 window."""
+
+from __future__ import annotations
+
+import inspect
+from functools import lru_cache
+from typing import Any, Callable
+
+import torch
+from packaging.version import Version
+
+try:
+ from transformers.utils.generic import merge_with_config_defaults
+ from transformers.utils.output_capturing import capture_outputs
+
+ def model_input_compat(func: Callable[..., Any]) -> Callable[..., Any]:
+ return merge_with_config_defaults(capture_outputs(func))
+
+except ImportError: # Transformers 4.x
+ from transformers.utils.generic import check_model_inputs as model_input_compat
+
+
+@lru_cache(maxsize=None)
+def _parameter_names(callable_: Callable[..., Any]) -> frozenset[str]:
+ return frozenset(inspect.signature(callable_).parameters)
+
+
+def causal_mask_kwargs(
+ mask_factory: Callable[..., Any],
+ *,
+ config: Any,
+ inputs_embeds: torch.Tensor,
+ attention_mask: torch.Tensor | None,
+ cache_position: torch.Tensor,
+ past_key_values: Any,
+ position_ids: torch.Tensor,
+) -> dict[str, Any]:
+ """Build arguments accepted by the installed ``create_causal_mask``.
+
+ Transformers 4.57 uses ``input_embeds`` and ``cache_position`` while newer
+ 5.x releases use ``inputs_embeds`` and derive the cache position internally.
+ """
+ parameters = _parameter_names(mask_factory)
+ embedding_parameter = "inputs_embeds" if "inputs_embeds" in parameters else "input_embeds"
+ candidates = {
+ "config": config,
+ embedding_parameter: inputs_embeds,
+ "attention_mask": attention_mask,
+ "cache_position": cache_position,
+ "past_key_values": past_key_values,
+ "position_ids": position_ids,
+ }
+ return {name: value for name, value in candidates.items() if name in parameters}
+
+
+def pretrained_dtype_kwargs(dtype: torch.dtype) -> dict[str, torch.dtype]:
+ """Use the public dtype keyword supported throughout Transformers 4.57+."""
+ return {"dtype": dtype}
+
+
+def tied_weights_keys(output_key: str, input_key: str) -> list[str] | dict[str, str]:
+ """Return the `_tied_weights_keys` shape expected by Transformers 4 or 5."""
+ import transformers
+
+ if Version(transformers.__version__).major >= 5:
+ return {output_key: input_key}
+ return [output_key]
diff --git a/unirl/models/sensenova_u1/vendor/neo_unify/utils.py b/unirl/models/sensenova_u1/vendor/neo_unify/utils.py
new file mode 100644
index 000000000..1dccc55fa
--- /dev/null
+++ b/unirl/models/sensenova_u1/vendor/neo_unify/utils.py
@@ -0,0 +1,163 @@
+from __future__ import annotations
+
+import math
+
+import torch
+import torchvision.transforms as T
+from PIL import Image
+
+SYSTEM_MESSAGE_FOR_GEN = (
+ "You are an image generation and editing assistant that accurately understands and executes "
+ "user intent.\n\nYou support two modes:\n\n1. Think Mode:\nIf the task requires reasoning, you "
+ "MUST start with a block. Put all reasoning inside the block using plain text. "
+ "DO NOT include any image tags. Keep it reasonable and directly useful for producing the final "
+ "image.\n\n2. Non-Think Mode:\nIf no reasoning is needed, directly produce the final image.\n\n"
+ "Task Types:\n\nA. Text-to-Image Generation:\n"
+ "- Generate a high-quality image based on the user's description.\n"
+ "- Ensure visual clarity, semantic consistency, and completeness.\n"
+ "- DO NOT introduce elements that contradict or override the user's intent.\n\n"
+ "B. Image Editing:\n"
+ "- Use the provided image(s) as input or reference for modification or transformation.\n"
+ "- The result can be an edited image or a new image based on the reference(s).\n"
+ "- Preserve all unspecified attributes unless explicitly changed.\n\n"
+ "General Rules:\n"
+ "- For any visible text in the image, follow the language specified for the rendered text in "
+ "the user's description, not the language of the prompt. If no language is specified, use the "
+ "user's input language."
+)
+
+IMAGENET_MEAN = (0.485, 0.456, 0.406)
+IMAGENET_STD = (0.229, 0.224, 0.225)
+
+
+def round_by_factor(number: float, factor: int) -> int:
+ """Returns the closest integer to `number` that is divisible by `factor`."""
+ return round(number / factor) * factor
+
+
+def ceil_by_factor(number: float, factor: int) -> int:
+ """Returns the smallest integer >= `number` that is divisible by `factor`."""
+ return math.ceil(number / factor) * factor
+
+
+def floor_by_factor(number: float, factor: int) -> int:
+ """Returns the largest integer <= `number` that is divisible by `factor`."""
+ return math.floor(number / factor) * factor
+
+
+def smart_resize(
+ height: int,
+ width: int,
+ factor: int = 32,
+ min_pixels: int = 65536,
+ max_pixels: int = 4194304,
+) -> tuple[int, int]:
+ """Rescale so that H/W are divisible by `factor` and total pixels ∈ [min, max].
+
+ Copied from https://github.com/QwenLM/Qwen2.5-VL/blob/main/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L60
+ """
+ if max(height, width) / min(height, width) > 200:
+ raise ValueError(
+ f"absolute aspect ratio must be smaller than 200, got {max(height, width) / min(height, width)}"
+ )
+ h_bar = max(factor, round_by_factor(height, factor))
+ w_bar = max(factor, round_by_factor(width, factor))
+ if h_bar * w_bar > max_pixels:
+ beta = math.sqrt((height * width) / max_pixels)
+ h_bar = max(factor, floor_by_factor(height / beta, factor))
+ w_bar = max(factor, floor_by_factor(width / beta, factor))
+ elif h_bar * w_bar < min_pixels:
+ beta = math.sqrt(min_pixels / (height * width))
+ h_bar = ceil_by_factor(height * beta, factor)
+ w_bar = ceil_by_factor(width * beta, factor)
+ return h_bar, w_bar
+
+
+def dynamic_preprocess_native_resolution(
+ image: Image.Image,
+ size_factor: int = 32,
+ min_pixels: int = 65536,
+ max_pixels: int = 4194304,
+ **_kwargs,
+) -> Image.Image:
+ width, height = image.size
+ resized_height, resized_width = smart_resize(
+ height,
+ width,
+ factor=size_factor,
+ min_pixels=min_pixels,
+ max_pixels=max_pixels,
+ )
+ return image.resize((resized_width, resized_height))
+
+
+def preprocess_pixel_values(pixel_values: torch.Tensor, patch_size: int = 16):
+ c, h, w = pixel_values.shape
+ grid_h = h // patch_size
+ grid_w = w // patch_size
+
+ flatten_pixel_values = (
+ pixel_values.view(c, grid_h, patch_size, grid_w, patch_size)
+ .permute(1, 3, 0, 2, 4) # [grid_h, grid_w, c, patch_size, patch_size]
+ .reshape(grid_h * grid_w, c * patch_size ** 2)
+ )
+
+ grid_hw = torch.tensor([[grid_h, grid_w]], device=pixel_values.device)
+ return flatten_pixel_values, grid_hw
+
+
+def get_contrasting_background(image: Image.Image):
+ """Return a background color for RGBA->RGB conversion, or ``None`` to use default.
+
+ The original Neo_Unify implementation computed a contrasting background
+ from the alpha channel. For this open-source release we fall back to a
+ plain white background; callers that need the smarter behavior can override
+ this function.
+ """
+ del image
+ return (255, 255, 255)
+
+
+def load_image_native(
+ image,
+ patch_size: int = 16,
+ downsample_ratio: float = 0.5,
+ min_pixels: int = 65536,
+ max_pixels: int = 4194304,
+ upscale: bool = False,
+):
+ """Load and preprocess an image: RGB convert, smart-resize, normalize, patchify."""
+ if not isinstance(image, Image.Image):
+ image = Image.open(image)
+ if image.mode == "RGBA":
+ bg_color = get_contrasting_background(image)
+ if bg_color:
+ background = Image.new("RGB", image.size, bg_color)
+ background.paste(image, mask=image.split()[3])
+ image = background.convert("RGB")
+ else:
+ image = image.convert("RGB")
+ else:
+ image = image.convert("RGB")
+
+ if upscale:
+ image = image.resize((image.width * 2, image.height * 2), Image.BILINEAR)
+
+ transform = T.Compose(
+ [
+ T.Lambda(lambda img: img.convert("RGB") if img.mode != "RGB" else img),
+ T.ToTensor(),
+ T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
+ ]
+ )
+
+ new_image = dynamic_preprocess_native_resolution(
+ image,
+ size_factor=int(patch_size // downsample_ratio),
+ min_pixels=min_pixels,
+ max_pixels=max_pixels,
+ )
+ pixel_values, grid_hw = preprocess_pixel_values(
+ transform(new_image).to(torch.float32), patch_size=patch_size
+ )
+ return pixel_values, grid_hw
diff --git a/unirl/rollout/engine/vllm_omni/adapters/__init__.py b/unirl/rollout/engine/vllm_omni/adapters/__init__.py
index 655391f58..4ba46cac0 100644
--- a/unirl/rollout/engine/vllm_omni/adapters/__init__.py
+++ b/unirl/rollout/engine/vllm_omni/adapters/__init__.py
@@ -1,4 +1,4 @@
-"""Adapter registry — importing this package registers all 12 modalities."""
+"""Adapter registry — importing this package registers all rollout modalities."""
from unirl.rollout.engine.vllm_omni.adapters.bagel import (
BagelAdapter,
@@ -44,6 +44,11 @@
QwenImageT2iAdapter,
)
from unirl.rollout.engine.vllm_omni.adapters.sd3 import Sd3InputAdapter, Sd3OutputAdapter, Sd3T2iAdapter
+from unirl.rollout.engine.vllm_omni.adapters.sensenova_u1 import (
+ SenseNovaU1InputAdapter,
+ SenseNovaU1OutputAdapter,
+ SenseNovaU1T2IAdapter,
+)
__all__ = [
"DitInputAdapter",
@@ -75,6 +80,9 @@
"QwenImageInputAdapter",
"QwenImageOutputAdapter",
"QwenImageT2iAdapter",
+ "SenseNovaU1InputAdapter",
+ "SenseNovaU1OutputAdapter",
+ "SenseNovaU1T2IAdapter",
"Sd3InputAdapter",
"Sd3OutputAdapter",
"Sd3T2iAdapter",
diff --git a/unirl/rollout/engine/vllm_omni/adapters/sensenova_u1.py b/unirl/rollout/engine/vllm_omni/adapters/sensenova_u1.py
new file mode 100644
index 000000000..9d6604033
--- /dev/null
+++ b/unirl/rollout/engine/vllm_omni/adapters/sensenova_u1.py
@@ -0,0 +1,175 @@
+"""SenseNova-U1.5 family: vLLM-Omni T2I request and response adapters."""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List
+
+import torch
+
+from unirl.models.sensenova_u1.conditions import SenseNovaU1Conditions
+from unirl.models.sensenova_u1.diffusion import SenseNovaU1DiffusionParams
+from unirl.rollout.engine.vllm_omni.adapters.base import ModelAdapter, register_adapter
+from unirl.rollout.engine.vllm_omni.adapters.dit import (
+ DitInputAdapter,
+ DitOutputAdapter,
+ _grouped_texts_from_sample,
+)
+from unirl.rollout.engine.vllm_omni.backends import GenerateCall, OmniRawResult, StageSampling
+from unirl.rollout.engine.vllm_omni.utils import collect_dit_outputs
+from unirl.sde.runtime import FlowMatchSchedulePolicy
+from unirl.types.sample import Sample
+
+
+class SenseNovaU1InputAdapter(DitInputAdapter):
+ """Build one same-prompt batched request per GRPO group."""
+
+ def __init__(self, modality: str, *, model_config: Any) -> None:
+ super().__init__(modality)
+ self.model_config = model_config
+
+ def build_prompts(self, sample: Sample) -> List[Any]:
+ grouped_texts, _ = _grouped_texts_from_sample(
+ sample,
+ caller=f"{self.modality}.build_prompts",
+ )
+ return [{"prompt": text} for text in grouped_texts]
+
+ def build_sampling(self, sample: Sample):
+ _, samples_per_prompt = _grouped_texts_from_sample(
+ sample,
+ caller=f"{self.modality}.build_sampling",
+ )
+ sampling = super().build_sampling(sample)
+ params = sample.frontier_gen_part(SenseNovaU1DiffusionParams).sampling_params
+ kwargs = sampling[0].kwargs
+ kwargs["num_outputs_per_prompt"] = samples_per_prompt
+
+ extra_args = kwargs.setdefault("extra_args", {})
+ extra_args.update(
+ {
+ "batch_size": samples_per_prompt,
+ "cfg_scale": float(params.guidance_scale),
+ "cfg_norm": str(params.cfg_norm),
+ "cfg_interval": [float(v) for v in params.cfg_interval],
+ "timestep_shift": float(self.model_config.timestep_shift),
+ "t_eps": float(params.t_eps),
+ "think": False,
+ "trajectory_precision": str(params.trajectory_precision),
+ }
+ )
+ if params.sigmas is not None:
+ # OmniDiffusionSamplingParams.sigmas follows Diffusers' T-entry
+ # convention; replay needs UniRL's complete T+1 schedule.
+ extra_args["unirl_sigmas"] = params.sigmas.detach().to("cpu", dtype=torch.float32).tolist()
+ return sampling
+
+ def build(self, sample: Sample) -> List[GenerateCall]:
+ """Split distinct prompts because upstream SenseNova batches one prompt only."""
+ prompts = self.build_prompts(sample)
+ sampling = self.build_sampling(sample)[0]
+ samples_per_prompt = int(sampling.kwargs["num_outputs_per_prompt"])
+ calls: List[GenerateCall] = []
+ for group_index, prompt in enumerate(prompts):
+ start = group_index * samples_per_prompt
+ end = start + samples_per_prompt
+ kwargs = dict(sampling.kwargs)
+ extra_args = dict(kwargs.get("extra_args") or {})
+ if "initial_noise_batch" in extra_args:
+ extra_args["initial_noise_batch"] = extra_args["initial_noise_batch"][start:end]
+ if "init_noise_group_ids" in extra_args:
+ extra_args["init_noise_group_ids"] = list(extra_args["init_noise_group_ids"][start:end])
+ extra_args["sde_seed"] = (int(kwargs.get("seed", 0)) + 1_000_003 * group_index) % (2**31)
+ kwargs["extra_args"] = extra_args
+ calls.append(
+ GenerateCall(
+ prompts=[prompt],
+ sampling=[StageSampling(kind=sampling.kind, kwargs=kwargs)],
+ )
+ )
+ return calls
+
+
+class SenseNovaU1OutputAdapter(DitOutputAdapter):
+ """Rebuild replay-ready prefix-cache conditions from worker captures."""
+
+ _CAPTURE_KEY = "sensenova_u1_capture"
+
+ def build_conditions(self, sample: Sample, per_request: List[List[OmniRawResult]]) -> Dict[str, Any]:
+ diff_outputs, _, _ = collect_dit_outputs(
+ per_request,
+ final_output_type=self.final_output_type,
+ stage_id=self.stage_id,
+ modality=self.modality,
+ )
+ captures = [(getattr(output, "custom_output", None) or {}).get(self._CAPTURE_KEY) for output in diff_outputs]
+ if any(capture is None for capture in captures):
+ raise RuntimeError(
+ "build_response: SenseNova rollout returned no "
+ f"{self._CAPTURE_KEY!r} on DiffusionOutput.custom_output. "
+ "Check that RLSenseNovaU1Pipeline was installed by the stage YAML."
+ )
+
+ fields = {
+ "prompts": [],
+ "condition_caches": [],
+ "uncondition_caches": [],
+ "condition_image_indexes": [],
+ "uncondition_image_indexes": [],
+ "image_shapes": [],
+ }
+ for capture in captures:
+ for name in fields:
+ fields[name].extend(capture[name])
+
+ conditions = SenseNovaU1Conditions(**fields)
+ conditions.validate()
+ expected = len(sample.frontier_gen_part(SenseNovaU1DiffusionParams).sample_ids)
+ if conditions.batch_size != expected:
+ raise RuntimeError(
+ "build_response: SenseNova condition batch "
+ f"{conditions.batch_size} != diffusion sample count {expected}."
+ )
+ return conditions.to_dict()
+
+
+@register_adapter("sensenova_u1_t2i")
+class SenseNovaU1T2IAdapter(ModelAdapter):
+ """SenseNova-U1.5 text-to-image rollout on one vLLM-Omni diffusion stage."""
+
+ stage_yaml = "sensenova_u1_t2i_rl.yaml"
+ omni_mode = "text-to-image"
+ needs_driver_tokenizer = False
+
+ def __init__(self, config: Any, model_config: Any, *, strategy: Any = None, tokenize_fn: Any = None) -> None:
+ super().__init__(config, model_config, strategy=strategy, tokenize_fn=tokenize_fn)
+ self.input_adapter = SenseNovaU1InputAdapter(self.modality, model_config=model_config)
+ self.output_adapter = SenseNovaU1OutputAdapter(self.modality)
+
+ def schedule_policy(self) -> FlowMatchSchedulePolicy:
+ return FlowMatchSchedulePolicy.static_only(float(self.model_config.timestep_shift))
+
+ def validate(self) -> None:
+ if self.model_config is None or not hasattr(self.model_config, "timestep_shift"):
+ raise ValueError(
+ f"SenseNovaU1T2IAdapter requires model_config.timestep_shift; got {type(self.model_config).__name__}."
+ )
+
+ def validate_request(self, sample: Sample) -> None:
+ if sample.has_image_input():
+ raise ValueError(
+ f"modality={self.modality!r} rejects image-bearing requests; "
+ "the initial integration supports text-to-image only."
+ )
+
+ def build_inputs(self, sample: Sample) -> List[GenerateCall]:
+ return self.input_adapter.build(sample)
+
+ def build_response(self, sample: Sample, per_request: List[List[OmniRawResult]]) -> Sample:
+ return self.output_adapter.build(sample, per_request)
+
+
+__all__ = [
+ "SenseNovaU1InputAdapter",
+ "SenseNovaU1OutputAdapter",
+ "SenseNovaU1T2IAdapter",
+]
diff --git a/unirl/rollout/engine/vllm_omni/patches/README.md b/unirl/rollout/engine/vllm_omni/patches/README.md
index 76133ccc1..5201a16ac 100644
--- a/unirl/rollout/engine/vllm_omni/patches/README.md
+++ b/unirl/rollout/engine/vllm_omni/patches/README.md
@@ -44,12 +44,12 @@ All in `runtime.py` unless noted.
| `patch_fp32_skip` | Punica kernels hard-assert dtype; HI3's MoE router gate is fp32, so non-fp16/bf16 layers must be skipped for LoRA wrapping | vllm's `from_layer` skips unsupported dtypes itself |
| `patch_lora_request_passthrough` | `Omni.generate` never forwards `lora_request`, needed by the HI3 AR-prelude stage. Verified still absent at upstream main (~v0.22.0rc1); `AsyncOmniEngine.add_request` has accepted the kwarg all along, so a small upstream PR forwarding it would retire this | vllm-omni upstreams the kwarg (then the `ar_lora_passthrough` gate drops too) |
| `patch_per_request_ar_seed` | One `SamplingParams` is shared across requests, so a GRPO group's N requests collapse to identical tokens | vllm-omni stops sharing one `SamplingParams` |
-| `patch_qwen3_omni_thinker_lora` | Backport of vllm-omni #3915: expose the Thinker LoRA interface, select `thinker_config` during model init, accept the current M-RoPE signature | pin vllm-omni ≥ 0.22 |
+| `patch_qwen3_omni_thinker_lora` | Compatibility bundle around vllm-omni #3915 plus UniRL's audio fixes. On v0.22, native Thinker LoRA gates both the class backport and the legacy audio-video M-RoPE shift, preventing a double correction; only audio truncation remains | upstream audio truncation is verified and this bundle is deleted |
| `patch_sigmas_passthrough` | HI3's DiT `scheduler.set_timesteps` never receives `sampling_params.sigmas` | upstream forwards `sigmas` itself |
-| `patch_hi3_flow_alignment` | Port of upstream `eed27812` to the pinned v0.20.0 KV-cache API | pin ≥ v0.21 — upstream removed the v0.20.0 `ImageKVCacheManager` API, so the patch self-skips there and is dead code once the pin moves |
+| `patch_hi3_flow_alignment` | Legacy port of upstream `eed27812` for the old v0.20 KV-cache API; it self-skips on the current v0.22 pin | remove the compatibility function after the v0.22 rollout regression pass |
| `install_fate_sharing` | `PR_SET_PDEATHSIG` is bound by Linux to the **specific creating thread**, so arming it for children of short-lived init threads kills healthy workers; and a worker inside a CUDA/NCCL call never observes vLLM's `death_pipe` EOF | vllm's own child-reaping is thread-safe |
| `compat_tokenizer` (module) | HI3's `__init__` looks up `` and computes `ratio_36 + 1`; the Base checkpoint ships ratio tokens 0-32 only → `TypeError: … 'NoneType' and 'int'`. Both the slow **and** fast tokenizer classes must be patched, not the shared base. The module import *is* the install trigger (it is the `HI3ARWorkerExtension` qualname target). Upstream ≥ v0.20.0 raises a clean `ValueError` instead — a better error, but the Base ckpt still needs this 0-fallback to work | Base-ckpt support is dropped (Instruct ships the tokens) |
-| `compat_hi3_lora` (module) | vllm 0.20 expects a flat `list[tuple[str, str, int, str]]` from `get_expert_mapping`; HI3 returns a 2-tuple, so `process_packed_modules_mapping` trips `ValueError: too many values to unpack` at boot under `enable_lora` | vllm handles the 2-tuple / HI3 returns the flat list |
+| `compat_hi3_lora` (module) | Some vLLM LoRA paths expect a flat expert mapping while HI3 returns a 2-tuple, so `process_packed_modules_mapping` can trip `ValueError: too many values to unpack` at boot under `enable_lora` | vllm handles the 2-tuple / HI3 returns the flat list |
| `compat_qwen3_omni` (module) | Compatibility helpers for Qwen3-Omni on the pinned runtime | the pin carries them |
| `patch_moe_workspace_pool` (`compat_moe_workspace`) | vLLM's reusable MoE `WorkspaceManager` scratch buffer is allocated outside the `weights`/`kv_cache` CuMem pools, so worker sleep leaves it resident through the colocated train phase. Gives each DBO ubatch an independently replaceable CuMem pool while tagging all allocations `moe_workspace`; worker sleep then discards the scratch pages with the other non-weight pools and UniRL's untagged wake remaps them. Self-skips when the allocator is absent (sleep mode off → `CuMemAllocator.instance is None`) | vLLM allocates the MoE workspace inside a sleep-managed pool itself |
@@ -80,5 +80,6 @@ known-invalid adapter.
submodules, loaded lazily — `import unirl.rollout.engine.vllm_omni.patches` must
not pull vllm.
- **Every patch needs a DELETE-WHEN row.** Without one it is permanent by default.
-- **`patch_hi3_flow_alignment` self-skips on newer pins** — it is dead code, not a
- live patch, once the pin moves past v0.20.0.
+- **`patch_hi3_flow_alignment` self-skips on the v0.22 pin** — it remains only
+ as quarantined dead compatibility code until the rollout regression pass
+ confirms it can be deleted.
diff --git a/unirl/rollout/engine/vllm_omni/patches/compat_hi3_lora.py b/unirl/rollout/engine/vllm_omni/patches/compat_hi3_lora.py
index e735a6335..4c259597f 100644
--- a/unirl/rollout/engine/vllm_omni/patches/compat_hi3_lora.py
+++ b/unirl/rollout/engine/vllm_omni/patches/compat_hi3_lora.py
@@ -6,7 +6,7 @@
def install() -> None:
- """Patch ``get_moe_expert_mapping`` everywhere it's used, unwrapping HI3's 2-tuple to vllm 0.20's flat list."""
+ """Patch ``get_moe_expert_mapping`` everywhere it is used, unwrapping HI3's 2-tuple result."""
global _INSTALLED
if _INSTALLED:
return
diff --git a/unirl/rollout/engine/vllm_omni/patches/runtime.py b/unirl/rollout/engine/vllm_omni/patches/runtime.py
index aaa0ae325..0bfef5ab1 100644
--- a/unirl/rollout/engine/vllm_omni/patches/runtime.py
+++ b/unirl/rollout/engine/vllm_omni/patches/runtime.py
@@ -136,7 +136,7 @@ def start(self):
def patch_qwen3_omni_thinker_lora() -> None:
- """Backport Qwen3-Omni Thinker LoRA support to vLLM-Omni 0.20."""
+ """Install only the Qwen3-Omni compatibility pieces absent upstream."""
module_name = "vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker"
if importlib.util.find_spec(module_name) is None:
return
@@ -152,8 +152,12 @@ def patch_qwen3_omni_thinker_lora() -> None:
patch_qwen3_omni_thinker_class,
)
+ has_native_thinker_support = bool(getattr(Qwen3OmniMoeThinkerForConditionalGeneration, "supports_lora", False))
patch_qwen3_omni_thinker_class(Qwen3OmniMoeThinkerForConditionalGeneration)
- patch_qwen3_omni_audio_video_mrope(Qwen3OmniMoeThinkerForConditionalGeneration)
+ if not has_native_thinker_support:
+ # The 0.22 Thinker already computes interleaved audio/video positions.
+ # Applying the old token-shift correction again moves delimiters twice.
+ patch_qwen3_omni_audio_video_mrope(Qwen3OmniMoeThinkerForConditionalGeneration)
patch_qwen3_omni_audio_truncation(Qwen3OmniMoeThinkerMultiModalProcessor)
@@ -580,9 +584,11 @@ def patch_hi3_flow_alignment() -> None:
except (ImportError, AttributeError):
return
- _ImageKVCacheManager = _trans.ImageKVCacheManager
- _DecoderLayer = _trans.HunyuanImage3DecoderLayer
+ _ImageKVCacheManager = getattr(_trans, "ImageKVCacheManager", None)
+ _DecoderLayer = getattr(_trans, "HunyuanImage3DecoderLayer", None)
+ if _ImageKVCacheManager is None or _DecoderLayer is None:
+ return
if not hasattr(_ImageKVCacheManager, "_save_image_kv_caches"):
return
diff --git a/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/__init__.py b/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/__init__.py
new file mode 100644
index 000000000..bb5373881
--- /dev/null
+++ b/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/__init__.py
@@ -0,0 +1,16 @@
+"""SenseNova-U1.5 pipeline extensions for vLLM-Omni rollout."""
+
+from typing import Any
+
+
+def __getattr__(name: str) -> Any:
+ if name == "RLSenseNovaU1Pipeline":
+ from unirl.rollout.engine.vllm_omni.pipelines.sensenova_u1.pipeline import (
+ RLSenseNovaU1Pipeline,
+ )
+
+ return RLSenseNovaU1Pipeline
+ raise AttributeError(name)
+
+
+__all__ = ["RLSenseNovaU1Pipeline"]
diff --git a/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/pipeline.py b/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/pipeline.py
new file mode 100644
index 000000000..2e44b9899
--- /dev/null
+++ b/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/pipeline.py
@@ -0,0 +1,265 @@
+"""RL-aware SenseNova-U1.5 vLLM-Omni pipeline."""
+
+from __future__ import annotations
+
+import copy
+from types import SimpleNamespace
+from typing import Any, Dict, List, Optional
+
+import torch
+from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
+from vllm_omni.diffusion.models.sensenova_u1.pipeline_sensenova_u1 import (
+ SenseNovaU1Pipeline,
+ _patchify,
+ _to_pil,
+ _unpatchify,
+)
+from vllm_omni.diffusion.models.sensenova_u1.sensenova_u1_transformer import (
+ clear_flash_kv_cache,
+)
+from vllm_omni.diffusion.request import OmniDiffusionRequest
+
+from unirl.rollout.engine.vllm_omni.pipelines._shared.interception import (
+ resolve_request_noise,
+)
+from unirl.rollout.engine.vllm_omni.pipelines.sensenova_u1.weight_names import (
+ missing_weight_sync_names,
+)
+from unirl.sde.kernels import FlowSDEStrategy
+from unirl.types.sampling import compute_trajectory_positions
+from unirl.utils.dtypes import parse_torch_dtype
+
+
+def _cache_row_to_cpu(cache: Any, row: int, batch_size: int) -> Any:
+ """Copy one expanded DynamicCache row without retaining worker CUDA tensors."""
+ if cache is None:
+ return None
+ result = copy.copy(cache)
+ result.layers = []
+ for source_layer in cache.layers:
+ target_layer = copy.copy(source_layer)
+ for name, value in vars(source_layer).items():
+ if name.startswith("flash_"):
+ if hasattr(target_layer, name):
+ delattr(target_layer, name)
+ continue
+ if not isinstance(value, torch.Tensor):
+ continue
+ if value.ndim > 0 and int(value.shape[0]) == batch_size:
+ value = value[row : row + 1]
+ setattr(target_layer, name, value.detach().to("cpu").clone())
+ result.layers.append(target_layer)
+ return result
+
+
+def _capture_conditions(caches: Dict[str, Any], p: SimpleNamespace) -> Dict[str, List[Any]]:
+ """Materialize the worker's prefix caches in the trainer replay format."""
+ batch_size = int(p.batch_size)
+ use_cfg = float(p.cfg_scale) > 1.0 and "uncond" in caches
+ if float(p.cfg_scale) > 1.0 and "img_cond" not in caches and "uncond" not in caches:
+ raise RuntimeError("SenseNova T2I CFG requested an unconditional branch, but the worker returned no cache.")
+ # One engine request contains multiple outputs of the same prompt. Prefix
+ # caches and indexes are therefore identical expanded views; copy them once
+ # and preserve aliases so pickle sends one payload per prompt group.
+ condition_cache = _cache_row_to_cpu(caches["cond"], 0, batch_size)
+ condition_index = caches["idx_cond"].detach().to("cpu").clone()
+ condition_caches = [condition_cache] * batch_size
+ if use_cfg:
+ uncondition_cache = _cache_row_to_cpu(caches["uncond"], 0, batch_size)
+ uncondition_index = caches["idx_uncond"].detach().to("cpu").clone()
+ uncondition_caches = [uncondition_cache] * batch_size
+ uncondition_indexes: List[Optional[torch.Tensor]] = [uncondition_index] * batch_size
+ else:
+ uncondition_caches = [None] * batch_size
+ uncondition_indexes = [None] * batch_size
+
+ return {
+ "prompts": [str(p.prompt)] * batch_size,
+ "condition_caches": condition_caches,
+ "uncondition_caches": uncondition_caches,
+ "condition_image_indexes": [condition_index] * batch_size,
+ "uncondition_image_indexes": uncondition_indexes,
+ "image_shapes": [(int(p.image_size[1]), int(p.image_size[0]))] * batch_size,
+ }
+
+
+class RLSenseNovaU1Pipeline(SenseNovaU1Pipeline):
+ """SenseNova pixel-flow inference with driver x_T and FlowGRPO capture."""
+
+ def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = "") -> None:
+ super().__init__(od_config=od_config, prefix=prefix)
+ self._pending_initial_noise: Optional[torch.Tensor] = None
+
+ def _parse_request(self, req: OmniDiffusionRequest) -> SimpleNamespace:
+ p = super()._parse_request(req)
+ sampling = req.sampling_params
+ extra = getattr(sampling, "extra_args", None) or {}
+ full_sigmas = extra.get("unirl_sigmas")
+ p.sigmas = (
+ torch.as_tensor(full_sigmas, dtype=torch.float32, device=self.device) if full_sigmas is not None else None
+ )
+ p.eta = float(getattr(sampling, "eta", 0.0) or 0.0)
+ p.sde_indices = tuple(sorted({int(index) for index in extra.get("sde_indices", [])}))
+ p.trajectory_dtype = parse_torch_dtype(
+ extra.get("trajectory_precision", "bf16"),
+ field_name="trajectory_precision",
+ )
+ p.sde_seed = int(extra.get("sde_seed", p.seed))
+ self._pending_initial_noise = resolve_request_noise(
+ req,
+ caller="RLSenseNovaU1Pipeline._parse_request",
+ )
+ return p
+
+ def _init_noise_and_schedule(self, p: SimpleNamespace) -> SimpleNamespace:
+ ns = super()._init_noise_and_schedule(p)
+ if p.sigmas is not None:
+ if p.sigmas.ndim != 1 or int(p.sigmas.numel()) != int(p.num_steps) + 1:
+ raise ValueError(
+ "RLSenseNovaU1Pipeline: driver sigma schedule must have "
+ f"{int(p.num_steps) + 1} entries, got shape={tuple(p.sigmas.shape)}."
+ )
+ if not bool(torch.all(p.sigmas[:-1] >= p.sigmas[1:])):
+ raise ValueError("RLSenseNovaU1Pipeline: driver sigma schedule must be non-increasing.")
+ ns.timesteps = 1.0 - p.sigmas
+ else:
+ p.sigmas = 1.0 - ns.timesteps
+
+ initial_noise = self._pending_initial_noise
+ self._pending_initial_noise = None
+ if initial_noise is not None:
+ expected = (int(p.batch_size), 3, int(p.image_size[1]), int(p.image_size[0]))
+ if tuple(initial_noise.shape) != expected:
+ raise RuntimeError(
+ f"RLSenseNovaU1Pipeline: driver x_T shape {tuple(initial_noise.shape)} != expected {expected}."
+ )
+ ns.image_prediction = initial_noise.to(
+ device=self.device,
+ dtype=p.trajectory_dtype,
+ ) * float(ns.noise_scale)
+ else:
+ ns.image_prediction = ns.image_prediction.to(dtype=p.trajectory_dtype)
+ return ns
+
+ def validate_weight_sync_names(self, weights: List[tuple[str, torch.Tensor]]) -> None:
+ """Reject full-weight buckets containing names this pipeline would skip."""
+ parameter_names = set(dict(self.named_parameters()))
+ missing = missing_weight_sync_names((name for name, _ in weights), parameter_names)
+ if missing:
+ sample = ", ".join(missing[:5])
+ raise RuntimeError(
+ f"SenseNova full-weight sync would silently skip {len(missing)} parameter(s); first names: [{sample}]"
+ )
+
+ def _run_denoising_loop(
+ self,
+ ns: SimpleNamespace,
+ caches: Dict[str, Any],
+ p: SimpleNamespace,
+ think_text: str = "",
+ ) -> DiffusionOutput:
+ """Run upstream model predictions with UniRL's stochastic transition."""
+ merge_size = self.merge_size
+ image_prediction = ns.image_prediction
+ sigmas = p.sigmas.to(device=self.device, dtype=torch.float32)
+ strategy = FlowSDEStrategy()
+ strategy.init_schedule(sigmas)
+ sigma_max = sigmas[1] if int(sigmas.numel()) > 1 else sigmas[0]
+ sde_indices = frozenset(int(index) for index in p.sde_indices)
+ if sde_indices and p.eta <= 0.0:
+ raise ValueError("RLSenseNovaU1Pipeline: non-empty sde_indices require eta > 0.")
+
+ trajectory_positions = set(compute_trajectory_positions(set(sde_indices), int(p.num_steps)))
+ trajectory_positions.add(int(p.num_steps))
+ stored_positions: List[int] = []
+ trajectory: List[torch.Tensor] = []
+ if 0 in trajectory_positions:
+ stored_positions.append(0)
+ trajectory.append(_patchify(image_prediction, self.patch_size * merge_size).detach().clone())
+ log_probs: List[torch.Tensor] = []
+ generator = torch.Generator(self.device).manual_seed(int(p.sde_seed))
+
+ for step_i in range(p.num_steps):
+ t = ns.timesteps[step_i]
+ t_next = ns.timesteps[step_i + 1]
+ z = _patchify(image_prediction, self.patch_size * merge_size)
+ image_input = _patchify(image_prediction, self.patch_size, channel_first=True)
+ image_embeds = self._extract_feature(
+ image_input.view(p.batch_size * ns.grid_h * ns.grid_w, -1),
+ gen_model=True,
+ grid_hw=ns.grid_hw,
+ ).view(p.batch_size, ns.token_h * ns.token_w, -1)
+
+ t_expanded = t.expand(p.batch_size * ns.token_h * ns.token_w)
+ timestep_embeddings = self.fm_modules["timestep_embedder"](t_expanded).view(
+ p.batch_size,
+ ns.token_h * ns.token_w,
+ -1,
+ )
+ if self.top_cfg.add_noise_scale_embedding:
+ noise_scale_tensor = torch.full_like(
+ t_expanded,
+ ns.noise_scale / self.top_cfg.noise_scale_max_value,
+ )
+ timestep_embeddings = timestep_embeddings + self.fm_modules["noise_scale_embedder"](
+ noise_scale_tensor
+ ).view(
+ p.batch_size,
+ ns.token_h * ns.token_w,
+ -1,
+ )
+ image_embeds = image_embeds + timestep_embeddings
+
+ velocity = self._denoise_step(image_prediction, ns, t, z, image_embeds, caches, p, step_i)
+ if step_i in sde_indices:
+ unit_next, log_prob, _ = strategy.denoise(
+ noise_pred=-velocity / float(ns.noise_scale),
+ sample=z / float(ns.noise_scale),
+ sigma=sigmas[step_i],
+ sigma_next=sigmas[step_i + 1],
+ eta=float(p.eta),
+ generator=generator,
+ sigma_max=float(sigma_max),
+ step_index=step_i,
+ )
+ z = unit_next * float(ns.noise_scale)
+ if log_prob is None:
+ raise RuntimeError(f"RLSenseNovaU1Pipeline: SDE step {step_i} produced no log probability.")
+ log_probs.append(log_prob.to(torch.float32))
+ else:
+ z = z + (t_next - t) * velocity
+
+ z = z.to(dtype=p.trajectory_dtype)
+ if step_i + 1 in trajectory_positions:
+ stored_positions.append(step_i + 1)
+ trajectory.append(z.detach().clone())
+ image_prediction = _unpatchify(
+ z,
+ self.patch_size * merge_size,
+ p.image_size[1],
+ p.image_size[0],
+ )
+
+ for key in ("cond", "uncond", "img_cond"):
+ if key in caches and not isinstance(caches[key], dict):
+ clear_flash_kv_cache(caches[key])
+
+ images = _to_pil(image_prediction)
+ custom_output: Dict[str, Any] = {
+ "sde_step_indices": list(sorted(sde_indices)),
+ "trajectory_indices": stored_positions,
+ "sensenova_u1_capture": _capture_conditions(caches, p),
+ }
+ if think_text:
+ custom_output["think_text"] = think_text
+ return DiffusionOutput(
+ output=images if len(images) != 1 else images[0],
+ trajectory_latents=torch.stack(trajectory, dim=1),
+ trajectory_timesteps=sigmas,
+ trajectory_log_probs=torch.stack(log_probs, dim=1) if log_probs else None,
+ custom_output=custom_output,
+ to_cpu=True,
+ )
+
+
+__all__ = ["RLSenseNovaU1Pipeline"]
diff --git a/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/weight_names.py b/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/weight_names.py
new file mode 100644
index 000000000..5831bd1c4
--- /dev/null
+++ b/unirl/rollout/engine/vllm_omni/pipelines/sensenova_u1/weight_names.py
@@ -0,0 +1,38 @@
+"""SenseNova full-weight sync name validation without runtime imports."""
+
+from __future__ import annotations
+
+from collections.abc import Iterable, Set
+
+STACKED_WEIGHT_MAPPINGS = (
+ (".qkv_proj_mot_gen", ".q_proj_mot_gen"),
+ (".qkv_proj_mot_gen", ".k_proj_mot_gen"),
+ (".qkv_proj_mot_gen", ".v_proj_mot_gen"),
+ (".qkv_proj", ".q_proj"),
+ (".qkv_proj", ".k_proj"),
+ (".qkv_proj", ".v_proj"),
+ (".gate_up_proj", ".gate_proj"),
+ (".gate_up_proj", ".up_proj"),
+)
+
+
+def missing_weight_sync_names(names: Iterable[str], parameter_names: Set[str]) -> list[str]:
+ """Return names neither direct-loadable nor covered by a fused mapping."""
+ missing: list[str] = []
+ for name in names:
+ if name in parameter_names:
+ continue
+ mapped = next(
+ (
+ name.replace(source_name, target_name)
+ for target_name, source_name in STACKED_WEIGHT_MAPPINGS
+ if source_name in name and name.replace(source_name, target_name) in parameter_names
+ ),
+ None,
+ )
+ if mapped is None:
+ missing.append(name)
+ return missing
+
+
+__all__ = ["STACKED_WEIGHT_MAPPINGS", "missing_weight_sync_names"]
diff --git a/unirl/rollout/engine/vllm_omni/stage_configs/sensenova_u1_t2i_rl.yaml b/unirl/rollout/engine/vllm_omni/stage_configs/sensenova_u1_t2i_rl.yaml
new file mode 100644
index 000000000..030db3ded
--- /dev/null
+++ b/unirl/rollout/engine/vllm_omni/stage_configs/sensenova_u1_t2i_rl.yaml
@@ -0,0 +1,45 @@
+# Single-stage SenseNova-U1.5 text-to-image config for RL rollout.
+#
+# vLLM-Omni owns the TP-aware NEO-Unify implementation. UniRL replaces only
+# the outer pipeline loop so the rollout uses driver-authored x_T, stochastic
+# FlowGRPO transitions, and replay-ready prefix-cache capture.
+
+stage_args:
+ - stage_id: 0
+ stage_type: diffusion
+ runtime:
+ process: true
+ devices: "0"
+ max_batch_size: 4
+ engine_args:
+ model_stage: diffusion
+ enforce_eager: true
+ distributed_executor_backend: "mp"
+ cache_backend: null
+ cache_config: null
+ enable_cache_dit_summary: false
+ enable_lora: false
+ parallel_config:
+ pipeline_parallel_size: 1
+ data_parallel_size: 1
+ tensor_parallel_size: 1
+ enable_expert_parallel: false
+ sequence_parallel_size: 1
+ ulysses_degree: 1
+ ring_degree: 1
+ cfg_parallel_size: 1
+ vae_patch_parallel_size: 1
+ use_hsdp: false
+ hsdp_shard_size: -1
+ hsdp_replicate_size: 1
+ custom_pipeline_args:
+ pipeline_class: unirl.rollout.engine.vllm_omni.pipelines.sensenova_u1.pipeline.RLSenseNovaU1Pipeline
+ worker_extension_cls: unirl.rollout.engine.vllm_omni.worker.dit_extension.DiTWeightSyncExtension
+ final_output: true
+ final_output_type: image
+
+runtime:
+ enabled: true
+ defaults:
+ window_size: -1
+ max_inflight: 1
diff --git a/unirl/rollout/engine/vllm_omni/utils/tracks.py b/unirl/rollout/engine/vllm_omni/utils/tracks.py
index 59aad2b36..9885cd13e 100644
--- a/unirl/rollout/engine/vllm_omni/utils/tracks.py
+++ b/unirl/rollout/engine/vllm_omni/utils/tracks.py
@@ -142,13 +142,37 @@ def build_image_segment(
)
head_custom = getattr(head, "custom_output", None) or {}
sde_step_indices_raw = head_custom.get("sde_step_indices")
+ trajectory_indices_raw = head_custom.get("trajectory_indices")
indices: Optional[torch.Tensor] = None
sde_indices: Optional[torch.Tensor] = None
+ if traj_latents is not None:
+ stored_steps = int(traj_latents.shape[1])
+ if trajectory_indices_raw is None:
+ indices = torch.arange(stored_steps, dtype=torch.long)
+ else:
+ indices = torch.as_tensor([int(i) for i in trajectory_indices_raw], dtype=torch.long)
+ if int(indices.numel()) != stored_steps:
+ raise RuntimeError(
+ "build_image_segment: trajectory_indices has "
+ f"{int(indices.numel())} entries but trajectory_latents stores {stored_steps} steps."
+ )
+ if indices.numel() and (
+ not bool(torch.all(indices[1:] > indices[:-1]))
+ or int(indices[0]) < 0
+ or (torch.is_tensor(seg_sigmas) and int(indices[-1]) >= int(seg_sigmas.numel()))
+ ):
+ raise RuntimeError(
+ "build_image_segment: trajectory_indices must be strictly increasing and within the sigma schedule; "
+ f"got {indices.tolist()}."
+ )
+ for output in diff_outputs[1:]:
+ other = (getattr(output, "custom_output", None) or {}).get("trajectory_indices")
+ if other is None or [int(i) for i in other] != indices.tolist():
+ raise RuntimeError("build_image_segment: trajectory_indices differ across engine requests.")
+
K = int(traj_log_probs.shape[1]) if traj_log_probs is not None else 0
if K > 0:
- T_plus_1 = int(traj_latents.shape[1]) if traj_latents is not None else K + 1
- indices = torch.arange(T_plus_1, dtype=torch.long)
if sde_step_indices_raw is not None:
sde_indices = torch.as_tensor([int(i) for i in sde_step_indices_raw], dtype=torch.long)
if int(sde_indices.numel()) != K:
@@ -170,8 +194,6 @@ def build_image_segment(
sde_indices = torch.arange(K, dtype=torch.long)
elif traj_latents is not None:
traj_log_probs = None
- T_plus_1 = int(traj_latents.shape[1])
- indices = torch.arange(T_plus_1, dtype=torch.long)
sde_indices = None
return make_image_segment(
diff --git a/unirl/rollout/engine/vllm_omni/worker/ipc_receive_mixin.py b/unirl/rollout/engine/vllm_omni/worker/ipc_receive_mixin.py
index 4d10273b7..2cb4551a1 100644
--- a/unirl/rollout/engine/vllm_omni/worker/ipc_receive_mixin.py
+++ b/unirl/rollout/engine/vllm_omni/worker/ipc_receive_mixin.py
@@ -107,11 +107,19 @@ def _diffrl_load_bucket(
def _diffrl_load_weights(self, weights: list[tuple[str, torch.Tensor]]) -> None:
"""Forward weights to whichever loader the underlying worker exposes."""
+ runner = getattr(self, "model_runner", None)
+ if runner is not None:
+ for attr in ("pipeline", "model"):
+ obj = getattr(runner, attr, None)
+ validator = getattr(obj, "validate_weight_sync_names", None) if obj is not None else None
+ if callable(validator):
+ validator(weights)
+ break
+
loader = getattr(self, "load_weights", None)
if callable(loader):
loader(weights)
return
- runner = getattr(self, "model_runner", None)
if runner is None:
raise RuntimeError(f"{type(self).__name__}: no `load_weights` and no `model_runner`.")
for attr in ("model", "pipeline"):