DanTim05 commited on
Commit
e15cfd1
·
verified ·
1 Parent(s): 4634e5b

Add Bridge native Stage2 training source; GPU validation NOT_RUN

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. releases/bridge-native-v3/README.md +160 -0
  2. releases/bridge-native-v3/code/ATTRIBUTIONS.md +0 -0
  3. releases/bridge-native-v3/code/LICENSE +52 -0
  4. releases/bridge-native-v3/code/NOTICE +54 -0
  5. releases/bridge-native-v3/code/README.md +142 -0
  6. releases/bridge-native-v3/code/configs/bridge_stage2_native_contract.json +47 -0
  7. releases/bridge-native-v3/code/cosmos_framework/__init__.py +3 -0
  8. releases/bridge-native-v3/code/cosmos_framework/configs/base/config.py +109 -0
  9. releases/bridge-native-v3/code/cosmos_framework/configs/base/defaults/model_config.py +380 -0
  10. releases/bridge-native-v3/code/cosmos_framework/configs/base/defaults/vlm.py +17 -0
  11. releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva.py +65 -0
  12. releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_pim.py +61 -0
  13. releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_pim_inference.py +36 -0
  14. releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_test.py +14 -0
  15. releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_transition_memory.py +66 -0
  16. releases/bridge-native-v3/code/cosmos_framework/data/__init__.py +2 -0
  17. releases/bridge-native-v3/code/cosmos_framework/data/generator/__init__.py +2 -0
  18. releases/bridge-native-v3/code/cosmos_framework/data/generator/action/__init__.py +2 -0
  19. releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/__init__.py +15 -0
  20. releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/robocasa365_lerobot_dataset.py +334 -0
  21. releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/widowx_bridge_v3_dataset.py +163 -0
  22. releases/bridge-native-v3/code/cosmos_framework/data/generator/action/domain_utils.py +83 -0
  23. releases/bridge-native-v3/code/cosmos_framework/data/generator/action/normalizer_stats/robocasa365_target_action_stats.json +11 -0
  24. releases/bridge-native-v3/code/cosmos_framework/data/generator/action/transforms.py +710 -0
  25. releases/bridge-native-v3/code/cosmos_framework/data/generator/joint_dataloader.py +1271 -0
  26. releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/modality.py +256 -0
  27. releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/packers.py +446 -0
  28. releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/sequence.py +1203 -0
  29. releases/bridge-native-v3/code/cosmos_framework/inference/common/public_model_config.py +361 -0
  30. releases/bridge-native-v3/code/cosmos_framework/model/generator/mot/cosmos3_vfm_network.py +1536 -0
  31. releases/bridge-native-v3/code/cosmos_framework/model/generator/omni_mot_model.py +0 -0
  32. releases/bridge-native-v3/code/cosmos_framework/model/generator/utils/data_and_condition.py +182 -0
  33. releases/bridge-native-v3/code/cosmos_framework/model/zeva/__init__.py +52 -0
  34. releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_artifacts.py +77 -0
  35. releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_artifacts_test.py +31 -0
  36. releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_protocol.py +78 -0
  37. releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_protocol_test.py +45 -0
  38. releases/bridge-native-v3/code/cosmos_framework/model/zeva/brief_interaction_trace.py +24 -0
  39. releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_prompt.py +12 -0
  40. releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_transition_encoder.py +374 -0
  41. releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_transition_encoder_test.py +28 -0
  42. releases/bridge-native-v3/code/cosmos_framework/model/zeva/checkpoint_io.py +22 -0
  43. releases/bridge-native-v3/code/cosmos_framework/model/zeva/checkpoint_io_test.py +32 -0
  44. releases/bridge-native-v3/code/cosmos_framework/model/zeva/cte_losses.py +177 -0
  45. releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/__init__.py +1 -0
  46. releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/robocasa_transition_memory.py +49 -0
  47. releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory.py +246 -0
  48. releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_policy_adapter.py +116 -0
  49. releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_runtime.py +100 -0
  50. releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_test.py +161 -0
releases/bridge-native-v3/README.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Bridge/WidowX handoff — memory assets verified
2
+
3
+ **v3 implementation update:** `scripts/run_bridge_stage2_native.py` now exists.
4
+ See [native training instructions](code/docs/bridge_native_training.md). Its GPU training and
5
+ 50/500-step acceptance remain NOT_RUN. The previously published v2 code snapshot
6
+ predates this implementation. Use v3 for the current code; the GPU readiness flag stays false.
7
+
8
+ The v3 bundle contains the native trainer source and the verified v2 memory assets.
9
+ Use `releases/bridge-native-v3`. The earlier v1 upload was incomplete because
10
+ the Space's existing `.gitignore` silently omitted `.pt` files. It is retained
11
+ for provenance, not as a usable release. The publisher checks every remote
12
+ Git/LFS hash and preserves all pre-existing Space files and ignore rules.
13
+ Do not run `run_bridge_stage2_pilot.py`, `run_ipec_full_training.sh`, or
14
+ `run_widowx_training.sh` for this experiment: those historical paths use the old
15
+ policy contracts and/or old RoboCasa/IPEC initialization.
16
+
17
+ ## What is delivered
18
+
19
+ - `memory/cte_30000_inference.pt`: the exact 177 model tensors of the selected
20
+ Bridge reached-state-delta CTE at step 30,000. No optimizer/RNG is exported.
21
+ - `memory/static_bank_3000/`: unchanged bank and its original manifest, selection,
22
+ support/query partition, and the shards needed to verify the original manifest.
23
+ Keys are `[3000,128]`, values `[3000,256]`, one entry per support episode.
24
+ - `memory/stage1_split.json`, `pilot_split.json`, and the separate CTE legacy and
25
+ policy native10 normalizers.
26
+ - `code/`: the actual ZeVA overlay source snapshot, including local/uncommitted
27
+ scripts, the native geometry contract helpers, tests, and handoff tools.
28
+ - `handoff_manifest.json`: per-file SHA256/size, exact upstream versions, data/VAE
29
+ provenance, CTE export-to-original tensor binding, and explicit readiness.
30
+
31
+ The local original 30k CTE checkpoint is unchanged. Its SHA256 is
32
+ `5c94378ea87b875dd709d46cf7941cdb5ca33584df071fef254311c6bbab912e`.
33
+ It contained a Stage1 optimizer, so the downloadable inference export necessarily
34
+ has a **different file hash**. All model tensors were checked exactly equal;
35
+ the export manifest records both identities. The bank's original source-checkpoint
36
+ identity is preserved, not silently rewritten to the export hash.
37
+
38
+ The original bank SHA256 remains
39
+ `bd248a1df2a908e3f16a6a6df66c50ad691514a011edb4c4704493c2e5799ffb`.
40
+ Absolute paths in original provenance JSON are historical records only, not
41
+ runtime paths. Use bundle-relative paths and local CLI arguments.
42
+
43
+ ## Download and verify on another machine
44
+
45
+ First obtain the release commit from the handoff message or publication receipt.
46
+ Replace `RELEASE_COMMIT` below with that full commit. Never substitute a moving
47
+ `main` when reproducing an experiment.
48
+
49
+ ```bash
50
+ hf download DanTim05/DynaTTT --repo-type space \
51
+ --revision RELEASE_COMMIT --include 'releases/bridge-native-v3/*' \
52
+ --local-dir ./zeva-transfer
53
+
54
+ cd zeva-transfer/releases/bridge-native-v3
55
+ python code/scripts/bridge_handoff.py verify --bundle .
56
+ ```
57
+
58
+ That first check uses only the Python standard library. A successful memory check
59
+ does **not** imply training readiness: `training_ready` remains false.
60
+
61
+ Install the pinned NVIDIA Cosmos Framework following its setup guide. ZeVA is an
62
+ overlay, not a stand-alone pip package. In a new dedicated workspace:
63
+
64
+ ```bash
65
+ git clone https://github.com/NVIDIA/cosmos-framework.git cosmos-framework
66
+ git -C cosmos-framework checkout --detach ee58e41467f33c49ddde08b4d0ef4923876a95ac
67
+
68
+ # Run from the downloaded release directory; use an absolute path to the clone.
69
+ export BRIDGE_FRAMEWORK_DIR=/absolute/path/to/cosmos-framework
70
+ rsync -a --exclude=README.md ./code/ "$BRIDGE_FRAMEWORK_DIR/"
71
+ cd "$BRIDGE_FRAMEWORK_DIR"
72
+
73
+ # Requires the CUDA/PyTorch prerequisites documented by this pinned framework.
74
+ uv sync --all-extras --group=cu128-train
75
+ source .venv/bin/activate
76
+ ```
77
+
78
+ Run in the resulting environment, specifying the release directory:
79
+
80
+ ```bash
81
+ python -m unittest -v scripts.test_bridge_handoff scripts.test_bridge_native_contract
82
+ python -m scripts.bridge_handoff verify \
83
+ --bundle /absolute/path/to/zeva-transfer/releases/bridge-native-v3 --deep
84
+ ```
85
+
86
+ The export uses safe tensor/basic-type deserialization (`weights_only=True`).
87
+ It loads strictly into CTE and is frozen. Nothing here loads a policy checkpoint,
88
+ starts a training job, allocates GPUs, or stops another process.
89
+
90
+ ## Download pinned external dependencies
91
+
92
+ ```bash
93
+ python -m scripts.bridge_handoff download \
94
+ --bundle /absolute/path/to/zeva-transfer/releases/bridge-native-v3 \
95
+ --destination /large/disk/bridge-run --base --dataset --vae
96
+ ```
97
+
98
+ The downloader uses:
99
+
100
+ - `nvidia/Cosmos3-Nano@e59a53c25979a090fa8706c9acc0c254a6e89b92`, including
101
+ root configuration, transformer, vision encoder and tokenizer. A URL ending
102
+ in `/transformer` alone is not a complete training installation.
103
+ - `IPEC-COMMUNITY/bridge_orig_lerobot@0e9d76d07e9df3ea3eba257b2520d4913833fad2`.
104
+ Its metadata must match the CTE source hashes; no task strings are invented.
105
+ - The exact pinned Wan VAE revision recorded in the manifest. Expected SHA256:
106
+ `20eb789667fa5e60e7516bf509512f6cb61f01b0aa0695eadaea930c13892b36`.
107
+
108
+ Use each machine's own HF/W&B credentials; no credentials are included here.
109
+ Before a full run, verify all expected per-episode parquet/image_0 video files,
110
+ not merely the presence of `meta/info.json`.
111
+
112
+ ## Fixed research design / release validation
113
+
114
+ `code/configs/bridge_stage2_native_contract.json` describes the research contract.
115
+ The native launcher implements it; use the v3 source and
116
+ [training instructions](code/docs/bridge_native_training.md), alongside the included memory assets.
117
+ `verify --require-training-ready` still exits nonzero for this release
118
+ because its GPU acceptance has not passed.
119
+
120
+ The native path must preserve:
121
+
122
+ - Native policy `[B,32,10]` (XYZ + rot6d + gripper index 9), current absolute
123
+ proprio `[B,10]`, independent reached CTE7 (RPY, gripper index 6), runtime-verified
124
+ internal width 64 with padded channels masked. Labels come from adjacent raw
125
+ states through NVIDIA's transforms, not reached7 converted to 10D.
126
+ - Original Nano domain-7 action projections and shared action embedding. Compare
127
+ checkpoint/constructed/pre-optimizer hashes. No DROID specialist, RoboCasa,
128
+ old IPEC delta, old 7D policy, or old policy optimizer.
129
+ - Frozen shared understanding/reasoner, Wan VAE and CTE; train generation
130
+ `moe_gen/time_embedder/vae2llm/llm2vae`, domain7 action rows, and new ZeVA/proprio
131
+ modules. Never put the entire policy forward in no_grad. Use a real WAM vision
132
+ loss so llm2vae receives nonzero gradients. Other-domain rows must not change.
133
+ - A **new** trained/frozen Bridge query head (not supplied yet), pure frozen
134
+ understanding image+instruction readout 4096→1024→128, cosine top5/T=0.07.
135
+ Do not reuse a RoboCasa head or treat random queries as trained retrieval.
136
+ - Support/query separation; phase/effects from query prefixes only; one
137
+ transition per four actual controls, first completed effect at 16 controls,
138
+ last four effects only. No all-episode future context and no full-dataset cache.
139
+ - Native10 decoding must invert the tool/flange/OpenCV transforms and account
140
+ for the Simpler controller's previous target. Recompute reached7 from actual
141
+ before/after states only after env.step.
142
+ - Real runtime shape trace; 32 train/8 development 50-step engineering gate,
143
+ then 500-step fit gate, then continuous. The 8 development episodes are held
144
+ out from Stage2, not independent of CTE training. Log sampled actions, masks,
145
+ per-dimension/physical/gripper errors, BIT histogram, gradients and deltas.
146
+ - Generation/action/ZeVA LR 2e-5/1e-4/3e-5; domain tables weight_decay=0;
147
+ every 50 steps verify other-domain diff=0 and frozen hashes unchanged.
148
+ - Save resumable trainable replacements, optimizer/scheduler/RNG/data cursor
149
+ every 10k optimizer updates; never delete old checkpoints. Estimate ~91 GiB
150
+ per checkpoint with FP32-master Adam; plan disk capacity accordingly.
151
+
152
+ 50/500-step gates, full model runtime dimensions, online environment acceptance,
153
+ and continuous training are **NOT RUN**. CPU geometry/asset tests are not substitutes.
154
+
155
+ ## Licenses and provenance
156
+
157
+ The source snapshot includes the original LICENSE/NOTICE/ATTRIBUTIONS. The
158
+ dataset, NVIDIA base and Wan VAE retain their respective upstream licenses.
159
+ The 3000-entry bank is derived from IPEC Bridge; it is not a copy of the raw videos.
160
+ No base model, raw dataset, policy optimizer, secret, or training log is redistributed.
releases/bridge-native-v3/code/ATTRIBUTIONS.md ADDED
The diff for this file is too large to render. See raw diff
 
releases/bridge-native-v3/code/LICENSE ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+
3
+
4
+ OpenMDW License Agreement, version 1.1 (OpenMDW-1.1)
5
+
6
+ By exercising rights granted to you under this agreement, you accept and agree
7
+ to its terms.
8
+
9
+ As used in this agreement, "Model Materials" means the materials provided to
10
+ you under this agreement, consisting of: (1) one or more machine learning
11
+ models (including architecture and parameters); and (2) all related artifacts
12
+ (including associated data, documentation and software) that are provided to
13
+ you hereunder.
14
+
15
+ Subject to your compliance with this agreement, permission is hereby granted,
16
+ free of charge, to deal in the Model Materials without restriction, including
17
+ under all copyright, patent, database, and trade secret rights included or
18
+ embodied therein.
19
+
20
+ If you distribute any portion of the Model Materials, you shall retain in your
21
+ distribution (1) a copy of this agreement, and (2) all copyright notices and
22
+ other notices of origin included in the Model Materials that are applicable to
23
+ your distribution.
24
+
25
+ If you file, maintain, or voluntarily participate in a lawsuit against any
26
+ person or entity asserting that the Model Materials directly or indirectly
27
+ infringe any patent or copyright, then all rights and grants made to you
28
+ hereunder are terminated, unless that lawsuit was in response to a
29
+ corresponding lawsuit first brought against you.
30
+
31
+ This agreement does not impose any restrictions or obligations with respect to
32
+ any use, modification, or sharing of any outputs generated by using the Model
33
+ Materials.
34
+
35
+ THE MODEL MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
36
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE, NONINFRINGEMENT, ACCURACY, OR THE
38
+ ABSENCE OF LATENT OR OTHER DEFECTS OR ERRORS, WHETHER OR NOT DISCOVERABLE, ALL
39
+ TO THE GREATEST EXTENT PERMISSIBLE UNDER APPLICABLE LAW.
40
+
41
+ YOU ARE SOLELY RESPONSIBLE FOR (1) CLEARING RIGHTS OF OTHER PERSONS THAT MAY
42
+ APPLY TO THE MODEL MATERIALS OR ANY USE THEREOF, INCLUDING WITHOUT LIMITATION
43
+ ANY PERSON'S COPYRIGHTS OR OTHER RIGHTS INCLUDED OR EMBODIED IN THE MODEL
44
+ MATERIALS; (2) OBTAINING ANY NECESSARY CONSENTS, PERMISSIONS OR OTHER RIGHTS
45
+ REQUIRED FOR ANY USE OF THE MODEL MATERIALS; OR (3) PERFORMING ANY DUE
46
+ DILIGENCE OR UNDERTAKING ANY OTHER INVESTIGATIONS INTO THE MODEL MATERIALS OR
47
+ ANYTHING INCORPORATED OR EMBODIED THEREIN.
48
+
49
+ IN NO EVENT SHALL THE PROVIDERS OF THE MODEL MATERIALS BE LIABLE FOR ANY CLAIM,
50
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
51
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MODEL MATERIALS, THE
52
+ USE THEREOF OR OTHER DEALINGS THEREIN.
releases/bridge-native-v3/code/NOTICE ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Cosmos
2
+
3
+ Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES.
4
+
5
+ This file contains attribution notices for third-party source code that is
6
+ adapted, copied, or vendored in this repository. Dependency notices are listed
7
+ separately in ATTRIBUTIONS.md.
8
+
9
+ HuggingFace Transformers / Qwen
10
+ Source: https://github.com/huggingface/transformers
11
+ License: Apache-2.0
12
+ Notices:
13
+ Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team.
14
+ Copyright 2025 The Qwen team, Alibaba Group and the HuggingFace Inc. team.
15
+ Copyright 2025 The Qwen Team and The HuggingFace Inc. team.
16
+ Copyright 2025 HuggingFace Inc. team.
17
+ Files:
18
+ cosmos_framework/model/generator/vlm/qwen3_vl/configuration_qwen3_vl.py
19
+ cosmos_framework/model/generator/vlm/qwen3_vl/qwen3_vl.py
20
+ cosmos_framework/model/generator/vlm/qwen3_vl/utils.py
21
+ cosmos_framework/model/generator/vlm/qwen3_vl/video_processing_qwen3_vl.py
22
+
23
+ ByteDance-Seed Bagel / Qwen2 tokenizer
24
+ Source: https://github.com/ByteDance-Seed/Bagel
25
+ License: Apache-2.0
26
+ Notice:
27
+ Copyright 2024 The Qwen Team and The HuggingFace Inc. team.
28
+ File:
29
+ cosmos_framework/model/generator/tokenizers/tokenization_qwen2.py
30
+
31
+ HuggingFace Diffusers UniPC scheduler
32
+ Source: https://github.com/huggingface/diffusers
33
+ License: Apache-2.0
34
+ Notices:
35
+ Copyright 2024 TSAIL Team and The HuggingFace Team. All rights reserved.
36
+ Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
37
+ File:
38
+ cosmos_framework/model/generator/diffusion/samplers/fm_solvers_unipc.py
39
+
40
+ Detectron2 lazy config helpers
41
+ Source: https://github.com/facebookresearch/detectron2
42
+ License: Apache-2.0
43
+ Notice:
44
+ Copyright (c) Facebook, Inc. and its affiliates.
45
+ File:
46
+ cosmos_framework/utils/lazy_config/__init__.py
47
+
48
+ TaylorSeer
49
+ Source: https://github.com/Shenyi-Z/TaylorSeer
50
+ License: GPL-3.0
51
+ Notice:
52
+ Upstream source files do not contain an explicit copyright notice.
53
+ Files:
54
+ cosmos_framework/model/generator/utils/taylorseer.py
releases/bridge-native-v3/code/README.md ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # zeva
2
+
3
+ **In-Context Causal Learning for Generalizable Embodied Manipulation**
4
+
5
+ [Project page](https://air-embodied-brain.github.io/Zeva) ·
6
+ [Model weights](https://huggingface.co/chen123fu/zeva) ·
7
+ [Atomic5 reproduction](docs/reproduce.md) ·
8
+ [Cross-attempt reproduction](docs/zeva_pim.md)
9
+
10
+ ---
11
+
12
+ ## Overview
13
+
14
+ zeva enables a frozen robot policy to learn from its own physical interaction
15
+ experience at deployment time. Instead of updating model parameters, zeva
16
+ extracts action-induced state changes, stores them in a dual-timescale causal
17
+ memory, and retrieves relevant interaction evidence as context for subsequent
18
+ actions.
19
+
20
+ ![zeva overview](assets/zeva_overview.png)
21
+
22
+ ## Method
23
+
24
+ zeva implements **In-Context Causal Learning (ICCL)** in three parts:
25
+
26
+ 1. **Causal Interaction Extraction.** A Causal Transition Encoder (CTE)
27
+ integrates visual state, executed action, and observed state change. It
28
+ produces a phase token \(p_\tau\) and a causal interaction signal
29
+ \(e_\tau\). CTE is trained with causal-effect prediction, task-identity
30
+ clustering, and phase-progression objectives.
31
+ 2. **Dual-timescale Causal Memory.** A Brief Interaction Trace (BIT) retains
32
+ recent evidence within an attempt, while Persistent Interaction Memory (PIM)
33
+ consolidates phase/effect evidence across attempts in the same episode.
34
+ 3. **In-Context Policy Injection.** Phase-conditioned retrieval selects relevant
35
+ PIM entries. A memory encoder fuses the task, current phase, BIT, and retrieved
36
+ PIM evidence into a Causal Prompt \(M_\tau\) for the diffusion policy.
37
+
38
+ > [!IMPORTANT]
39
+ > During deployment, all neural parameters remain frozen. Only BIT and PIM are
40
+ > updated online.
41
+
42
+ ## Installation
43
+
44
+ zeva is released as an overlay for
45
+ [NVIDIA Cosmos Framework](https://github.com/NVIDIA/cosmos-framework) at commit
46
+ `ee58e41467f33c49ddde08b4d0ef4923876a95ac`.
47
+
48
+ ```bash
49
+ git clone https://github.com/NVIDIA/cosmos-framework.git
50
+ cd cosmos-framework
51
+ git checkout ee58e41467f33c49ddde08b4d0ef4923876a95ac
52
+ cd ..
53
+
54
+ git clone https://github.com/dsdfasfaac/zeva-test.git zeva
55
+ rsync -a --exclude=.git --exclude=README.md zeva/ cosmos-framework/
56
+ cd cosmos-framework
57
+ ```
58
+
59
+ Install the remaining Cosmos Framework dependencies following the upstream
60
+ [setup instructions](https://github.com/NVIDIA/cosmos-framework/blob/main/docs/setup.md).
61
+
62
+ ## Reproduction
63
+
64
+ > [!NOTE]
65
+ > The released checkpoint was trained on success-only trajectories and
66
+ > evaluated without cross-attempt self-evolution. Its benchmark success rate
67
+ > therefore measures the frozen policy rather than improvement from accumulated
68
+ > PIM experience.
69
+
70
+ ### Atomic5 benchmark reproduction
71
+
72
+ Follow [docs/reproduce.md](docs/reproduce.md) to start the frozen policy server
73
+ and reproduce the locked five-task evaluation. The released protocol uses a
74
+ 32-action horizon, 30 UniPC steps, guidance 3.0, shift 5.0, and no retries.
75
+
76
+ ### Fixed-seed cross-attempt evolution
77
+
78
+ Follow [docs/zeva_pim.md](docs/zeva_pim.md) to run PIM-enabled evaluation on a
79
+ fixed episode. Each case below was reproduced twice from empty PIM; `F` denotes
80
+ failure and `S` denotes success.
81
+
82
+ > [!NOTE]
83
+ > The current release includes these reproducible case studies.
84
+
85
+ | Task | Seed | Attempt sequence |
86
+ | --- | ---: | --- |
87
+ | OpenStandMixerHead | 199 | `FSSSS` |
88
+ | TurnOnElectricKettle | 197 | `FSSSSS` |
89
+ | CloseToasterOvenDoor | 199 | `FFFSSSSSSSS` |
90
+ | TurnOnMicrowave | 200 | `FFFFSSSS` |
91
+ | CoffeeSetupMug | 204 | `FFFFFFFFFSSSS` |
92
+
93
+ Validate an exported pair of runs with:
94
+
95
+ ```bash
96
+ python experiment_manifests/verify_pim_evolve_case.py \
97
+ --first /path/to/first_run \
98
+ --repeat /path/to/repeat_run \
99
+ --task TurnOnElectricKettle \
100
+ --seed 197
101
+ ```
102
+
103
+ ## Repository layout
104
+
105
+ | Path | Description |
106
+ | --- | --- |
107
+ | `cosmos_framework/model/zeva/` | CTE, BIT, PIM, retrieval, Causal Prompt, and policy injection |
108
+ | `cosmos_framework/scripts/` | Policy servers and RoboCasa365 evaluators |
109
+ | `experiment_manifests/` | Locked evaluation and verification utilities |
110
+ | `examples/` | Training launchers and experiment configurations |
111
+ | `docs/reproduce.md` | Atomic5 reproduction protocol |
112
+ | `docs/zeva_pim.md` | PIM and cross-attempt case studies |
113
+
114
+ ## Citation
115
+
116
+ If you find this work useful, please cite:
117
+
118
+ ```bibtex
119
+ @misc{zeva2026,
120
+ title={Zeva: In-Context Causal Learning for Generalizable Embodied Manipulation},
121
+ author={Fu Chen, Xin Ding, Bingjia Huang, Xiangyu Li, Mingju Wang, Jiawei He, Kun Li, Wei Sun, Yunxin Liu, Hao Wu, Ting Cao},
122
+ howpublished={\url{https://air-embodied-brain.github.io/Zeva}},
123
+ year={2026},
124
+ }
125
+ ```
126
+
127
+ ## Acknowledgements
128
+
129
+ zeva builds on the following open-source projects:
130
+
131
+ - [NVIDIA Cosmos Framework](https://github.com/NVIDIA/cosmos-framework)
132
+ - [RoboCasa365](https://github.com/robocasa/robocasa)
133
+ - [BehaviorVLA](https://github.com/iLearn-Lab/ICML26-BehaviorVLA)
134
+
135
+ Please follow the licenses and citation requirements of all external models,
136
+ datasets, simulators, and software dependencies.
137
+
138
+ ## License
139
+
140
+ This repository is released under the included [OpenMDW-1.1 license](LICENSE).
141
+ External models, datasets, and simulators remain subject to their respective
142
+ licenses.
releases/bridge-native-v3/code/configs/bridge_stage2_native_contract.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "bridge_native_stage2_design_v1",
3
+ "implementation_status": "IMPLEMENTED_GPU_VALIDATION_NOT_RUN",
4
+ "entrypoint": "python -m scripts.run_bridge_stage2_native",
5
+ "base_repo": "nvidia/Cosmos3-Nano",
6
+ "base_revision": "e59a53c25979a090fa8706c9acc0c254a6e89b92",
7
+ "framework_revision": "ee58e41467f33c49ddde08b4d0ef4923876a95ac",
8
+ "dataset_repo": "IPEC-COMMUNITY/bridge_orig_lerobot",
9
+ "dataset_revision": "0e9d76d07e9df3ea3eba257b2520d4913833fad2",
10
+ "domain_name": "bridge_orig_lerobot",
11
+ "domain_id": 7,
12
+ "policy_action_dim": 10,
13
+ "policy_gripper_index": 9,
14
+ "cte_action_dim": 7,
15
+ "cte_gripper_index": 6,
16
+ "proprio_dim": 10,
17
+ "policy_horizon": 32,
18
+ "expected_model_action_dim": 64,
19
+ "model_action_dim_source": "runtime configuration AND constructed action projections",
20
+ "global_dim": 256,
21
+ "phase_dim": 128,
22
+ "effect_dim": 128,
23
+ "effect_history_length": 4,
24
+ "boundary_controls": 4,
25
+ "completed_effect_controls": 16,
26
+ "cte_preprocess_id": "bridge_cte_image0_rgb_singleframe_256_bf16_wan22_v1",
27
+ "cte_normalizer_id": "bridge_reached_delta_legacy_quantile_v0",
28
+ "policy_normalizer": "NVIDIA bridge_orig_lerobot native10 quantile (unclipped)",
29
+ "proprio_normalizer": null,
30
+ "cte_source_step": 30000,
31
+ "static_bank_entries": 3000,
32
+ "retrieval": {"readout": "frozen Nano understanding-only image+instruction prefill", "head_dimensions": [4096, 1024, 128], "head_status": "NOT_TRAINED", "top_k": 5, "temperature": 0.07},
33
+ "learning_rates": {"generation": 0.00002, "action": 0.0001, "zeva": 0.00003},
34
+ "frozen": ["shared_reasoner", "shared_understanding", "semantic_vision_encoder", "Wan_VAE", "Bridge_CTE", "bank", "retrieval_head_after_training"],
35
+ "train_generation": ["moe_gen", "time_embedder", "vae2llm", "llm2vae"],
36
+ "train_action": ["action2llm_domain7", "llm2action_domain7", "action_modality_embed_shared"],
37
+ "train_zeva": ["proprio_projector_and_gate", "behavior_pbd", "behavior_adapter", "behavior_global_projector", "phase_effect_receivers"],
38
+ "action_table_weight_decay": 0.0,
39
+ "domain7_initialization": "original Nano checkpoint tensors; never reinitialize",
40
+ "other_domain_max_absolute_diff": 0.0,
41
+ "losses": {"mode": "wam", "vision_weight": 1.0, "action_weight": 10.0, "pbd_weight": 0.01, "ignore_tail_padding": true},
42
+ "gates": {"train_episodes": 32, "development_episodes": 8, "smoke_optimizer_steps": 50, "fit_optimizer_steps": 500, "both_required_before_continuous": true},
43
+ "checkpoint": {"save_every_optimizer_steps": 10000, "automatic_deletion": false, "save_frozen_base": false, "save_optimizer_and_data_cursor": true},
44
+ "forbidden_initialization": ["Cosmos3-Nano-Policy-DROID", "ZeVA RoboCasa Stage2", "RoboCasa Atomic5", "old ipec_full delta", "old Bridge 7D policy", "old optimizer state"],
45
+ "no_full_dataset_feature_cache": true,
46
+ "seed": 20260926
47
+ }
releases/bridge-native-v3/code/cosmos_framework/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Zeva overlay package; extend imports to the upstream Cosmos framework."""
2
+ from pkgutil import extend_path
3
+ __path__ = extend_path(__path__, __name__)
releases/bridge-native-v3/code/cosmos_framework/configs/base/config.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ from typing import Any, List
5
+
6
+ import attrs
7
+
8
+ from cosmos_framework.trainer import ImaginaireTrainer as Trainer
9
+ from cosmos_framework.utils import config
10
+
11
+
12
+ @attrs.define(slots=False)
13
+ class DataSetting:
14
+ """Configuration for data.
15
+
16
+ Attributes:
17
+ qwen_max_video_token_length: Maximum video token length.
18
+ qwen_target_fps: Target fps for video sampling.
19
+ text_chat_order: Order of text items in user messages.
20
+ """
21
+
22
+ qwen_max_video_token_length: int = 8192
23
+
24
+
25
+ @attrs.define(slots=False)
26
+ class Config(config.Config):
27
+ data_setting: DataSetting = attrs.field(factory=DataSetting)
28
+ defaults: List[Any] = attrs.field(
29
+ factory=lambda: [
30
+ "_self_",
31
+ {"model": "mot_fsdp"},
32
+ {"data_train": None},
33
+ {"data_val": None},
34
+ {"optimizer": "adamw"},
35
+ {"scheduler": "warmup_cosine_lr"},
36
+ {"checkpoint": "s3"},
37
+ {"callbacks": ["basic", "optimization", "job_monitor", "generation"]},
38
+ {"ema": "power"},
39
+ {"tokenizer": "wan2pt2_tokenizer"},
40
+ {"sound_tokenizer": None}, # Optional: for audio-video generation
41
+ {"vlm_config": None},
42
+ {"ckpt_type": "dcp"},
43
+ {"experiment": None},
44
+ ]
45
+ )
46
+
47
+
48
+ def make_config() -> Config:
49
+ c = Config(
50
+ model=None,
51
+ optimizer=None,
52
+ scheduler=None,
53
+ dataloader_train=None,
54
+ dataloader_val=None,
55
+ )
56
+
57
+ # Specifying values through instances of attrs
58
+ c.job.project = "cosmos3_vfm"
59
+ c.job.group = "debug"
60
+ c.job.name = "delete_${now:%Y-%m-%d}_${now:%H-%M-%S}"
61
+
62
+ c.trainer.type = Trainer
63
+ c.trainer.straggler_detection.enabled = False
64
+ c.trainer.max_iter = 400_000
65
+ c.trainer.logging_iter = 20
66
+ c.trainer.validation_iter = 100
67
+ c.trainer.run_validation = False
68
+ c.trainer.callbacks = None
69
+
70
+ c.upload_reproducible_setup = False
71
+
72
+ from cosmos_framework.configs.base.defaults.callbacks import register_callbacks
73
+ from cosmos_framework.configs.base.defaults.checkpointer import register_checkpoint, register_ckpt_type
74
+ from cosmos_framework.configs.base.defaults.ema import register_ema
75
+
76
+ # from cosmos_framework.configs.base.defaults.data import register_data
77
+ from cosmos_framework.configs.base.defaults.model import register_model
78
+ from cosmos_framework.configs.base.defaults.optimizer import register_optimizer, register_scheduler
79
+ from cosmos_framework.configs.base.defaults.tokenizer import register_sound_tokenizer, register_tokenizer
80
+ from cosmos_framework.configs.base.defaults.reasoner import register_vlm
81
+
82
+ # Call this function to register config groups for advanced overriding. the order follows the default config groups
83
+ # register_data()
84
+ register_model()
85
+ register_checkpoint()
86
+ register_ckpt_type()
87
+ register_optimizer()
88
+ register_scheduler()
89
+ register_callbacks()
90
+ register_tokenizer()
91
+ register_sound_tokenizer()
92
+ register_ema()
93
+ register_vlm()
94
+
95
+ # Register shipped experiments explicitly. (vision_sft_nano also defines
96
+ # vision_sft_nano_mapstyle_dataloader — the CosmosDataLoader variant — in the same module.)
97
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_droid_nano # noqa: F401
98
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva # noqa: F401
99
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva_transition_memory # noqa: F401
100
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva_pim # noqa: F401
101
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva_pim_inference # noqa: F401
102
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_libero_all_nano # noqa: F401
103
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_libero_nano # noqa: F401
104
+ import cosmos_framework.configs.base.experiment.action.posttrain_config.action_fd_droid_posttrain # noqa: F401
105
+ import cosmos_framework.configs.base.experiment.sft.vision_sft_nano # noqa: F401
106
+ import cosmos_framework.configs.base.experiment.sft.vision_sft_super # noqa: F401
107
+ import cosmos_framework.configs.base.experiment.sft.vision_sft_edge # noqa: F401
108
+
109
+ return c
releases/bridge-native-v3/code/cosmos_framework/configs/base/defaults/model_config.py ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ from typing import Any
5
+
6
+ import attrs
7
+
8
+ from cosmos_framework.utils.lazy_config import LazyDict
9
+ from cosmos_framework.configs.base.defaults.activation_checkpointing import ActivationCheckpointingConfig
10
+ from cosmos_framework.configs.base.defaults.compile import CompileConfig
11
+ from cosmos_framework.configs.base.defaults.ema import EMAConfig
12
+ from cosmos_framework.configs.base.defaults.parallelism import ParallelismConfig
13
+ from cosmos_framework.configs.base.defaults.reasoner import VLMConfig
14
+
15
+
16
+ @attrs.define(slots=False)
17
+ class DiffusionExpertConfig:
18
+ # This determines the range of timesteps before the fourier feature embedding is applied.
19
+ timestep_range: float = 1.0
20
+ # Whether to load the generation pathway weights from pretrained LLM/VLM weights.
21
+ load_weights_from_pretrained: bool = True
22
+
23
+ patch_spatial: int = 2
24
+ max_vae_latent_side_after_patchify: int = (
25
+ 20 # Max dimension (h or w) of the VAE latent after patchification (320/(8*2))
26
+ )
27
+ # Vision/action/sound position information is always provided through
28
+ # Qwen3VL-style 3D mRoPE attention IDs.
29
+ enable_fps_modulation: bool = False
30
+ base_fps: int = 24
31
+ # Base temporal compression factor for SOUND m-RoPE. None = current behavior
32
+ # (sound advances at base_fps positions/sec). Set to the vision tcf (4) to put
33
+ # sound on the same latent-frame temporal grid as vision/action.
34
+ sound_base_temporal_compression_factor: int | None = None
35
+ # Temporal coordinates used for unified_3d_mrope vision tokens.
36
+ # - "latent_index": legacy behavior, positions are 0, 1, ..., T_latent-1.
37
+ # - "uniae_source_right_edge": use UniAE padded-patch right-edge source-frame coordinates.
38
+ vision_temporal_position_mode: str = "latent_index"
39
+ # For unified_3d_mrope: whether spatial (H, W) indices reset to 0 for each vision segment
40
+ unified_3d_mrope_reset_spatial_ids: bool = True
41
+ # Setting the temporal gap on the boundary of the different modalities, default is 0, using a value greater than 0 will add an additional offset on the accumulated temporal offset.
42
+ unified_3d_mrope_temporal_modality_margin: int = 0
43
+
44
+
45
+ @attrs.define(slots=False)
46
+ class LBLConfig:
47
+ # For load balancing loss computation.
48
+ # - "local": Use the fraction of tokens routed to each expert only for the local rank.
49
+ # - "global": Use the fraction of tokens routed to each expert across all ranks.
50
+ method: str = "local"
51
+
52
+ # Coefficients for the load balancing loss.
53
+ # - "und": Coefficient for the load balancing loss for the "und" pathway.
54
+ # - "gen": Coefficient for the load balancing loss for the "gen" pathway.
55
+ coeff_und: float | None = None
56
+ coeff_gen: float | None = None
57
+
58
+
59
+ @attrs.define(slots=False)
60
+ class RectifiedFlowTrainingConfig:
61
+ shift: Any = 5 # Training time shift. If dict, maps resolution (str) to shift value (int)
62
+ use_dynamic_shift: bool = False # Whether to use dynamic shifting
63
+ train_time_image_distribution: str = "logitnormal" # Training time distribution for images
64
+ train_time_video_distribution: str = "logitnormal" # Training time distribution for videos
65
+ train_time_action_distribution: str = "logitnormal" # Training time distribution for actions
66
+ train_time_sound_distribution: str = "logitnormal" # Training time distribution for sound
67
+ train_time_weight: str = "uniform" # Training time weight
68
+ loss_scale: float = 1.0 # Loss scale
69
+ image_loss_scale: float | None = None # If set, overrides loss_scale for images
70
+ sound_loss_scale: float | None = None # If set, overrides loss_scale for sound
71
+ use_discrete_rf: bool = False # Whether to use discrete formulation of rectified flow
72
+
73
+ # user: please adjust this value according to loss_scale to balance the action loss with the video loss.
74
+ # default is 10.0 to align with previous training settings.
75
+ action_loss_weight: float = 10.0
76
+
77
+ # Independent noise schedule for action. When False (default), action shares the sigma
78
+ # sampled from the vision RF on every step — legacy behavior. When True, action samples
79
+ # its own sigma from `rectified_flow_action` using `shift_action`. Action always uses a
80
+ # shared scalar sigma per sample ([B,1]), independent of vision's DF mode.
81
+ independent_action_schedule: bool = False
82
+ shift_action: int | None = None # must be int; None → inherit `shift` (which must also be int)
83
+
84
+ # Independent noise schedule for sound. When False (default), sound shares the vision
85
+ # sigma schedule, reindexed to the dense audio-bearing subset. When True, sound samples
86
+ # its own scalar sigma per sample ([B,1]) from `rectified_flow_sound` using `shift_sound`.
87
+ independent_sound_schedule: bool = False
88
+ shift_sound: int | None = None # must be int; None → inherit `shift` (which must also be int)
89
+
90
+ # When True, per-instance flow-matching loss is normalized by the count of
91
+ # active (noisy) elements rather than all elements — preserves sum/active_count
92
+ # semantics so conditioning-heavy samples (e.g. I2V, forward_dynamics, diffusion
93
+ # forcing, AR rollout teacher-forcing) contribute gradient on par with K=0
94
+ # samples. With .mean() the gradient of a K-conditioned sample is scaled by
95
+ # (T-K)/T, which undertrains the attend-to-clean-history dynamics. Kept
96
+ # False by default to preserve legacy loss magnitudes; enable for AR/DF training.
97
+ normalize_loss_by_active: bool = False
98
+
99
+ # Sample-level (vs rank-level) loss averaging for the vision modality.
100
+ #
101
+ # By default the vision loss on each rank is a mean over that rank's samples, and
102
+ # FSDP/DDP averages gradients across ranks — so every *rank* contributes equally
103
+ # regardless of how many image/video samples it holds ("rank-level averaging").
104
+ # When ranks carry different sample counts this over-weights samples on sparse ranks.
105
+ #
106
+ # When True, the loss is renormalized so every *sample* contributes equally across
107
+ # the whole data-parallel group: each iteration all-reduces the total number of image
108
+ # and video samples over the DP group, and image and video losses are each normalized
109
+ # by their own global sample count and then summed. This counteracts the framework's
110
+ # rank-level gradient averaging so the effective objective is a true per-sample mean.
111
+ # The two independently normalized terms are weighted by the existing `image_loss_scale`
112
+ # (images; falls back to `loss_scale` when None) and `loss_scale` (videos), so their
113
+ # balance is tuned with the same knobs as the legacy rank-level path.
114
+ sample_level_loss_averaging: bool = False
115
+
116
+
117
+ @attrs.define(slots=False)
118
+ class ZevaPolicyConfig:
119
+ """Configuration for Zeva causal-context policy conditioning.
120
+
121
+ Data paths intentionally live in the dataset recipe. This model config only
122
+ describes the learned policy-injection prior and is disabled by default, preserving
123
+ every existing Cosmos recipe.
124
+ """
125
+
126
+ enabled: bool = False
127
+ global_dim: int = 256
128
+ phase_dim: int = 128
129
+ # Ordered causal observed-effect history from Stage-1 effect-v3.
130
+ effect_dim: int = 128
131
+ effect_history_length: int = 4
132
+ action_dim: int = 8
133
+ horizon: int = 32
134
+ num_anchors: int = 8
135
+ hidden_dim: int = 256
136
+ num_heads: int = 4
137
+ prior_loss_weight: float = 0.01
138
+ prior_dropout_rate: float = 0.4
139
+ prior_inference_guidance_scale: float = 0.5
140
+ global_prefix_tokens: int = 1
141
+ # Number of leading action tokens that encode a clean initial state rather
142
+ # than policy commands. DROID/RoboTwin use one; RoboCasa365 uses zero
143
+ # because its 16-D proprio state is not isomorphic to its 12-D action.
144
+ leading_condition_steps: int = 1
145
+ # Optional same-task-session transition context. The context is already
146
+ # encoded by TransitionMemoryEncoder before entering the model.
147
+ online_memory_enabled: bool = False
148
+ online_context_dim: int = 256
149
+ online_prefix_tokens: int = 1
150
+ # Paper-aligned Persistent Interaction Memory. PIM is injected as a
151
+ # gated residual into the existing global-behavior prefix, so enabling the
152
+ # module with gate_init=0 preserves the verified Stage-2 forward exactly.
153
+ pim_memory_enabled: bool = False
154
+ pim_persistent_length: int = 4
155
+ pim_context_dim: int = 256
156
+ pim_gate_init: float = 0.0
157
+ # Static inference-only kill switch. The PIM modules remain instantiated
158
+ # and load their trained weights, but the forward takes the original
159
+ # Stage-2 prefix path without executing the PIM adapter. This is stronger
160
+ # than an empty validity mask because it also preserves the compiled graph
161
+ # used by the frozen base policy.
162
+ pim_force_bypass: bool = False
163
+
164
+
165
+ # Serialized releases may still reference this fully qualified type name.
166
+ BehaviorStage2Config = ZevaPolicyConfig
167
+
168
+
169
+ @attrs.define(slots=False)
170
+ class ProprioConditionConfig:
171
+ """Optional low-dimensional robot-state prefix for action policies."""
172
+
173
+ enabled: bool = False
174
+ input_dim: int = 16
175
+ prefix_tokens: int = 1
176
+
177
+
178
+ @attrs.define(slots=False)
179
+ class RectifiedFlowInferenceConfig:
180
+ scheduler_type: str = "unipc" # Scheduler type
181
+ num_train_timesteps: int = 1000
182
+ shift: int = 1
183
+ use_dynamic_shifting: bool = False
184
+
185
+
186
+ @attrs.define(slots=False)
187
+ class FixedStepSamplerConfig:
188
+ """Config for the fixed-step sampler used by distilled models.
189
+
190
+ Uses a fixed sigma schedule instead of a smooth multi-step solver.
191
+
192
+ Mirrors the constructor args of ``FixedStepSampler``.
193
+ """
194
+
195
+ # Discrete noise-level schedule (descending, excluding the final 0.0 step).
196
+ # Convention: exclude the final 0.0 step — FixedStepSampler appends it automatically.
197
+ # Values must be descending. Using 0.999 instead of 1.0 avoids numeric edge cases at sigma=1.
198
+ t_list: list[float] = [0.999, 0.75, 0.5, 0.25]
199
+ # Distilled fixed-step sampling uses stochastic re-noising at each step.
200
+ sample_type: str = "sde"
201
+
202
+
203
+ # Don't have any defaults and init only in config file.
204
+ @attrs.define(slots=False)
205
+ class OmniMoTModelConfig:
206
+ """
207
+ Config for Omni MoT model.
208
+ """
209
+
210
+ tokenizer: LazyDict = None
211
+ load_vision_tokenizer: bool = True
212
+ """Instantiate the vision tokenizer.
213
+
214
+ Reasoner-only inference disables this to avoid loading the generation VAE.
215
+ """
216
+ net: LazyDict = None
217
+ ema: EMAConfig = EMAConfig()
218
+
219
+ # Parallelism (CP, CFGP, FSDP, DP) and FSDP reduce-dtype configuration.
220
+ parallelism: ParallelismConfig = ParallelismConfig()
221
+
222
+ # torch.compile knobs (enabled, compiled_region, dynamic, ...).
223
+ compile: CompileConfig = CompileConfig()
224
+
225
+ # Activation-checkpointing policy (trade-off between memory and speed).
226
+ activation_checkpointing: ActivationCheckpointingConfig = ActivationCheckpointingConfig()
227
+
228
+ # Model parameter / activation dtype (consumed by MixedPrecisionPolicy and
229
+ # ``model.precision`` for LowPrecisionCallback). One of "bfloat16",
230
+ # "float16", "float32".
231
+ precision: str = "bfloat16"
232
+
233
+ # LoRA (parameter-efficient fine-tuning). When `lora_enabled=True`,
234
+ # `OmniMoTModel.build_net` injects custom LoRA adapters BEFORE FSDP wrap on
235
+ # the meta-device network, then re-initializes lora_A/lora_B after
236
+ # to_empty + init_weights. Pair with `optimizer.keys_to_select=["lora_"]`
237
+ # and `checkpoint.keys_to_skip_loading=[..., "lora_"]`.
238
+ lora_enabled: bool = False
239
+ lora_rank: int = 16
240
+ lora_alpha: int = 32
241
+ lora_target_modules: str = "q_proj_moe_gen,k_proj_moe_gen,v_proj_moe_gen,o_proj_moe_gen"
242
+ # Optional base-policy teacher anchor for LoRA post-training. The teacher
243
+ # is the same frozen network with adapters temporarily disabled, so this
244
+ # does not allocate a second copy of the model. The coefficient is applied
245
+ # in the same units as action_loss_weight; zero preserves legacy behavior.
246
+ lora_teacher_anchor_weight: float = 0.0
247
+ lora_teacher_anchor_data_key: str = "teacher_anchor_weight"
248
+
249
+ # Frozen-base action correction expert. When enabled, ``build_net``
250
+ # freezes every base parameter and trains only the zero-initialized
251
+ # ``action_residual_head``. The head is never merged into the base.
252
+ residual_expert: dict[str, Any] = attrs.Factory(dict)
253
+
254
+ # R3 correction-policy controls. These are intentionally kept in the
255
+ # model config (rather than in the serving request) so an exported policy
256
+ # is a single always-on model: no phase/predicate/task router is required
257
+ # at inference time. ``event_weighted`` only changes the training loss.
258
+ r3_event_weighted: bool = False
259
+ r3_event_weight: float = 2.0
260
+ r3_task_weight_key: str = "r3_task_weight"
261
+ r3_event_weight_key: str = "residual_release_event"
262
+
263
+ # Rectified flow configs
264
+ rectified_flow_training_config: RectifiedFlowTrainingConfig = RectifiedFlowTrainingConfig()
265
+ rectified_flow_inference_config: RectifiedFlowInferenceConfig = RectifiedFlowInferenceConfig()
266
+ # The field name is serialized in released configs; the type and all new
267
+ # public APIs use the Zeva paper terminology.
268
+ behavior_stage2: ZevaPolicyConfig = ZevaPolicyConfig()
269
+ proprio_condition: ProprioConditionConfig = ProprioConditionConfig()
270
+
271
+ # Optional fixed-step sampler for distilled models (None for base models).
272
+ fixed_step_sampler_config: FixedStepSamplerConfig | None = None
273
+
274
+ # Model configs
275
+ vlm_config: VLMConfig = VLMConfig()
276
+ diffusion_expert_config: DiffusionExpertConfig = DiffusionExpertConfig()
277
+ # Training data keys
278
+ input_video_key: str = "video"
279
+ input_image_key: str = "images" # key to fetch input image from data_batch
280
+ input_caption_key: str = "ai_caption" # Key used to fetch input captions
281
+
282
+ # State and sequence shapes
283
+ state_ch: int = 16 # for latent model, ref to the latent channel number
284
+ state_t: int = 8 # for latent model, ref to the latent number of frames
285
+ latent_downsample_factor: int = 8
286
+ resolution: str = "512"
287
+ max_num_tokens_after_packing: int = 13312 # Final num tokens after sequence packing
288
+
289
+ # Attention implementation for joint understanding + generation
290
+ # Note "two_way" and "three_way" disallow and remove "End-of-Vision" or other text token in the generation tower.
291
+ # "three_way" must only be used when introducing sparsity
292
+ joint_attn_implementation: str = "two_way" # "two_way" or "three_way"
293
+
294
+ # Per-layer NATTEN parameters
295
+ # Must use "three_way" attention if used.
296
+ # If None, all attention layers remain dense.
297
+ # If not None, must be a list exactly the size of number of layers, and each layer can be either
298
+ # None (dense) or a dictionary, with at least 'kernel_size' or 'kernel_size_float' keys
299
+ # specifying sparsity. NATTEN parameters 'dilation' and 'stride' may also be specified either as
300
+ # static integers, or as floating point values that will be mapped to their domain during
301
+ # runtime. Integer parameters should never be mixed with floating point ones.
302
+ #
303
+ # Floating point parameters are highly recommended, unless the use case will have a fixed token
304
+ # layout (input resolution).
305
+ #
306
+ # Examples:
307
+ # Interleaved sliding window layers, "GPT-OSS"-style, with static window size:
308
+ # natten_parameter_list = [None if layer_idx % 2 != 0 else {"kernel_size": (8, 8)}]
309
+ # Layers with odd indices ("None"s) will use dense attention, and layers with an even indices
310
+ # will use a static sliding window size of 8x8.
311
+ #
312
+ # Interleaved sliding window layers, "GPT-OSS"-style, with input-dependent window size:
313
+ # natten_parameter_list = [None if layer_idx % 2 != 0 else {"kernel_size_float": (0.5, 0.5)}]
314
+ # Layers with odd indices ("None"s) will use dense attention, and layers with an even indices
315
+ # will use a dynamic window size that is 50% of the input along each of the two dimensions.
316
+ #
317
+ # Interleaved sliding window and dilated layers, "DiNAT"-style:
318
+ # natten_parameter_list = [
319
+ # {
320
+ # "kernel_size_float": (0.5, 0.5),
321
+ # "dilation_float": (1.0, 1.0),
322
+ # } if layer_idx % 2 != 0 else {
323
+ # "kernel_size_float": (0.5, 0.5),
324
+ # }
325
+ # ]
326
+ # All layers will use a dynamic window size that is 50% of the input along each of the two
327
+ # dimensions. Layers with odd indices will also dilate to the maximum level possible.
328
+ #
329
+ natten_parameter_list: list | None = None
330
+
331
+ # Temporal causality for training autoregressive video generation models.
332
+ # When enabled, applies temporal causal attention to generation supertokens.
333
+ # Each supertoken is num_action_tokens_per_supertoken action tokens followed
334
+ # by H*W vision tokens; the value is stamped onto the packed sequence by the
335
+ # temporal-causal packer and read by attention/KV-cache code unchanged.
336
+ # Only supports image2video modes (with or without actions).
337
+ # Requires joint_attn_implementation="three_way".
338
+ video_temporal_causal: bool = False
339
+ # "none": standard joint denoising (shared σ, no clean context)
340
+ # "teacher_forcing": all frames noised with shared σ; clean history via cross-attention
341
+ # "diffusion_forcing": each latent frame gets independent σ ~ Uniform[0,1]
342
+ # "teacher_forcing_dcm": replayed teacher-forcing discrete-time consistency distillation
343
+ causal_training_strategy: str = attrs.field(
344
+ default="none",
345
+ validator=attrs.validators.in_({"none", "teacher_forcing", "diffusion_forcing", "teacher_forcing_dcm"}),
346
+ )
347
+
348
+ # Load balancing loss config.
349
+ lbl: LBLConfig = LBLConfig()
350
+
351
+ # vision configs
352
+ vision_gen: bool = True # whether to use vision related parameters and condition/generate vision tokens
353
+
354
+ # action configs
355
+ action_gen: bool = False # whether to use action related parameters and condition/generate action tokens
356
+ max_action_dim: int = 32 # maximum dimension of the action space, we need to pad the data to this dimension.
357
+ num_embodiment_domains: int = 32 # number of domains for the domain-aware linear layer
358
+
359
+ # sound configs
360
+ sound_gen: bool = False # whether to use sound related parameters and condition/generate sound tokens
361
+ sound_tokenizer: LazyDict | None = None # Sound tokenizer config (e.g., AVAE)
362
+ sound_dim: int | None = None # Sound latent channel size (e.g., 64 for AVAE 48kHz)
363
+ sound_latent_fps: int = 25 # Sound tokenizer's latent rate (e.g., 48kHz / 1920 hop = 25 Hz)
364
+
365
+ # When False, removes bias from vae2llm, sound2llm, and the two Linear layers inside
366
+ # time_embedder. These biases seem to inject token-constant DC offsets that dominate
367
+ # the MoE router input and create prompt invariant routing. This is observed empirically
368
+ # in the 30B-A3B checkpoint.
369
+ enable_input_bias: bool = True
370
+
371
+ log_enc_time_every_n: int = 100 # Frequency of logging encoding time to W&B
372
+
373
+ # When True, ``OmniMoTModel.state_dict`` / ``load_state_dict`` skip the
374
+ # reasoner (und) pathway weights under ``language_model`` — i.e. every key
375
+ # WITHOUT a ``_moe_gen`` suffix (including ``visual`` / ``lm_head`` /
376
+ # ``embed_tokens``). These are not written to checkpoints and are left
377
+ # untouched on load (typically already populated from the HF pretrained
378
+ # backbone). Generation-pathway (``_moe_gen``) and VFM heads are saved /
379
+ # loaded as usual.
380
+ exclude_reasoner_weights_from_checkpoint: bool = False
releases/bridge-native-v3/code/cosmos_framework/configs/base/defaults/vlm.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compatibility shim: behavior branch renamed defaults/vlm -> defaults/reasoner.
2
+ Hydra/export may still locate class paths under defaults.vlm.* for checkpoints
3
+ serialized before the rename; forward them to the reasoner definitions.
4
+ """
5
+ from cosmos_framework.configs.base.defaults.reasoner import (
6
+ PretrainedWeightsConfig,
7
+ VLMConfig,
8
+ create_qwen2_tokenizer_with_download,
9
+ create_vlm_config,
10
+ )
11
+
12
+ __all__ = [
13
+ "PretrainedWeightsConfig",
14
+ "VLMConfig",
15
+ "create_qwen2_tokenizer_with_download",
16
+ "create_vlm_config",
17
+ ]
releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Inference recipe for the fixed-base RoboCasa Atomic-5 Zeva policy."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+
10
+ from hydra.core.config_store import ConfigStore
11
+
12
+ from cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_droid_nano import (
13
+ action_policy_droid_nano,
14
+ )
15
+
16
+ cs = ConfigStore.instance()
17
+
18
+ action_policy_robocasa365_atomic5_zeva = copy.deepcopy(action_policy_droid_nano)
19
+ action_policy_robocasa365_atomic5_zeva["job"].update(
20
+ project="zeva",
21
+ group="zeva_robocasa_atomic5",
22
+ name="action_policy_robocasa365_atomic5_zeva",
23
+ )
24
+ action_policy_robocasa365_atomic5_zeva["model"]["config"]["behavior_stage2"] = dict(
25
+ enabled=True,
26
+ global_dim=256,
27
+ phase_dim=128,
28
+ effect_dim=128,
29
+ effect_history_length=4,
30
+ action_dim=7,
31
+ horizon=32,
32
+ num_anchors=8,
33
+ hidden_dim=256,
34
+ num_heads=4,
35
+ prior_loss_weight=0.01,
36
+ prior_dropout_rate=0.4,
37
+ prior_inference_guidance_scale=0.5,
38
+ global_prefix_tokens=1,
39
+ leading_condition_steps=0,
40
+ )
41
+ action_policy_robocasa365_atomic5_zeva["model"]["config"]["proprio_condition"] = dict(
42
+ enabled=True,
43
+ input_dim=9,
44
+ prefix_tokens=1,
45
+ )
46
+ # This is a post-trained Zeva checkpoint, not a fresh DROID initialization.
47
+ # Load every trained policy/action tensor; only the unused EMA namespace may be
48
+ # absent. Inheriting DROID's action-head skip list silently changes inference.
49
+ action_policy_robocasa365_atomic5_zeva["checkpoint"]["keys_to_skip_loading"] = ["net_ema."]
50
+ # The server supplies Qwen explicitly; retain the same environment contract as
51
+ # the training recipe so checkpoint metadata and manual startup agree.
52
+ action_policy_robocasa365_atomic5_zeva["model"]["config"]["vlm_config"]["tokenizer"][
53
+ "pretrained_model_name"
54
+ ] = "${oc.env:QWEN_VLM_PATH}"
55
+
56
+ # Serving does not require training dataloaders.
57
+ action_policy_robocasa365_atomic5_zeva["dataloader_train"] = None
58
+ action_policy_robocasa365_atomic5_zeva["dataloader_val"] = None
59
+
60
+ cs.store(
61
+ group="experiment",
62
+ package="_global_",
63
+ name="action_policy_robocasa365_atomic5_zeva",
64
+ node=action_policy_robocasa365_atomic5_zeva,
65
+ )
releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_pim.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Frozen-policy PIM training for RoboCasa Atomic-5 Zeva."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+
10
+ from hydra.core.config_store import ConfigStore
11
+
12
+ from cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva import (
13
+ action_policy_robocasa365_atomic5_zeva,
14
+ )
15
+
16
+ cs = ConfigStore.instance()
17
+
18
+ action_policy_robocasa365_atomic5_zeva_pim = copy.deepcopy(
19
+ action_policy_robocasa365_atomic5_zeva
20
+ )
21
+ action_policy_robocasa365_atomic5_zeva_pim["job"].update(
22
+ project="zeva",
23
+ group="pim_adapter_robocasa_atomic5",
24
+ name="action_policy_robocasa365_atomic5_zeva_pim",
25
+ )
26
+ zeva_policy = action_policy_robocasa365_atomic5_zeva_pim["model"]["config"]["behavior_stage2"]
27
+ zeva_policy.update(
28
+ pim_memory_enabled=True,
29
+ pim_persistent_length=4,
30
+ pim_context_dim=256,
31
+ pim_gate_init=0.0,
32
+ )
33
+
34
+ # Train only the Causal Prompt encoder, projection, and scalar gate.
35
+ keys = action_policy_robocasa365_atomic5_zeva_pim["optimizer"]["keys_to_select"]
36
+ keys[:] = ["behavior_pim_encoder", "behavior_pim_projector", "behavior_pim_gate"]
37
+ action_policy_robocasa365_atomic5_zeva_pim["optimizer"]["lr_multipliers"].update(
38
+ behavior_pim_encoder=5.0,
39
+ behavior_pim_projector=5.0,
40
+ behavior_pim_gate=1.0,
41
+ )
42
+ action_policy_robocasa365_atomic5_zeva_pim["checkpoint"]["keys_to_skip_loading"] += [
43
+ "behavior_pim_encoder",
44
+ "behavior_pim_projector",
45
+ "behavior_pim_gate",
46
+ ]
47
+
48
+ train_loader = action_policy_robocasa365_atomic5_zeva_pim.get("dataloader_train")
49
+ if train_loader is not None:
50
+ dataset = train_loader["dataloader"]["datasets"]["robocasa365"]["dataset"]
51
+ dataset["behavior_pim_training_bank"] = "${oc.env:ZEVA_PIM_TRAINING_BANK}"
52
+ dataset["behavior_pim_history_length"] = 4
53
+ dataset["behavior_pim_context_dropout"] = 0.2
54
+ dataset["behavior_pim_support_dropout"] = 0.2
55
+
56
+ cs.store(
57
+ group="experiment",
58
+ package="_global_",
59
+ name="action_policy_robocasa365_atomic5_zeva_pim",
60
+ node=action_policy_robocasa365_atomic5_zeva_pim,
61
+ )
releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_pim_inference.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Inference config that loads trained PIM weights instead of warm-start skipping them."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+
10
+ from hydra.core.config_store import ConfigStore
11
+
12
+ from cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva_pim import (
13
+ action_policy_robocasa365_atomic5_zeva_pim,
14
+ )
15
+
16
+ cs = ConfigStore.instance()
17
+
18
+ action_policy_robocasa365_atomic5_zeva_pim_inference = copy.deepcopy(
19
+ action_policy_robocasa365_atomic5_zeva_pim
20
+ )
21
+ action_policy_robocasa365_atomic5_zeva_pim_inference["job"]["name"] = (
22
+ "action_policy_robocasa365_atomic5_zeva_pim_inference"
23
+ )
24
+ # The training recipe skips these keys only because its input checkpoint is the
25
+ # old Stage-2 model. A trained PIM DCP contains them and inference must load
26
+ # them. Keep the established net_ema exclusion used by the Stage-2 release.
27
+ action_policy_robocasa365_atomic5_zeva_pim_inference["checkpoint"][
28
+ "keys_to_skip_loading"
29
+ ] = ["net_ema."]
30
+
31
+ cs.store(
32
+ group="experiment",
33
+ package="_global_",
34
+ name="action_policy_robocasa365_atomic5_zeva_pim_inference",
35
+ node=action_policy_robocasa365_atomic5_zeva_pim_inference,
36
+ )
releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_test.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva import (
2
+ action_policy_robocasa365_atomic5_zeva,
3
+ )
4
+
5
+
6
+ def test_released_stage2_loads_trained_action_heads() -> None:
7
+ assert action_policy_robocasa365_atomic5_zeva["checkpoint"][
8
+ "keys_to_skip_loading"
9
+ ] == ["net_ema."]
10
+
11
+
12
+ def test_inference_recipe_does_not_construct_training_loaders() -> None:
13
+ assert action_policy_robocasa365_atomic5_zeva["dataloader_train"] is None
14
+ assert action_policy_robocasa365_atomic5_zeva["dataloader_val"] is None
releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_transition_memory.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Experimental transition-memory adaptation for the RoboCasa Atomic-5 Zeva policy.
5
+
6
+ The CTE path and frozen diffusion policy are loaded from the verified
7
+ Atomic-5 Stage-2 checkpoint and frozen. The separately trained
8
+ history-only ``TransitionMemoryEncoder`` is consumed through the cached
9
+ ``behavior_online_context`` token; this recipe trains the zero-init Cosmos
10
+ transition-memory projector so that the new context affects only the action branch.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import copy
16
+
17
+ from hydra.core.config_store import ConfigStore
18
+
19
+ from cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa365_atomic5_zeva import (
20
+ action_policy_robocasa365_atomic5_zeva,
21
+ )
22
+
23
+ cs = ConfigStore.instance()
24
+
25
+ action_policy_robocasa365_atomic5_zeva_transition_memory = copy.deepcopy(
26
+ action_policy_robocasa365_atomic5_zeva
27
+ )
28
+ action_policy_robocasa365_atomic5_zeva_transition_memory["job"].update(
29
+ project="zeva",
30
+ group="transition_memory_robocasa_atomic5",
31
+ name="action_policy_robocasa365_atomic5_zeva_transition_memory",
32
+ )
33
+ zeva_policy = action_policy_robocasa365_atomic5_zeva_transition_memory["model"]["config"]["behavior_stage2"]
34
+ zeva_policy.update(
35
+ online_memory_enabled=True,
36
+ online_context_dim=256,
37
+ online_prefix_tokens=1,
38
+ )
39
+
40
+ # The already trained policy-injection prior and task-context projector are deliberately not added
41
+ # again. Only the new zero-init Cosmos action-branch projector is optimized;
42
+ # the query/key/value/history encoder is trained by the preceding
43
+ # history-only recipe and loaded into the context cache.
44
+ keys = action_policy_robocasa365_atomic5_zeva_transition_memory["optimizer"]["keys_to_select"]
45
+ # Stage-2 checkpoint parameters and the Cosmos backbone must remain frozen in
46
+ # this adaptation. Replacing (rather than subtracting from) the inherited
47
+ # selector also prevents the base RoboCasa recipe's action/proprio modules from
48
+ # being silently optimized.
49
+ keys[:] = ["behavior_online_projector"]
50
+ action_policy_robocasa365_atomic5_zeva_transition_memory["optimizer"]["lr_multipliers"].update(
51
+ behavior_online_projector=5.0,
52
+ )
53
+
54
+ train_loader = action_policy_robocasa365_atomic5_zeva_transition_memory.get("dataloader_train")
55
+ if train_loader is not None:
56
+ dataset = train_loader["dataloader"]["datasets"]["robocasa365"]["dataset"]
57
+ dataset["behavior_memory_bank"] = "${oc.env:ZEVA_TASK_CONTEXT_BANK}"
58
+ dataset["behavior_phase_cache"] = "${oc.env:ZEVA_CTE_FEATURE_CACHE}"
59
+ dataset["behavior_online_context_cache"] = "${oc.env:ZEVA_TRANSITION_CONTEXT_CACHE}"
60
+
61
+ cs.store(
62
+ group="experiment",
63
+ package="_global_",
64
+ name="action_policy_robocasa365_atomic5_zeva_transition_memory",
65
+ node=action_policy_robocasa365_atomic5_zeva_transition_memory,
66
+ )
releases/bridge-native-v3/code/cosmos_framework/data/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from pkgutil import extend_path
2
+ __path__ = extend_path(__path__, __name__)
releases/bridge-native-v3/code/cosmos_framework/data/generator/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from pkgutil import extend_path
2
+ __path__ = extend_path(__path__, __name__)
releases/bridge-native-v3/code/cosmos_framework/data/generator/action/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from pkgutil import extend_path
2
+ __path__ = extend_path(__path__, __name__)
releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pkgutil import extend_path
2
+ __path__ = extend_path(__path__, __name__)
3
+
4
+ # Preserve the upstream package's public dataset exports.
5
+ from .agibotworld_beta_lerobot_dataset import AgiBotWorldBetaLeRobotDataset
6
+ from .base_dataset import ActionBaseDataset
7
+ from .bridge_orig_lerobot_dataset import BridgeOrigLeRobotDataset
8
+ from .droid_merged_lerobot_dataset import DROIDMergedLeRobotDataset
9
+ from .droid_lerobot_dataset import DROIDLeRobotDataset
10
+ from .fractal_lerobot_dataset import FractalLeRobotDataset
11
+ from .human_hand_pose_lerobot_dataset import HumanHandPoseLeRobotDataset
12
+ from .libero_lerobot_dataset import LIBEROLeRobotDataset
13
+ from .robomind_franka_dataset import RoboMINDFrankaDataset
14
+ from .robomind_ur_dataset import RoboMINDURDataset
15
+ from .umi_lerobot_dataset import UMILeRobotDataset
releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/robocasa365_lerobot_dataset.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Read-only RoboCasa365 LeRobot-v2.1 adapter for Cosmos3 Action.
5
+
6
+ The target set is a directory of 50 independent LeRobot roots. Current
7
+ LeRobot rejects v2.1 metadata, so this reader indexes the immutable JSONL and
8
+ per-episode parquet files directly while using LeRobot's torchcodec decoder.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import bisect
14
+ import json
15
+ import random
16
+ import re
17
+ from collections import OrderedDict
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any, Sequence
21
+
22
+ import pyarrow.parquet as pq
23
+ import torch
24
+ import torch.nn.functional as F
25
+ from lerobot.datasets.video_utils import decode_video_frames
26
+ from torch.utils.data import Dataset
27
+
28
+ from cosmos_framework.data.generator.action.action_processing import ActionNormalizer, resolve_action_normalization
29
+ from cosmos_framework.data.generator.action.datasets.cosmos3_action_lerobot import split_episode_ids
30
+ from cosmos_framework.data.generator.action.domain_utils import get_domain_id
31
+
32
+ _CAMERAS = {
33
+ "left": "observation.images.robot0_agentview_left",
34
+ "right": "observation.images.robot0_agentview_right",
35
+ "wrist": "observation.images.robot0_eye_in_hand",
36
+ }
37
+
38
+ _CAMERA_LAYOUTS = {
39
+ "three_view_grid": ("left", "right", "wrist"),
40
+ "left_wrist_horizontal": ("left", "wrist"),
41
+ }
42
+
43
+ _ACTION_INDICES = {
44
+ "full12": tuple(range(12)),
45
+ # RoboCasa365 layout: base xy-yaw-height (0:4), control mode (4),
46
+ # end-effector delta pose + gripper (5:12).
47
+ "arm7": tuple(range(5, 12)),
48
+ }
49
+
50
+ _STATE_INDICES = {
51
+ "full16": tuple(range(16)),
52
+ # RoboCasa365 state: base pose (0:7), relative end-effector pose (7:14),
53
+ # gripper qpos (14:16). Fixed-base policies keep only the latter 9 values.
54
+ "arm9": tuple(range(7, 16)),
55
+ }
56
+
57
+
58
+ def _humanize_task(task: str) -> str:
59
+ words = re.sub(r"(?<!^)(?=[A-Z])", " ", task).strip()
60
+ return (words[:1].upper() + words[1:].lower() + ".") if words else task
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class RoboCasa365Episode:
65
+ root: Path
66
+ category: str
67
+ task_name: str
68
+ episode_id: int
69
+ length: int
70
+ command: str
71
+ parquet_path: Path
72
+ video_paths: dict[str, Path]
73
+
74
+
75
+ class RoboCasa365LeRobotDataset(Dataset):
76
+ """Cosmos policy windows over the 50-task RoboCasa365 target dataset."""
77
+
78
+ EMBODIMENT_TYPE = "robocasa-panda-omron"
79
+
80
+ def __init__(
81
+ self,
82
+ root: str,
83
+ *,
84
+ fps: float = 20.0,
85
+ chunk_length: int = 32,
86
+ split: str = "train",
87
+ split_seed: int = 42,
88
+ split_val_ratio: float = 0.03,
89
+ mode: str = "wam",
90
+ viewpoint: str = "concat_view",
91
+ use_state: bool = False,
92
+ use_image_augmentation: bool = False,
93
+ emit_behavior_metadata: bool = False,
94
+ task_set: str = "target50",
95
+ task_names: Sequence[str] | None = None,
96
+ camera_layout: str = "three_view_grid",
97
+ action_mode: str = "full12",
98
+ state_mode: str = "full16",
99
+ action_normalization: str | None = "minmax",
100
+ action_stats_path: str | None = None,
101
+ parquet_cache_size: int = 8,
102
+ max_roots: int | None = None,
103
+ ) -> None:
104
+ if viewpoint not in {"concat_view", "third_person_view"}:
105
+ raise ValueError(f"Unsupported RoboCasa365 viewpoint={viewpoint!r}")
106
+ if split not in {"train", "val", "full"}:
107
+ raise ValueError("split must be train, val or full")
108
+ if task_set not in {"target50", "atomic", "composite", "composite_seen", "composite_unseen"}:
109
+ raise ValueError(f"Unsupported RoboCasa365 task_set={task_set!r}")
110
+ if camera_layout not in _CAMERA_LAYOUTS:
111
+ raise ValueError(f"Unsupported RoboCasa365 camera_layout={camera_layout!r}")
112
+ if action_mode not in _ACTION_INDICES:
113
+ raise ValueError(f"Unsupported RoboCasa365 action_mode={action_mode!r}")
114
+ if state_mode not in _STATE_INDICES:
115
+ raise ValueError(f"Unsupported RoboCasa365 state_mode={state_mode!r}")
116
+ self.root = Path(root)
117
+ self.fps = float(fps)
118
+ self.chunk_length = int(chunk_length)
119
+ self.split = split
120
+ self.split_seed = int(split_seed)
121
+ self.split_val_ratio = float(split_val_ratio)
122
+ self.mode = mode
123
+ self.viewpoint = viewpoint
124
+ self.use_state = bool(use_state)
125
+ self.use_image_augmentation = bool(use_image_augmentation)
126
+ self.emit_behavior_metadata = bool(emit_behavior_metadata)
127
+ self.task_set = task_set
128
+ self.task_names = frozenset(task_names) if task_names is not None else None
129
+ self.camera_layout = camera_layout
130
+ self.action_mode = action_mode
131
+ self.action_indices = _ACTION_INDICES[action_mode]
132
+ self.state_mode = state_mode
133
+ self.state_indices = _STATE_INDICES[state_mode]
134
+ self.camera_names = _CAMERA_LAYOUTS[camera_layout]
135
+ self.domain_id = get_domain_id(self.EMBODIMENT_TYPE)
136
+ self.action_dim = len(self.action_indices)
137
+ self._parquet_cache_size = int(parquet_cache_size)
138
+ self._parquet_cache: OrderedDict[Path, dict[str, torch.Tensor]] = OrderedDict()
139
+ self._normalizer: ActionNormalizer | None = None
140
+ if action_normalization is not None:
141
+ stats_path = (
142
+ Path(action_stats_path)
143
+ if action_stats_path
144
+ else Path(__file__).parents[1] / "normalizer_stats" / "robocasa365_target_action_stats.json"
145
+ )
146
+ if not stats_path.is_file():
147
+ raise FileNotFoundError(
148
+ "RoboCasa365 action normalization requires the target50 global action statistics; "
149
+ f"missing {stats_path}"
150
+ )
151
+ raw = json.loads(stats_path.read_text())
152
+ raw = raw.get("action", raw)
153
+ index = torch.tensor(self.action_indices, dtype=torch.long)
154
+ stats = {
155
+ key: torch.tensor(raw[key], dtype=torch.float32).index_select(0, index)
156
+ for key in ("min", "max", "mean", "std", "q01", "q99")
157
+ }
158
+ self._normalizer = resolve_action_normalization(action_normalization, stats)
159
+
160
+ roots = sorted(path.parent.parent for path in self.root.glob("*/*/*/lerobot/meta/info.json"))
161
+ roots = [path for path in roots if self._include_root(path)]
162
+ if max_roots is not None:
163
+ roots = roots[:max_roots]
164
+ if not roots:
165
+ raise FileNotFoundError(f"No RoboCasa365 LeRobot-v2.1 roots under {self.root}")
166
+ self.episodes: list[RoboCasa365Episode] = []
167
+ for root_path in roots:
168
+ self._index_root(root_path)
169
+ # A length-H command needs H action rows and H+1 observation frames.
170
+ self._window_lengths = [max(0, episode.length - self.chunk_length) for episode in self.episodes]
171
+ self._cum_ends: list[int] = []
172
+ total = 0
173
+ for length in self._window_lengths:
174
+ total += length
175
+ self._cum_ends.append(total)
176
+ self._num_windows = total
177
+
178
+ def _include_root(self, path: Path) -> bool:
179
+ category, task_name = path.relative_to(self.root).parts[:2]
180
+ if self.task_names is not None and task_name not in self.task_names:
181
+ return False
182
+ if self.task_set == "atomic":
183
+ return category == "atomic"
184
+ if self.task_set == "composite":
185
+ return category == "composite"
186
+ if self.task_set == "composite_seen":
187
+ return category == "composite" and task_name not in _COMPOSITE_UNSEEN
188
+ if self.task_set == "composite_unseen":
189
+ return category == "composite" and task_name in _COMPOSITE_UNSEEN
190
+ return True
191
+
192
+ def _index_root(self, path: Path) -> None:
193
+ info = json.loads((path / "meta" / "info.json").read_text())
194
+ if info["codebase_version"] != "v2.1" or info["fps"] != self.fps:
195
+ raise ValueError(f"Unexpected RoboCasa365 metadata in {path}")
196
+ category, task_name = path.relative_to(self.root).parts[:2]
197
+ records = [json.loads(line) for line in (path / "meta" / "episodes.jsonl").read_text().splitlines()]
198
+ selected = split_episode_ids(len(records), self.split_seed, self.split_val_ratio, self.split)
199
+ selected.sort()
200
+ for episode_id in selected:
201
+ record = records[episode_id]
202
+ length = int(record["length"])
203
+ if length <= self.chunk_length:
204
+ continue
205
+ command = str(record.get("tasks", [""])[0]).strip() or _humanize_task(task_name)
206
+ video_paths = {
207
+ name: path / f"videos/chunk-{episode_id // 1000:03d}/{key}/episode_{episode_id:06d}.mp4"
208
+ for name, key in _CAMERAS.items()
209
+ if name in self.camera_names
210
+ }
211
+ parquet_path = path / f"data/chunk-{episode_id // 1000:03d}/episode_{episode_id:06d}.parquet"
212
+ if not parquet_path.is_file() or any(not video.is_file() for video in video_paths.values()):
213
+ raise FileNotFoundError(f"Incomplete RoboCasa365 episode {path}#{episode_id}")
214
+ self.episodes.append(
215
+ RoboCasa365Episode(path, category, task_name, episode_id, length, command, parquet_path, video_paths)
216
+ )
217
+
218
+ def __len__(self) -> int:
219
+ return self._num_windows
220
+
221
+ def get_shuffle_blocks(self) -> list[tuple[int, int]]:
222
+ blocks: list[tuple[int, int]] = []
223
+ start = 0
224
+ for length in self._window_lengths:
225
+ blocks.append((start, length))
226
+ start += length
227
+ return blocks
228
+
229
+ def _resolve_index(self, index: int) -> tuple[int, int]:
230
+ if index < 0:
231
+ index += len(self)
232
+ if not 0 <= index < len(self):
233
+ raise IndexError(index)
234
+ episode_index = bisect.bisect_right(self._cum_ends, index)
235
+ start = 0 if episode_index == 0 else self._cum_ends[episode_index - 1]
236
+ return episode_index, index - start
237
+
238
+ def _load_low_dim(self, episode: RoboCasa365Episode) -> dict[str, torch.Tensor]:
239
+ cached = self._parquet_cache.pop(episode.parquet_path, None)
240
+ if cached is None:
241
+ table = pq.read_table(episode.parquet_path, columns=["action", "observation.state"])
242
+ payload = table.to_pydict()
243
+ cached = {
244
+ "action": torch.tensor(payload["action"], dtype=torch.float32),
245
+ "state": torch.tensor(payload["observation.state"], dtype=torch.float32),
246
+ }
247
+ self._parquet_cache[episode.parquet_path] = cached
248
+ while len(self._parquet_cache) > self._parquet_cache_size:
249
+ self._parquet_cache.popitem(last=False)
250
+ return cached
251
+
252
+ def _decode(self, path: Path, start: int) -> torch.Tensor:
253
+ timestamps = [(start + offset) / self.fps for offset in range(self.chunk_length + 1)]
254
+ return decode_video_frames(path, timestamps, tolerance_s=2e-4, backend="torchcodec")
255
+
256
+ def _compose_video(self, episode: RoboCasa365Episode, start: int) -> torch.Tensor:
257
+ left = self._decode(episode.video_paths["left"], start)
258
+ if self.viewpoint == "third_person_view":
259
+ return left
260
+ wrist = self._decode(episode.video_paths["wrist"], start)
261
+ if self.camera_layout == "left_wrist_horizontal":
262
+ if self.use_image_augmentation:
263
+ brightness = random.uniform(0.85, 1.15)
264
+ contrast = random.uniform(0.85, 1.15)
265
+ combined = torch.cat((left, wrist), dim=0)
266
+ mean = combined.mean(dim=(-2, -1), keepdim=True)
267
+ combined = ((combined - mean) * contrast + mean).mul(brightness).clamp(0, 1)
268
+ left, wrist = combined.chunk(2, dim=0)
269
+ # Preserve both native 256x256 views: agent-left | wrist.
270
+ return torch.cat((left, wrist), dim=-1)
271
+ right = self._decode(episode.video_paths["right"], start)
272
+ if self.use_image_augmentation:
273
+ brightness = random.uniform(0.85, 1.15)
274
+ contrast = random.uniform(0.85, 1.15)
275
+ combined = torch.cat((left, right, wrist), dim=0)
276
+ mean = combined.mean(dim=(-2, -1), keepdim=True)
277
+ combined = ((combined - mean) * contrast + mean).mul(brightness).clamp(0, 1)
278
+ n = left.shape[0]
279
+ left, right, wrist = combined[:n], combined[n : 2 * n], combined[2 * n :]
280
+ right = F.interpolate(right, size=(128, 128), mode="bilinear", align_corners=False)
281
+ wrist = F.interpolate(wrist, size=(128, 128), mode="bilinear", align_corners=False)
282
+ # Left agent view on top, right agent + wrist views in the bottom row.
283
+ return torch.cat((left, torch.cat((right, wrist), dim=-1)), dim=-2)
284
+
285
+ def get_action_normalizer(self, _sample: dict[str, Any] | None = None) -> ActionNormalizer | None:
286
+ return self._normalizer
287
+
288
+ def __getitem__(self, index: int) -> dict[str, Any]:
289
+ episode_index, frame_offset = self._resolve_index(index)
290
+ episode = self.episodes[episode_index]
291
+ low_dim = self._load_low_dim(episode)
292
+ action = low_dim["action"][frame_offset : frame_offset + self.chunk_length, self.action_indices]
293
+ if self.use_state:
294
+ # State and action have different physical layouts (16-D vs 12-D),
295
+ # so expose state separately instead of pretending it is an action.
296
+ initial_state = low_dim["state"][frame_offset, self.state_indices]
297
+ else:
298
+ initial_state = None
299
+ video = self._compose_video(episode, frame_offset)
300
+ result: dict[str, Any] = {
301
+ "ai_caption": episode.command,
302
+ "video": (video * 255.0).clamp(0, 255).to(torch.uint8).permute(1, 0, 2, 3),
303
+ "action": action,
304
+ "conditioning_fps": torch.tensor(int(self.fps), dtype=torch.long),
305
+ "mode": self.mode,
306
+ "domain_id": torch.tensor(self.domain_id, dtype=torch.long),
307
+ "viewpoint": self.viewpoint,
308
+ "additional_view_description": (
309
+ "The left panel is the left agent view and the right panel is the wrist view."
310
+ if self.camera_layout == "left_wrist_horizontal"
311
+ else "The top panel is the left agent view. The bottom-left panel is the right agent view, "
312
+ "and the bottom-right panel is the wrist view."
313
+ ),
314
+ "task_cluster": episode.task_name,
315
+ "task_category": episode.category,
316
+ }
317
+ if initial_state is not None:
318
+ result["proprio"] = initial_state
319
+ if self.emit_behavior_metadata:
320
+ # Keep ordinal source_index for diagnostics; resolve the canonical
321
+ # Stage-1 key from task + episode_id in the behavior store.
322
+ result["behavior_source_index"] = torch.tensor(episode_index, dtype=torch.long)
323
+ result["behavior_episode_id"] = torch.tensor(episode.episode_id, dtype=torch.long)
324
+ result["behavior_task_cluster"] = episode.task_name
325
+ result["behavior_frame_offset"] = torch.tensor(frame_offset, dtype=torch.long)
326
+ return result
327
+
328
+
329
+ _COMPOSITE_UNSEEN = {
330
+ "ArrangeBreadBasket", "ArrangeTea", "BreadSelection", "CategorizeCondiments",
331
+ "CuttingToolSelection", "GarnishPancake", "GatherTableware", "HeatKebabSandwich",
332
+ "MakeIceLemonade", "PanTransfer", "PortionHotDogs", "RecycleBottlesByType",
333
+ "SeparateFreezerRack", "WaffleReheat", "WashFruitColander", "WeighIngredients",
334
+ }
releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/widowx_bridge_v3_dataset.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small, self-contained WidowX/Bridge LeRobot-v3 dataset adapter.
2
+
3
+ The smoke export is deliberately kept independent from the legacy LeRobot
4
+ parquet layout used by the upstream Cosmos loaders. This adapter is also
5
+ useful for quick Zeva policy experiments and returns normalized 7-D Bridge
6
+ actions plus the 9-D proprioception contract used by the WidowX server.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Dict
14
+ from collections import OrderedDict
15
+
16
+ import av
17
+ import numpy as np
18
+ import pyarrow.parquet as pq
19
+ import torch
20
+ from torch.utils.data import Dataset
21
+
22
+ Q01 = np.asarray([-0.028752606511116028, -0.041702210046350954, -0.026096698231995105,
23
+ -0.08052822157740593, -0.09249736212193965, -0.20738411962985992], np.float32)
24
+ Q99 = np.asarray([0.02830690816044803, 0.04089860741049051, 0.04018005654215808,
25
+ 0.08173405691981314, 0.07760896608233431, 0.20384809583425495], np.float32)
26
+
27
+
28
+ def normalize_actions(a: np.ndarray) -> np.ndarray:
29
+ out = np.asarray(a, np.float32).copy()
30
+ out[..., :6] = np.clip(2 * ((out[..., :6] - Q01) / (Q99 - Q01)) - 1, -1, 1)
31
+ out[..., 6] = np.where(out[..., 6] > 0.5, 1.0, -1.0)
32
+ return out
33
+
34
+
35
+ class WidowXBridgeV3Dataset(Dataset):
36
+ def __init__(self, root: str, chunk_length: int = 32, max_episodes: int | None = None,
37
+ image_size: int = 256, feature_cache: str | None = None,
38
+ window_mode: str | None = None, validate_cache: bool = True):
39
+ self.root = Path(root)
40
+ self.chunk_length = int(chunk_length)
41
+ self.window_mode = window_mode or os.environ.get('WIDOWX_WINDOW_MODE', 'full')
42
+ if self.window_mode not in ('full', 'legacy'):
43
+ raise ValueError('window_mode must be full or legacy')
44
+ self.image_size = int(image_size)
45
+ self.info = json.loads((self.root / 'meta/info.json').read_text())
46
+ self.video_key = 'observation.images.image_0'
47
+ if self.video_key not in self.info['features']:
48
+ raise ValueError('Bridge front camera image_0 is missing')
49
+ self.feature_cache = Path(feature_cache) if feature_cache else None
50
+ episodes = [json.loads(x) for x in (self.root / "meta/episodes.jsonl").read_text().splitlines()]
51
+ tasks = [json.loads(x) for x in (self.root / "meta/tasks.jsonl").read_text().splitlines()]
52
+ self.tasks = {int(x['task_index']): x['task'] for x in tasks}
53
+ if max_episodes is not None:
54
+ episodes = episodes[:int(max_episodes)]
55
+ self.episodes = episodes
56
+ self.windows = [(e["episode_index"], s) for e in episodes
57
+ for s in range(0, int(e['length']) if self.window_mode == 'full'
58
+ else max(0, int(e['length']) - self.chunk_length), 4)]
59
+ self.feature_fingerprint = None
60
+ if self.feature_cache is not None:
61
+ if self.window_mode == 'full':
62
+ from scripts.widowx_contract import read_manifest
63
+ self.feature_fingerprint = read_manifest(self.root, self.feature_cache)
64
+ # Fail during dataset construction instead of much later in a
65
+ # worker, where a missing frozen CTE feature looks like a random
66
+ # dataloader crash after training has already started.
67
+ required_episodes = sorted({episode for episode, _ in self.windows})
68
+ missing = [episode for episode in required_episodes
69
+ if not (self.feature_cache / f"episode_{episode:06d}.pt").is_file()]
70
+ if missing and validate_cache:
71
+ preview = ", ".join(str(x) for x in missing[:8])
72
+ raise FileNotFoundError(
73
+ f"WIDOWX_FEATURE_CACHE is missing {len(missing)} episode feature files "
74
+ f"(first: {preview}); run scripts/prepare_widowx_features.py before training"
75
+ )
76
+ self._frames = OrderedDict()
77
+ self._rows: Dict[int, Dict[str, np.ndarray]] = {}
78
+ self._feature_episode = None
79
+ self._features = None
80
+
81
+ def __len__(self):
82
+ return len(self.windows)
83
+
84
+ def _load(self, episode: int):
85
+ if episode in self._rows:
86
+ return
87
+ chunk = episode // int(self.info.get('chunks_size', 1000))
88
+ fmt = dict(episode_chunk=chunk, episode_index=episode, video_key=self.video_key)
89
+ dp = self.root / self.info['data_path'].format(**fmt)
90
+ tab = pq.read_table(dp)
91
+ rows = {k: np.asarray(tab[k].to_pylist())
92
+ for k in ("observation.state", "action", "task_index")}
93
+ vp = self.root / self.info['video_path'].format(**fmt)
94
+ with av.open(str(vp)) as container:
95
+ frames = [f.to_ndarray(format="rgb24") for f in container.decode(video=0)]
96
+ if len(frames) != len(rows['action']):
97
+ raise ValueError(f'Episode {episode}: video/action frame count mismatch')
98
+ if len(self._frames) >= 2:
99
+ old, _ = self._frames.popitem(last=False)
100
+ del self._rows[old]
101
+ self._rows[episode] = rows
102
+ self._frames[episode] = np.stack(frames)
103
+
104
+ def __getitem__(self, index: int):
105
+ episode, start = self.windows[index]
106
+ self._load(episode)
107
+ rows, frames = self._rows[episode], self._frames[episode]
108
+ end = start + self.chunk_length
109
+ # A terminal recorded action remains a valid policy label even where
110
+ # the following image was not exported. Mask those modalities separately.
111
+ n_action = min(self.chunk_length, len(rows['action']) - start)
112
+ n_frames = min(self.chunk_length + 1, len(frames) - start)
113
+ image_indices = np.minimum(np.arange(start, end + 1), len(frames) - 1)
114
+ imgs = torch.from_numpy(frames[image_indices]).permute(3, 0, 1, 2).contiguous()
115
+ action = np.zeros((self.chunk_length, 7), np.float32)
116
+ action[:n_action] = normalize_actions(rows["action"][start:start+n_action])
117
+ action[n_action:, 6] = action[n_action-1, 6]
118
+ state = rows["observation.state"][start]
119
+ proprio = np.concatenate([state, state[-1:]], axis=0).astype(np.float32)
120
+ task_index = int(rows['task_index'][start])
121
+ sample = {"video": imgs, "action": torch.from_numpy(action),
122
+ "proprio": torch.from_numpy(proprio),
123
+ "ai_caption": self.tasks[task_index], 'mode': 'wam',
124
+ 'viewpoint': 'ego_view', 'domain_id': torch.tensor(7),
125
+ 'conditioning_fps': torch.tensor(int(self.info['fps']))}
126
+ sample['widowx_action_valid'] = torch.arange(self.chunk_length) < n_action
127
+ # Wan's causal latents represent raw boundaries 0,4,8,...; require
128
+ # an entirely real four-frame group for each supervised future latent.
129
+ sample['widowx_vision_valid'] = torch.arange(0, self.chunk_length + 1, 4) < n_frames
130
+ sample['widowx_episode_start'] = torch.tensor([episode, start])
131
+ if self.feature_cache:
132
+ if self._feature_episode != episode:
133
+ features = torch.load(self.feature_cache / f'episode_{episode:06d}.pt', weights_only=True)
134
+ if self.feature_fingerprint is not None:
135
+ if features.get('fingerprint') != self.feature_fingerprint:
136
+ raise ValueError(f'Episode {episode}: feature fingerprint mismatch')
137
+ if features['starts'].tolist() != list(range(0, len(frames), 4)):
138
+ raise ValueError(f'Episode {episode}: incomplete feature boundaries')
139
+ self._features, self._feature_episode = features, episode
140
+ features = self._features
141
+ for key in ('behavior_global', 'behavior_phase', 'behavior_effect', 'behavior_effect_valid'):
142
+ sample[key] = features[key][start // 4]
143
+ return sample
144
+
145
+ def get_shuffle_blocks(self):
146
+ blocks = []
147
+ for i, (episode, _) in enumerate(self.windows):
148
+ if i == 0 or self.windows[i-1][0] != episode:
149
+ blocks.append([i, 1])
150
+ else:
151
+ blocks[-1][1] += 1
152
+ return blocks
153
+
154
+
155
+ def get_action_widowx_sft_dataset(root: str, tokenizer_config=None, max_action_dim=64,
156
+ resolution='256', **kwargs):
157
+ from cosmos_framework.data.generator.action.datasets.action_sft_dataset import ActionSFTDataset
158
+ from scripts.widowx_data_resume import WidowXIterableDataset
159
+ from cosmos_framework.data.generator.action.transforms import ActionTransformPipeline
160
+ raw = WidowXBridgeV3Dataset(root=root, **kwargs)
161
+ transform = ActionTransformPipeline(tokenizer_config=tokenizer_config, max_action_dim=max_action_dim,
162
+ format_prompt_as_json=True)
163
+ return WidowXIterableDataset(ActionSFTDataset(raw, transform, resolution))
releases/bridge-native-v3/code/cosmos_framework/data/generator/action/domain_utils.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Domain ID helpers for cross-embodiment action datasets."""
5
+
6
+ EMBODIMENT_TO_DOMAIN_ID: dict[str, int] = {
7
+ "no_action": 0,
8
+ "av": 1,
9
+ "camera_pose": 2,
10
+ "hand_pose": 3,
11
+ "pusht": 4,
12
+ "libero": 5,
13
+ "umi": 6,
14
+ "bridge_orig_lerobot": 7,
15
+ "droid_lerobot": 8,
16
+ "robomind-franka": 8,
17
+ "embodiment_b": 9,
18
+ "robomind-franka-dual": 12,
19
+ "robomind-ur": 13,
20
+ "agibotworld": 15,
21
+ "embodiment_c_gripper": 15,
22
+ "embodiment_c_gripper_ext": 15,
23
+ "xdof_yam": 16,
24
+ "molmoact2_yam": 16,
25
+ "abc_yam": 16,
26
+ "fractal": 20,
27
+ "drawanything": 21,
28
+ # Dedicated Panda-Omron embedding used when the released policy was trained.
29
+ # Do not alias this to DROID or to an unused numeric slot.
30
+ "robocasa-panda-omron": 22,
31
+ }
32
+
33
+
34
+ EMBODIMENT_TO_RAW_ACTION_DIM: dict[str, int] = {
35
+ "av": 9,
36
+ "camera_pose": 9,
37
+ "pusht": 2,
38
+ "umi": 10,
39
+ "bridge_orig_lerobot": 10,
40
+ "droid_lerobot": 10,
41
+ "robomind-franka": 10,
42
+ "robomind-franka-dual": 20,
43
+ "robomind-ur": 10,
44
+ "embodiment_b": 30,
45
+ "agibotworld": 29,
46
+ "embodiment_c_gripper": 29,
47
+ "embodiment_c_gripper_ext": 29,
48
+ "xdof_yam": 20,
49
+ "molmoact2_yam": 20,
50
+ "abc_yam": 20,
51
+ "fractal": 10,
52
+ "drawanything": 3,
53
+ # Full source command before the Atomic-5 arm-only [5:12] selection.
54
+ "robocasa-panda-omron": 12,
55
+ }
56
+
57
+
58
+ def get_domain_id(embodiment_type: str) -> int:
59
+ """Get the model domain embedding ID for an embodiment."""
60
+ key = embodiment_type.lower().strip()
61
+ if key not in EMBODIMENT_TO_DOMAIN_ID:
62
+ raise KeyError(
63
+ f"Unknown embodiment type: {embodiment_type!r}. "
64
+ f"Available embodiments: {sorted(EMBODIMENT_TO_DOMAIN_ID.keys())}"
65
+ )
66
+ return EMBODIMENT_TO_DOMAIN_ID[key]
67
+
68
+
69
+ def get_action_dim(embodiment_type: str) -> int:
70
+ """Get the raw action width before dataset-specific channel selection."""
71
+ key = embodiment_type.lower().strip()
72
+ if key not in EMBODIMENT_TO_RAW_ACTION_DIM:
73
+ raise KeyError(
74
+ f"Unknown embodiment type: {embodiment_type!r}. "
75
+ f"Available embodiments: {sorted(EMBODIMENT_TO_RAW_ACTION_DIM.keys())}"
76
+ )
77
+ return EMBODIMENT_TO_RAW_ACTION_DIM[key]
78
+
79
+
80
+ def is_valid_domain_name(embodiment_type: str) -> bool:
81
+ """Check whether an embodiment has a canonical raw action width."""
82
+ key = embodiment_type.lower().strip()
83
+ return key in EMBODIMENT_TO_RAW_ACTION_DIM
releases/bridge-native-v3/code/cosmos_framework/data/generator/action/normalizer_stats/robocasa365_target_action_stats.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "action": {
3
+ "min": [-1.0, -1.0, -1.0, 0.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0],
4
+ "max": [1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
5
+ "mean": [0.02590581175350872, 0.00202585452064382, -0.000603911072971857, 0.0, -0.7797204007304583, 0.007985740558506635, -0.009527682245632989, -0.02404516475177401, 0.0024829546270168945, -0.009724957579023025, -0.00014074740094141753, -0.17212704805076373],
6
+ "std": [0.19966604450417424, 0.18601724713237694, 0.12950011360377808, 0.0, 0.6261278595805986, 0.4100137479669798, 0.3704388299067406, 0.3502567436993047, 0.10230268334142267, 0.11492962521591003, 0.11579242435141789, 0.9850747576645584],
7
+ "q01": [-0.3200703506810163, -0.34884918400617965, -0.21919116581311202, 3.9995784462121255e-16, -0.9968481276386474, -0.9422251311439209, -0.8442310960485607, -0.7521983922010141, -0.2722379479403091, -0.36124760554870705, -0.31036559990487256, -0.9993595477431751],
8
+ "q99": [0.39805452510884953, 0.33510666883940965, 0.21445504450811365, 3.9595826617500056e-14, 0.03382406019703607, 0.8090603250091402, 0.8343534480833622, 0.8754301710780941, 0.2783829802274566, 0.28977084389036595, 0.32830698508606554, 0.9179061147815708],
9
+ "count": [14957899]
10
+ }
11
+ }
releases/bridge-native-v3/code/cosmos_framework/data/generator/action/transforms.py ADDED
@@ -0,0 +1,710 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Dataset transform wrappers for the Action project.
5
+
6
+ This module provides the ``ActionTransformPipeline`` and spatial padding utilities.
7
+
8
+ The reflection padding snaps each sample to the closest predefined resolution from
9
+ ``VIDEO_RES_SIZE_INFO`` (matching VFM's approach), guaranteeing a bounded set of
10
+ output shapes that are all multiples of 16.
11
+
12
+ See :func:`~.unified_dataset.wrap_dataset` for the convenience factory that
13
+ combines datasets with transforms, and :class:`~.unified_dataset.MapToIterableAdapter`
14
+ for the map-to-iterable wrapper.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import torch
20
+ import torchvision.transforms.functional as transforms_F
21
+
22
+ from cosmos_framework.utils import log
23
+ from cosmos_framework.data.generator.action.action_processing import (
24
+ ActionNormalizer,
25
+ ActionProcessor,
26
+ )
27
+ from cosmos_framework.data.generator.action.json_formatter import ActionPromptJsonFormatter
28
+ from cosmos_framework.data.generator.action.viewpoint_utils import ViewpointTextInfo
29
+ from cosmos_framework.data.generator.augmentors.duration_fps_text_timestamps import DurationFPSTextTimeStamps
30
+ from cosmos_framework.data.generator.augmentors.idle_frames_text_info import IdleFramesTextInfo
31
+ from cosmos_framework.data.generator.augmentors.resolution_text_info import ResolutionTextInfo
32
+ from cosmos_framework.data.generator.augmentors.text_tokenizer import TextTokenizerTransform
33
+ from cosmos_framework.data.generator.utils import VIDEO_RES_SIZE_INFO
34
+ from cosmos_framework.data.generator.sequence_packing import SequencePlan
35
+ from cosmos_framework.utils.generator.data_utils import get_vision_data_resolution
36
+
37
+
38
+ def _should_append_idle_frame_info(mode: object) -> bool:
39
+ """Return whether idle-frame prompt metadata should be surfaced."""
40
+ return mode != "inverse_dynamics"
41
+
42
+
43
+ def find_closest_target_size(h: int, w: int, resolution: str | int) -> tuple[int, int]:
44
+ """Find the closest predefined target size for a given input resolution.
45
+
46
+ Looks up ``VIDEO_RES_SIZE_INFO[resolution]`` and selects the aspect ratio
47
+ whose ``H/W`` ratio is closest to the input ``h/w``.
48
+
49
+ Args:
50
+ h: Input height in pixels.
51
+ w: Input width in pixels.
52
+ resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``).
53
+
54
+ Returns:
55
+ ``(target_w, target_h)`` from the predefined table.
56
+
57
+ Raises:
58
+ ValueError: If *resolution* is not a key in ``VIDEO_RES_SIZE_INFO``.
59
+ """
60
+ if isinstance(resolution, int):
61
+ resolution = str(resolution)
62
+ if resolution not in VIDEO_RES_SIZE_INFO:
63
+ raise ValueError(
64
+ f"Resolution '{resolution}' not found in VIDEO_RES_SIZE_INFO. Available: {list(VIDEO_RES_SIZE_INFO.keys())}"
65
+ )
66
+
67
+ candidates = VIDEO_RES_SIZE_INFO[resolution]
68
+ input_ratio = h / w
69
+
70
+ best_key: str | None = None
71
+ best_diff = float("inf")
72
+ for aspect_key, (cand_w, cand_h) in candidates.items():
73
+ cand_ratio = cand_h / cand_w
74
+ diff = abs(input_ratio - cand_ratio)
75
+ if diff < best_diff:
76
+ best_diff = diff
77
+ best_key = aspect_key
78
+
79
+ assert best_key is not None
80
+ target_w, target_h = candidates[best_key]
81
+ return target_w, target_h
82
+
83
+
84
+ def reflection_pad_to_target(
85
+ data_dict: dict,
86
+ keys: list[str],
87
+ keep_aspect_ratio: bool,
88
+ target_w: int,
89
+ target_h: int,
90
+ ) -> dict:
91
+ """Resize (aspect-preserving) and reflection-pad tensors to exact target size.
92
+
93
+ For each key in *keys*, the tensor is:
94
+
95
+ 1. Resized so its spatial dimensions fit within ``(target_h, target_w)``
96
+ while preserving the aspect ratio (matching VFM's
97
+ ``ResizeLargestSideAspectPreserving``).
98
+ 2. Reflection-padded (or edge-padded when the padding exceeds the spatial
99
+ dimension) to reach exactly ``(target_h, target_w)`` (matching VFM's
100
+ ``ReflectionPadding``).
101
+
102
+ After processing, the following entries are added to *data_dict*:
103
+
104
+ - ``"image_size"``: ``torch.Tensor`` of shape ``(4,)`` containing
105
+ ``[target_h, target_w, orig_h_resized, orig_w_resized]`` where
106
+ ``target_h/w`` is the padded canvas size and ``orig_h/w_resized``
107
+ is the original spatial size after aspect-preserving resize (i.e.
108
+ the content region before padding). After ``default_collate``
109
+ this becomes ``(B, 4)``; the ``IterativeJointDataLoader`` then
110
+ splits it into per-sample ``(1, 4)`` tensors so the model can
111
+ index as ``data_batch["image_size"][i][0][0]``.
112
+
113
+ Args:
114
+ data_dict: The sample dictionary (mutated in-place).
115
+ keys: Data-dict keys whose tensors should be resized and padded.
116
+ Tensors must have shape ``(C, H, W)`` or ``(C, T, H, W)``.
117
+ keep_aspect_ratio: Whether to keep the aspect ratio of the input tensor.
118
+ target_w: Target width in pixels.
119
+ target_h: Target height in pixels.
120
+
121
+ Returns:
122
+ The mutated *data_dict*.
123
+ """
124
+ orig_h_resized: int = 0
125
+ orig_w_resized: int = 0
126
+
127
+ for key in keys:
128
+ if key not in data_dict:
129
+ continue
130
+ tensor = data_dict[key]
131
+ if not isinstance(tensor, torch.Tensor):
132
+ continue
133
+
134
+ # Extract spatial dims
135
+ if tensor.ndim == 3:
136
+ orig_h, orig_w = tensor.shape[-2:]
137
+ elif tensor.ndim == 4:
138
+ orig_h, orig_w = tensor.shape[-2:]
139
+ else:
140
+ raise ValueError(f"Unexpected tensor ndim={tensor.ndim} for key '{key}', expected 3 or 4")
141
+
142
+ # Step 1: aspect-preserving resize to fit within (target_h, target_w)
143
+ if keep_aspect_ratio:
144
+ # Prevent upscaling the video by setting the upper bound of scaling_ratio to 1.0.
145
+ scaling_ratio = min(target_w / orig_w, target_h / orig_h, 1.0)
146
+ orig_h_resized = int(scaling_ratio * orig_h + 0.5)
147
+ orig_w_resized = int(scaling_ratio * orig_w + 0.5)
148
+ assert orig_h_resized <= target_h and orig_w_resized <= target_w, (
149
+ f"Resize error: orig ({orig_h}, {orig_w}) target ({target_h}, {target_w}) "
150
+ f"computed ({orig_h_resized}, {orig_w_resized})"
151
+ )
152
+ else:
153
+ orig_h_resized = target_h
154
+ orig_w_resized = target_w
155
+
156
+ if orig_h_resized != orig_h or orig_w_resized != orig_w:
157
+ tensor = transforms_F.resize(
158
+ tensor,
159
+ size=[orig_h_resized, orig_w_resized],
160
+ interpolation=transforms_F.InterpolationMode.BICUBIC,
161
+ antialias=True,
162
+ )
163
+
164
+ # Step 2: padding to exact target size (bottom and right only)
165
+ if orig_w_resized != target_w or orig_h_resized != target_h:
166
+ padding_right = target_w - orig_w_resized
167
+ padding_bottom = target_h - orig_h_resized
168
+ padding = [0, 0, padding_right, padding_bottom]
169
+
170
+ if padding_right >= orig_w_resized or padding_bottom >= orig_h_resized:
171
+ tensor = transforms_F.pad(tensor, padding, padding_mode="edge")
172
+ else:
173
+ tensor = transforms_F.pad(tensor, padding, padding_mode="reflect")
174
+
175
+ data_dict[key] = tensor
176
+
177
+ # image_size: shape (4,) — [target_h, target_w, orig_h_resized, orig_w_resized].
178
+ # Matches VFM's item_dataset convention. default_collate stacks to (B, 4);
179
+ # IterativeJointDataLoader._get_next_sample slices to (1, 4) per sample so
180
+ # the model can index [i][0][0].
181
+ data_dict["image_size"] = torch.tensor(
182
+ [target_h, target_w, orig_h_resized, orig_w_resized], dtype=torch.float
183
+ ) # [4]
184
+
185
+ return data_dict
186
+
187
+
188
+ def remove_reflection_padding(
189
+ tensor: torch.Tensor,
190
+ image_size: torch.Tensor | list[torch.Tensor] | None,
191
+ ) -> torch.Tensor:
192
+ """Remove reflection padding added by :func:`reflection_pad_to_target`.
193
+
194
+ Content is at top-left; crops to ``(orig_h_resized, orig_w_resized)``.
195
+
196
+ Args:
197
+ tensor: Tensor whose last two dimensions are the padded spatial dims.
198
+ Supports any leading dimensions, e.g. ``(C, T, H, W)`` or
199
+ ``(C, H, W)``.
200
+ image_size: Spatial metadata using the convention produced by
201
+ :func:`reflection_pad_to_target`. Accepted forms are ``None`` (no
202
+ crop), a tensor with shape ``(4,)`` or ``(1, 4)``, or a non-empty
203
+ list whose first element has one of those tensor shapes. The four
204
+ values are ``[target_h, target_w, orig_h_resized,
205
+ orig_w_resized]``, where ``orig_h/w_resized`` is the original
206
+ spatial size after aspect-preserving resize (i.e. the content
207
+ region before padding). This matches the convention stored by
208
+ :func:`reflection_pad_to_target` and VFM's ``ReflectionPadding``.
209
+
210
+ Returns:
211
+ Cropped tensor of shape ``(..., orig_h_resized, orig_w_resized)``.
212
+ """
213
+ if image_size is None:
214
+ return tensor
215
+ if isinstance(image_size, list):
216
+ if not image_size:
217
+ raise ValueError("Expected at least one image_size entry")
218
+ image_size = image_size[0] # [1,4] or [4]
219
+ if image_size.ndim == 2 and image_size.shape[0] == 1:
220
+ image_size = image_size[0] # [4]
221
+ if image_size.ndim != 1:
222
+ raise ValueError(f"Expected image_size shape [4] or [1,4], got {tuple(image_size.shape)}")
223
+
224
+ target_h = int(image_size[0].item())
225
+ target_w = int(image_size[1].item())
226
+ orig_h_resized = int(image_size[2].item())
227
+ orig_w_resized = int(image_size[3].item())
228
+
229
+ if orig_h_resized == target_h and orig_w_resized == target_w:
230
+ return tensor
231
+
232
+ return tensor[..., :orig_h_resized, :orig_w_resized].contiguous()
233
+
234
+
235
+ def build_sequence_plan_from_mode(
236
+ mode: str,
237
+ video_length: int,
238
+ action_length: int,
239
+ has_text: bool = True,
240
+ video_temporal_downsample: int = 4,
241
+ num_history_actions: int = 0,
242
+ ) -> SequencePlan:
243
+ """Build a SequencePlan based on the training mode.
244
+
245
+ This function determines whether action should be included and computes the
246
+ appropriate condition frame indexes for vision and action based on the mode.
247
+
248
+ Args:
249
+ mode: Training mode. One of:
250
+ - "forward_dynamics": Predict video given first frame and all actions
251
+ - "inverse_dynamics": Predict actions given all video frames
252
+ - "wam": Jointly denoise video and actions given the first frame
253
+ video_length: Number of video frames (including the conditioning frame).
254
+ action_length: Number of action steps (typically video_length - 1).
255
+ has_text: Whether text conditioning is available. Defaults to True.
256
+ video_temporal_downsample: Temporal downsampling factor of the video
257
+ tokenizer. Used to compute condition frame indexes for inverse
258
+ dynamics mode. Defaults to 4.
259
+
260
+ Returns:
261
+ SequencePlan instance with appropriate settings.
262
+ Use ``sequence_plan.has_action`` to check if action should be included.
263
+
264
+ Raises:
265
+ ValueError: If mode is not one of the supported modes.
266
+
267
+ Example:
268
+ >>> sequence_plan = build_sequence_plan_from_mode(
269
+ ... mode="wam",
270
+ ... video_length=5,
271
+ ... action_length=4,
272
+ ... )
273
+ >>> sequence_plan.has_action
274
+ True
275
+ >>> sequence_plan.as_dict()
276
+ {'has_text': True, 'has_vision': True, 'has_action': True,
277
+ 'condition_frame_indexes_vision': [0], 'condition_frame_indexes_action': []}
278
+ """
279
+ valid_modes = ["forward_dynamics", "inverse_dynamics", "wam"]
280
+ if mode not in valid_modes:
281
+ raise ValueError(f"Invalid mode: {mode!r}. Must be one of {valid_modes}")
282
+
283
+ # Determine condition frame indexes based on mode
284
+ # forward_dynamics/wam: first frame is clean (conditioning)
285
+ # inverse_dynamics: all frames are provided as context
286
+ if mode in ["forward_dynamics", "wam"]:
287
+ condition_frame_indexes_vision = [0]
288
+ elif mode == "inverse_dynamics":
289
+ # All frames are observed for inverse dynamics
290
+ condition_frame_indexes_vision = list(range(0, (video_length - 1) // video_temporal_downsample + 1))
291
+ else:
292
+ condition_frame_indexes_vision = []
293
+
294
+ # For action conditioning indexes:
295
+ # forward_dynamics: all action steps are clean (conditioning)
296
+ # inverse_dynamics/wam: action is supervised (predicted)
297
+ # History frames (prepended) are always conditioning.
298
+ #
299
+ # `base_action_length` is the number of *predicted* action steps, i.e.
300
+ # `action_length` minus the prepended history block. There are three
301
+ # supported alignments between the action stream and the video stream
302
+ # (`video_length` counts the leading conditioning frame at index 0):
303
+ #
304
+ # Case A (dense video, action = video_length - 1)
305
+ # Standard forward-dynamics / WAM layout: one action per gap
306
+ # between consecutive video frames, no explicit initial-state
307
+ # action. Conditioning actions are only the prepended history.
308
+ #
309
+ # Case B (dense video, action = video_length)
310
+ # `use_state=True` layout: one extra action is prepended to
311
+ # encode the initial state, so history + one initial-state
312
+ # action are conditioning.
313
+ #
314
+ # Case C (subsampled video, general action_length > video_length)
315
+ # Video is subsampled by an integer factor while the action
316
+ # stream stays dense. `has_initial_state` mirrors Case B: it is
317
+ # True iff `(base_action_length - 1)` divides evenly by
318
+ # `(video_length - 1)`, i.e. the extra initial-state action is
319
+ # present.
320
+ base_action_length = action_length - num_history_actions
321
+ has_initial_state = video_length > 1 and (base_action_length - 1) % (video_length - 1) == 0
322
+ if mode == "forward_dynamics":
323
+ condition_frame_indexes_action = list(range(action_length))
324
+ elif base_action_length == video_length - 1:
325
+ # Case A: only the history block is conditioning.
326
+ condition_frame_indexes_action = list(range(num_history_actions))
327
+ elif base_action_length == video_length:
328
+ # Case B: history block + one initial-state action.
329
+ condition_frame_indexes_action = list(range(num_history_actions + 1))
330
+ else:
331
+ # Case C: same rule as A/B based on whether an initial-state action exists.
332
+ condition_frame_indexes_action = list(
333
+ range(num_history_actions + 1 if has_initial_state else num_history_actions)
334
+ )
335
+
336
+ # `action_start_frame_offset` shifts the action stream so that the first
337
+ # non-conditioning action aligns with video frame index 1 (the first
338
+ # frame the model must predict). The `-num_history_actions` term
339
+ # reserves room for the history block; the additional `+1` (i.e.
340
+ # `1 - num_history_actions`) is applied only when there is no
341
+ # initial-state action, so the offset skips past the first action
342
+ # slot that is instead consumed by the initial state.
343
+ if base_action_length == video_length - 1:
344
+ action_start_frame_offset = 1 - num_history_actions # Case A
345
+ elif base_action_length == video_length:
346
+ action_start_frame_offset = -num_history_actions # Case B
347
+ else:
348
+ # Case C: same rule as A/B based on whether an initial-state action exists.
349
+ action_start_frame_offset = -num_history_actions if has_initial_state else 1 - num_history_actions
350
+
351
+ return SequencePlan(
352
+ has_text=has_text,
353
+ has_vision=True,
354
+ has_action=True,
355
+ condition_frame_indexes_vision=condition_frame_indexes_vision,
356
+ condition_frame_indexes_action=condition_frame_indexes_action,
357
+ action_start_frame_offset=action_start_frame_offset,
358
+ )
359
+
360
+
361
+ class VideoResize:
362
+ """Resize and reflection-pad video-aligned tensors for a single sample.
363
+
364
+ Resolution is supplied at call time. When ``resolution`` is ``None``, the
365
+ tier is auto-detected from the sample's ``"video"`` spatial dimensions.
366
+
367
+ Args:
368
+ pad_keys: Data-dict keys whose values should be resized and padded.
369
+ Pass an empty list to disable padding entirely. Defaults to
370
+ ``["video"]``.
371
+ keep_aspect_ratio: Whether to resize aspect-preservingly to the closest
372
+ predefined target size before padding. Defaults to ``True``.
373
+ log_prefix: Prefix used in debug logging.
374
+ """
375
+
376
+ def __init__(
377
+ self,
378
+ pad_keys: list[str] | None = None,
379
+ keep_aspect_ratio: bool = True,
380
+ log_prefix: str = "VideoResize",
381
+ ) -> None:
382
+ self.pad_keys = pad_keys if pad_keys is not None else ["video"]
383
+ self.keep_aspect_ratio = keep_aspect_ratio
384
+ self.log_prefix = log_prefix
385
+
386
+ def __call__(self, data_dict: dict, resolution: str | int | None) -> dict:
387
+ """Resize and pad a sample in-place.
388
+
389
+ Args:
390
+ data_dict: Sample dictionary containing a ``"video"`` entry.
391
+ resolution: Resolution tier key (e.g. ``"256"``, ``"480"``,
392
+ ``"720"``). When ``None``, auto-detected from video dimensions.
393
+
394
+ Returns:
395
+ The same dictionary, mutated in-place with padded tensors and an
396
+ ``"image_size"`` entry.
397
+ """
398
+ video = data_dict.get("video")
399
+ assert isinstance(video, torch.Tensor), "video is required for reflection padding"
400
+ h, w = video.shape[-2:]
401
+
402
+ if resolution is None:
403
+ resolution = get_vision_data_resolution((h, w))
404
+
405
+ if self.keep_aspect_ratio:
406
+ target_w, target_h = find_closest_target_size(h, w, resolution)
407
+ else:
408
+ target_w = int(resolution)
409
+ target_h = int(resolution)
410
+ reflection_pad_to_target(data_dict, self.pad_keys, self.keep_aspect_ratio, target_w, target_h)
411
+
412
+ return data_dict
413
+
414
+ def _log_shapes(self, data_dict: dict, when: str) -> None:
415
+ """Log tensor shapes for the configured pad keys."""
416
+ for key in self.pad_keys:
417
+ val = data_dict.get(key)
418
+ if isinstance(val, torch.Tensor):
419
+ log.debug(f"{self.log_prefix}: {when} padding '{key}' shape = {tuple(val.shape)}")
420
+
421
+
422
+ class ActionTransformPipeline:
423
+ """A composable transform pipeline that chains ``VideoResize``, text
424
+ tokenization, and automatic sequence plan construction.
425
+
426
+ Reflection padding snaps each sample to the closest predefined aspect
427
+ ratio from ``VIDEO_RES_SIZE_INFO[resolution]``, resizes
428
+ (aspect-preserving) to fit within the target, then reflection-pads to
429
+ the exact target size. This guarantees a bounded set of output shapes
430
+ (5 per resolution tier), all multiples of 16. Resolution is supplied
431
+ at call time via the required ``resolution`` argument to ``__call__``;
432
+ when ``resolution`` is ``None``, the tier is auto-detected from the
433
+ video's spatial dimensions via ``get_vision_data_resolution``.
434
+
435
+ Text tokenization is enabled when ``tokenizer_config`` is provided.
436
+
437
+ When the data dictionary contains a ``"mode"`` key, the pipeline automatically
438
+ builds a ``SequencePlan`` via :func:`build_sequence_plan_from_mode` and attaches
439
+ it as ``data_dict["sequence_plan"]``.
440
+
441
+ Args:
442
+ pad_keys: Data-dict keys whose values should be resized and padded. Pass
443
+ an empty list to disable padding entirely. Defaults to ``["video"]``.
444
+ tokenizer_config: A lazy-instantiable config dict for the VLM tokenizer. When
445
+ ``None``, text tokenization is skipped. Defaults to ``None``.
446
+ cfg_dropout_rate: Probability of replacing the caption with an empty string for
447
+ classifier-free guidance. Only used when text tokenization is enabled.
448
+ Defaults to ``0.0``.
449
+ caption_key: The data-dict key that contains the input caption string.
450
+ Defaults to ``"ai_caption"``.
451
+ text_token_key: The data-dict key where tokenized text IDs will be stored.
452
+ Defaults to ``"text_token_ids"``.
453
+ video_temporal_downsample: Temporal downsampling factor of the video tokenizer.
454
+ Used when building a ``SequencePlan`` for ``"inverse_dynamics"`` mode.
455
+ Defaults to 4.
456
+ max_action_dim: Target action dimension to pad to. The ``"action"`` tensor
457
+ in every sample is padded along its last dimension via
458
+ :func:`pad_action_to_max_dim`. Defaults to 32.
459
+ action_channel_masking: When ``True`` (default), the original action
460
+ dimension is stored in ``"raw_action_dim"`` so that the model masks
461
+ loss/noise/velocity on zero-padded action channels. When ``False``,
462
+ ``"raw_action_dim"`` is set to ``None`` and the model treats all
463
+ ``max_action_dim`` channels equally (original main-branch behavior).
464
+ append_viewpoint_info: Whether to append viewpoint type metadata to the
465
+ caption (via ``ViewpointTextInfo`` augmentor). Requires that
466
+ samples contain a ``"viewpoint"`` key. Defaults to ``True``.
467
+ append_duration_fps_timestamps: Whether to append duration and FPS metadata to the
468
+ caption (matching VFM's ``DurationFPSTextTimeStamps`` augmentor).
469
+ Defaults to ``True``.
470
+ append_resolution_info: Whether to append resolution metadata to the
471
+ caption (matching VFM's ``ResolutionTextInfo`` augmentor).
472
+ Defaults to ``True``.
473
+ append_idle_frames: Whether to append the idle-frame count out of the
474
+ total action frames to the caption (Pi0.7-style metadata, via
475
+ ``IdleFramesTextInfo`` augmentor). The dataset is responsible for
476
+ populating ``data_dict["idle_frames"]``; samples without it are
477
+ silently skipped. Idle-frame text is skipped only for
478
+ ``"inverse_dynamics"`` mode. Defaults to ``False`` so existing
479
+ experiments are unaffected.
480
+ idle_frames_dropout: Per-field dropout rate for the idle-frame segment.
481
+ With this probability the augmentor leaves the caption unchanged
482
+ (matching Pi0.7's ~5% per-component dropout). Independent of the
483
+ global ``cfg_dropout_rate``, which empties the whole caption.
484
+ Defaults to 0.05.
485
+ format_prompt_as_json: Whether to replace the plain text prompt with a
486
+ structured JSON-compatible dictionary before tokenization. When
487
+ enabled, legacy string metadata appenders are skipped and the JSON
488
+ formatter owns viewpoint, action, resolution, duration, FPS, and
489
+ idle-frame fields. Defaults to ``False``.
490
+ """
491
+
492
+ def __init__(
493
+ self,
494
+ pad_keys: list[str] | None = None,
495
+ keep_aspect_ratio: bool = True,
496
+ tokenizer_config: dict | None = None,
497
+ cfg_dropout_rate: float = 0.0,
498
+ caption_key: str = "ai_caption",
499
+ text_token_key: str = "text_token_ids",
500
+ video_temporal_downsample: int = 4,
501
+ max_action_dim: int = 32,
502
+ action_channel_masking: bool = True,
503
+ append_viewpoint_info: bool = True,
504
+ append_duration_fps_timestamps: bool = True,
505
+ append_resolution_info: bool = True,
506
+ append_idle_frames: bool = False,
507
+ idle_frames_dropout: float = 0.05,
508
+ format_prompt_as_json: bool = False,
509
+ ) -> None:
510
+ self.caption_key: str = caption_key
511
+ self.video_temporal_downsample: int = video_temporal_downsample
512
+ self.max_action_dim: int = max_action_dim
513
+ self.action_channel_masking: bool = action_channel_masking
514
+ self.action_processor: ActionProcessor = ActionProcessor(
515
+ max_action_dim=max_action_dim,
516
+ action_channel_masking=action_channel_masking,
517
+ )
518
+
519
+ # --- Spatial resize/padding stage (resolution supplied at call time) ---
520
+ self.video_resize: VideoResize = VideoResize(
521
+ pad_keys=pad_keys,
522
+ keep_aspect_ratio=keep_aspect_ratio,
523
+ log_prefix="ActionTransformPipeline",
524
+ )
525
+ self.pad_keys: list[str] = self.video_resize.pad_keys
526
+ self.keep_aspect_ratio: bool = self.video_resize.keep_aspect_ratio
527
+
528
+ self.prompt_json_formatter: ActionPromptJsonFormatter | None = None
529
+ if format_prompt_as_json:
530
+ self.prompt_json_formatter = ActionPromptJsonFormatter(caption_key=caption_key)
531
+
532
+ # --- Viewpoint text augmentor (runs after ai_caption, before duration/FPS) ---
533
+ self.viewpoint_augmentor: ViewpointTextInfo | None = None
534
+ if append_viewpoint_info and self.prompt_json_formatter is None:
535
+ self.viewpoint_augmentor = ViewpointTextInfo(
536
+ input_keys=[caption_key, "viewpoint"],
537
+ output_keys=[caption_key],
538
+ args={"caption_key": caption_key, "viewpoint_key": "viewpoint", "enabled": True},
539
+ )
540
+
541
+ # --- Duration/FPS text augmentor (runs before tokenization) ---
542
+ self.duration_fps_augmentor: DurationFPSTextTimeStamps | None = None
543
+ if append_duration_fps_timestamps and self.prompt_json_formatter is None:
544
+ self.duration_fps_augmentor = DurationFPSTextTimeStamps(
545
+ input_keys=[caption_key, "video", "conditioning_fps"],
546
+ output_keys=[caption_key],
547
+ args={"caption_key": caption_key, "video_key": "video", "fps_key": "conditioning_fps"},
548
+ )
549
+
550
+ # --- Resolution text augmentor (runs before tokenization) ---
551
+ self.resolution_info_augmentor: ResolutionTextInfo | None = None
552
+ if append_resolution_info and self.prompt_json_formatter is None:
553
+ self.resolution_info_augmentor = ResolutionTextInfo(
554
+ input_keys=[caption_key, "video", "image_size"],
555
+ output_keys=[caption_key],
556
+ args={"caption_key": caption_key, "video_key": "video", "enabled": True},
557
+ )
558
+
559
+ # --- IdleFrames text augmentor (Pi0.7-style episode metadata) ---
560
+ # Runs after resolution info, before tokenization. Per-field dropout is
561
+ # independent from the tokenizer's global cfg_dropout_rate.
562
+ self.idle_frames_augmentor: IdleFramesTextInfo | None = None
563
+ if append_idle_frames and self.prompt_json_formatter is None:
564
+ self.idle_frames_augmentor = IdleFramesTextInfo(
565
+ input_keys=[caption_key, "idle_frames", "action"],
566
+ output_keys=[caption_key],
567
+ args={
568
+ "caption_key": caption_key,
569
+ "idle_frames_key": "idle_frames",
570
+ "action_key": "action",
571
+ "dropout_rate": idle_frames_dropout,
572
+ "enabled": True,
573
+ },
574
+ )
575
+
576
+ # --- Text tokenizer augmentor ---
577
+ self.text_tokenizer: TextTokenizerTransform | None = None
578
+ if tokenizer_config is not None:
579
+ self.text_tokenizer = TextTokenizerTransform(
580
+ input_keys=[caption_key],
581
+ output_keys=[text_token_key],
582
+ args={
583
+ "tokenizer_config": tokenizer_config,
584
+ "cfg_dropout_rate": cfg_dropout_rate,
585
+ },
586
+ )
587
+
588
+ def __call__(
589
+ self,
590
+ data_dict: dict,
591
+ resolution: str | None,
592
+ action_normalizer: ActionNormalizer | None = None,
593
+ ) -> dict:
594
+ """Apply the transform pipeline to a single data dictionary.
595
+
596
+ Resolution is required at call time and is the only source of truth
597
+ for this sample. When ``resolution`` is ``None``, the tier is
598
+ auto-detected from the video's spatial dimensions.
599
+
600
+ The pipeline runs in order:
601
+
602
+ 1. Resize + reflection-pad spatial dimensions to the closest
603
+ predefined target from ``VIDEO_RES_SIZE_INFO[resolution]``.
604
+ 2. Format the caption as a structured JSON prompt (if enabled).
605
+ 3. Otherwise, append viewpoint type metadata to caption (if enabled).
606
+ 4. Append duration/FPS metadata to caption (if enabled).
607
+ 5. Append resolution metadata to caption (if enabled).
608
+ 6. Append idle-frame metadata (Pi0.7-style) to caption unless the
609
+ sample is in inverse dynamics mode (if enabled).
610
+ 7. Tokenize caption text (if enabled).
611
+ 8. Build a ``SequencePlan`` from the ``"mode"`` key (if present).
612
+ 9. Preserve the canonical unnormalized and unpadded action as
613
+ ``"action_raw"``, normalize real channels, pad ``"action"`` to
614
+ ``max_action_dim``, and attach ``"action_processing_record"``.
615
+
616
+ Args:
617
+ data_dict: A sample dictionary as returned by a Action dataset.
618
+ resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``)
619
+ for this sample. When ``None``, auto-detected from video dimensions.
620
+ action_normalizer: Optional source-provided action normalizer. When
621
+ present, only unpadded real action channels are normalized
622
+ before model-space channel padding.
623
+
624
+ Returns:
625
+ The same dictionary, mutated in-place with padded tensors,
626
+ ``image_size``, tokenized text IDs, a ``"sequence_plan"`` entry,
627
+ and action processing metadata added.
628
+ """
629
+ mode = data_dict.get("mode")
630
+ assert mode is not None, "mode is required"
631
+
632
+ # 1. Resize + reflection-pad spatial dimensions to the closest predefined target from ``VIDEO_RES_SIZE_INFO[resolution]``.
633
+ data_dict = self.video_resize(data_dict, resolution)
634
+
635
+ # 2. Format the caption as structured JSON when requested; otherwise run the legacy string appenders.
636
+ if self.prompt_json_formatter is not None:
637
+ data_dict = self.prompt_json_formatter(data_dict)
638
+ else:
639
+ # The relabeling is only rendered by the JSON formatter. Drop it
640
+ # here so that enabling ``annotation_mode`` without
641
+ # ``format_prompt_as_json`` degrades to the plain caption instead of
642
+ # leaking a dict into the collate function.
643
+ data_dict.pop("relabel_prompt", None)
644
+
645
+ # 3. Append viewpoint type metadata to caption (if enabled).
646
+ if self.viewpoint_augmentor is not None:
647
+ result = self.viewpoint_augmentor(data_dict)
648
+ if result is not None:
649
+ data_dict = result
650
+
651
+ # 4. Append duration/FPS metadata to caption (if enabled).
652
+ if self.duration_fps_augmentor is not None:
653
+ result = self.duration_fps_augmentor(data_dict)
654
+ if result is not None:
655
+ data_dict = result
656
+
657
+ # 5. Append resolution metadata to caption (if enabled).
658
+ if self.resolution_info_augmentor is not None:
659
+ result = self.resolution_info_augmentor(data_dict)
660
+ if result is not None:
661
+ data_dict = result
662
+
663
+ # 6. Append idle-frame metadata to caption (if enabled for this mode).
664
+ if self.idle_frames_augmentor is not None and _should_append_idle_frame_info(mode):
665
+ result = self.idle_frames_augmentor(data_dict)
666
+ if result is not None:
667
+ data_dict = result
668
+
669
+ # 7. Tokenize caption text (if enabled).
670
+ if self.text_tokenizer is not None:
671
+ data_dict = self.text_tokenizer(data_dict)
672
+
673
+ # 8. Build a ``SequencePlan`` from the ``"mode"`` key (if present).
674
+ video = data_dict.get("video")
675
+ action = data_dict.get("action")
676
+ assert video is not None, "video is required"
677
+ video_length = video.shape[1] # [C,T,H,W] -> T
678
+ action_length = action.shape[0] if isinstance(action, torch.Tensor) else max(video_length - 1, 0)
679
+
680
+ # Prepend history action frames (ground-truth conditioning) if present.
681
+ history_action = data_dict.pop("history_action", None)
682
+ residual_gate = data_dict.get("residual_gate")
683
+ num_history_actions = 0
684
+ if history_action is not None and isinstance(action, torch.Tensor):
685
+ num_history_actions = history_action.shape[0]
686
+ action = torch.cat([history_action, action], dim=0)
687
+ action_length += num_history_actions
688
+ if isinstance(residual_gate, torch.Tensor):
689
+ residual_gate = torch.cat(
690
+ [torch.zeros((num_history_actions, 1), dtype=residual_gate.dtype), residual_gate], dim=0
691
+ )
692
+ data_dict["residual_gate"] = residual_gate
693
+
694
+ sequence_plan = build_sequence_plan_from_mode(
695
+ mode=mode,
696
+ video_length=video_length,
697
+ action_length=action_length,
698
+ video_temporal_downsample=self.video_temporal_downsample,
699
+ num_history_actions=num_history_actions,
700
+ )
701
+ data_dict["sequence_plan"] = sequence_plan
702
+
703
+ assert isinstance(action, torch.Tensor), "action tensor is required for action modes"
704
+ data_dict = self.action_processor.preprocess_action(
705
+ data_dict,
706
+ action,
707
+ action_normalizer=action_normalizer,
708
+ )
709
+
710
+ return data_dict
releases/bridge-native-v3/code/cosmos_framework/data/generator/joint_dataloader.py ADDED
@@ -0,0 +1,1271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ import math
5
+ import queue
6
+ import threading
7
+ from collections import deque
8
+ from collections.abc import Iterator, Mapping
9
+ from dataclasses import dataclass
10
+ from typing import Any, ClassVar, Dict, Union
11
+
12
+ import numpy as np
13
+ import torch
14
+ import webdataset
15
+ from torch.utils.data.dataloader import default_collate
16
+
17
+ from cosmos_framework.utils.lazy_config import instantiate
18
+ from cosmos_framework.utils import log
19
+ from cosmos_framework.model.generator.tokenizers.uniae.frame_math import (
20
+ get_uniae_chunk_frames,
21
+ get_uniae_latent_num_frames,
22
+ normalize_uniae_chunk_frames,
23
+ )
24
+ from cosmos_framework.utils.generator.data_utils import read_positive_int_metadata
25
+
26
+ _TIMING_KEYS = {"_sample_time", "_aug_time", "_pre_aug_time", "_aug_step_times"}
27
+ _BATCH_TIMING_KEYS = {
28
+ "_worker_batch_time",
29
+ "_worker_aug_time",
30
+ "_worker_io_time",
31
+ "_worker_aug_step_times",
32
+ "_worker_id",
33
+ }
34
+
35
+
36
+ def custom_collate_fn(batch):
37
+ """
38
+ Collate function that works like default_collate for all keys other than "text_token_ids", "images", and "video".
39
+ For "text_token_ids", "images", and "video" it simply returns them in a list, instead of stacking them as a tensor.
40
+ """
41
+ list_collate_keys = {
42
+ "text_token_ids",
43
+ "images",
44
+ "video",
45
+ "action",
46
+ "action_raw",
47
+ "domain_id",
48
+ "sequence_plan",
49
+ "sound",
50
+ "raw_action_dim",
51
+ "image_size",
52
+ "action_processing_record",
53
+ "residual_gate",
54
+ "residual_release_event",
55
+ }
56
+
57
+ # Data keys where a per-sample value of ``None`` is a meaningful signal
58
+ # (e.g. audio extraction failed for that sample → ``sound=None`` paired
59
+ # with ``plan.has_sound=False``). These keys must be kept as a list with
60
+ # ``None`` placeholders so the model can align per-sample data 1:1 with
61
+ # per-sample plans. Dropping the entire key on any None would leave the
62
+ # remaining sound tensors mis-aligned with the plans whose ``has_sound``
63
+ # flag was set BEFORE collation, causing ``sequence_packing`` to index
64
+ # past the end of ``x0_tokens_sound``.
65
+ sparse_data_keys = {"sound"}
66
+
67
+ # Handle the case where the batch is already a dictionary (e.g. column-wise batching)
68
+ if isinstance(batch, dict):
69
+ return {key: (value if key in list_collate_keys else default_collate(value)) for key, value in batch.items()}
70
+
71
+ # Handle standard list of samples
72
+ elem = batch[0]
73
+ if isinstance(elem, dict):
74
+ # Some Action datasets add optional metadata keys (for example
75
+ # ``additional_view_description`` for concat-view captions) only for a
76
+ # subset of samples. PyTorch can batch such samples together when
77
+ # DataLoader batch_size > 1; collating only elem's keys and indexing
78
+ # every sample by that key turns the optional field into a fatal
79
+ # KeyError. Use the union of keys and skip optional keys that are not
80
+ # present in every sample. Required training keys still fail loudly via
81
+ # downstream assertions if actually missing.
82
+ result = {}
83
+ keys = set().union(*(d.keys() for d in batch))
84
+ for key in keys:
85
+ if key in _TIMING_KEYS:
86
+ continue
87
+ values = [d.get(key) for d in batch]
88
+ if key == "action_processing_record":
89
+ result[key] = values
90
+ continue
91
+ if any(value is None for value in values):
92
+ # Sparse data keys keep their None placeholders to preserve
93
+ # 1:1 alignment with sequence_plan. Other (optional metadata)
94
+ # keys not present in every sample are dropped.
95
+ if key in sparse_data_keys:
96
+ result[key] = values
97
+ continue
98
+ if key in list_collate_keys:
99
+ result[key] = values
100
+ else:
101
+ result[key] = default_collate(values)
102
+ result.update(_aggregate_worker_timing(batch))
103
+ return result
104
+ else:
105
+ return default_collate(batch)
106
+
107
+
108
+ def _aggregate_worker_timing(samples: list[dict]) -> dict:
109
+ """Extract per-sample timing keys, aggregate into per-batch scalars."""
110
+ info: dict[str, float | int] = {}
111
+ if "_sample_time" in samples[0]:
112
+ info["_worker_batch_time"] = sum(s.get("_sample_time", 0.0) for s in samples)
113
+ if "_aug_time" in samples[0]:
114
+ aug_total = sum(s.get("_aug_time", 0.0) for s in samples)
115
+ info["_worker_aug_time"] = aug_total
116
+ if "_worker_batch_time" in info:
117
+ info["_worker_io_time"] = info["_worker_batch_time"] - aug_total
118
+ if "_aug_step_times" in samples[0]:
119
+ agg: dict[str, float] = {}
120
+ for s in samples:
121
+ for step_name, t in s.get("_aug_step_times", {}).items():
122
+ agg[step_name] = agg.get(step_name, 0.0) + t
123
+ info["_worker_aug_step_times"] = agg
124
+ worker_info = torch.utils.data.get_worker_info()
125
+ info["_worker_id"] = worker_info.id if worker_info is not None else 0
126
+ return info
127
+
128
+
129
+ @dataclass
130
+ class _PackingMetrics:
131
+ """Per-batch packing statistics collected during the packing loop.
132
+
133
+ Also serves as the single source of truth for packing-related metric names
134
+ via ``STATS_SPEC``, which the dataloading monitor callback consumes to
135
+ drive accumulation and logging.
136
+ """
137
+
138
+ current_sequence_length: int = 0
139
+ num_samples: int = 0
140
+ dropped_count: int = 0
141
+ from_buffer: int = 0
142
+ from_workers: int = 0
143
+
144
+ STATS_SPEC: ClassVar[list[tuple[str, str, str]]] = [
145
+ # (batch_key, wandb_suffix, aggregation_type)
146
+ ("_num_tokens", "token_fraction", "scalar"),
147
+ ("_num_samples", "samples_per_batch", "list"),
148
+ ("_from_buffer", "from_buffer", "list"),
149
+ ("_from_workers", "from_workers", "list"),
150
+ ("_buffer_size", "buffer_size", "list"),
151
+ ("_dropped_count", "dropped", "scalar"),
152
+ ]
153
+
154
+ def attach_to(self, output_batch: dict, buffer_size: int) -> None:
155
+ """Write packing statistics into the output batch dict."""
156
+ output_batch["_num_tokens"] = self.current_sequence_length
157
+ output_batch["_num_samples"] = self.num_samples
158
+ output_batch["_from_buffer"] = self.from_buffer
159
+ output_batch["_from_workers"] = self.from_workers
160
+ output_batch["_buffer_size"] = buffer_size
161
+ output_batch["_dropped_count"] = self.dropped_count
162
+
163
+
164
+ @dataclass
165
+ class _AsyncBatchBuildResult:
166
+ """The outcome of building one packed batch."""
167
+
168
+ sequence_id: int
169
+ batch: dict[str, Any] | None = None
170
+ error: BaseException | None = None
171
+ end_of_stream: bool = False
172
+
173
+
174
+ class _AsyncBatchBuilder:
175
+ """Build one requested batch at a time on a persistent daemon thread.
176
+
177
+ The caller submits one sequence ID, the daemon advances the existing
178
+ packing iterator once, and the caller receives that exact batch object
179
+ from the result queue. A second request is rejected until the first result
180
+ is consumed, so at most one future batch is built at a time.
181
+ """
182
+
183
+ def __init__(self, batch_iterator: Iterator[dict[str, Any]], timeout_s: float) -> None:
184
+ self._batch_iterator: Iterator[dict[str, Any]] = batch_iterator
185
+ self._timeout_s: float = timeout_s
186
+ self._requests: queue.Queue[int | None] = queue.Queue()
187
+ self._results: queue.Queue[_AsyncBatchBuildResult | None] = queue.Queue()
188
+ self._outstanding_sequence_id: int | None = None
189
+ self._closed: bool = False
190
+ self._thread: threading.Thread = threading.Thread(target=self._run, name="async-batch-builder", daemon=True)
191
+ self._thread.start()
192
+
193
+ @property
194
+ def is_alive(self) -> bool:
195
+ return self._thread.is_alive()
196
+
197
+ def request_batch(self, sequence_id: int) -> None:
198
+ """Ask the daemon thread to build exactly one batch."""
199
+ if self._closed:
200
+ raise RuntimeError("Cannot request a batch after the async batch builder is closed.")
201
+ if self._outstanding_sequence_id is not None:
202
+ raise RuntimeError(
203
+ "Cannot request another batch while async batch building is already in progress "
204
+ f"for sequence_id={self._outstanding_sequence_id}."
205
+ )
206
+ self._outstanding_sequence_id = sequence_id
207
+ self._requests.put(sequence_id)
208
+
209
+ def get_batch(self) -> dict[str, Any]:
210
+ """Wait for and return the exact batch object produced for the outstanding request."""
211
+ sequence_id = self._outstanding_sequence_id
212
+ if sequence_id is None:
213
+ raise RuntimeError("No async batch building request is outstanding.")
214
+ if self._closed:
215
+ raise RuntimeError("Async batch builder was closed while waiting for a batch.")
216
+
217
+ try:
218
+ result = self._results.get(timeout=self._timeout_s)
219
+ except queue.Empty as error:
220
+ producer_alive = self.is_alive
221
+ # A timed-out request may still be running in the daemon thread, so it cannot be
222
+ # safely abandoned or retried. Close the builder and make the timeout terminal.
223
+ self.close()
224
+ raise TimeoutError(
225
+ f"Timed out waiting for async batch building sequence_id={sequence_id}; "
226
+ f"producer_alive={producer_alive}."
227
+ ) from error
228
+
229
+ if self._closed or result is None:
230
+ raise RuntimeError("Async batch builder was closed while waiting for a batch.")
231
+ if result.sequence_id != sequence_id:
232
+ raise RuntimeError(
233
+ "Async batch builder returned an unexpected result: "
234
+ f"expected sequence_id={sequence_id}, got sequence_id={result.sequence_id}."
235
+ )
236
+ self._outstanding_sequence_id = None
237
+
238
+ if result.error is not None:
239
+ raise RuntimeError(f"Async batch building failed for sequence_id={sequence_id}.") from result.error
240
+ if result.end_of_stream:
241
+ raise StopIteration
242
+ if result.batch is None:
243
+ raise RuntimeError(f"Async batch builder returned no batch for sequence_id={sequence_id}.")
244
+ return result.batch
245
+
246
+ def _run(self) -> None:
247
+ """Build one batch for each request and publish its result by reference."""
248
+ while True:
249
+ sequence_id = self._requests.get()
250
+ if sequence_id is None or self._closed:
251
+ return
252
+
253
+ result = _AsyncBatchBuildResult(sequence_id=sequence_id)
254
+ try:
255
+ result.batch = next(self._batch_iterator)
256
+ except StopIteration:
257
+ result.end_of_stream = True
258
+ except BaseException as error:
259
+ result.error = error
260
+
261
+ if self._closed:
262
+ return
263
+ # queue.Queue stores object references within this process. The batch is neither
264
+ # copied nor serialized here; the consumer receives the exact dictionary above.
265
+ self._results.put(result)
266
+ if result.end_of_stream or result.error is not None:
267
+ return
268
+
269
+ def close(self) -> None:
270
+ """Permanently close the builder and perform a bounded join of its daemon thread."""
271
+ if not self._closed:
272
+ self._closed = True
273
+ self._requests.put(None)
274
+ self._results.put(None)
275
+ if self._thread is not threading.current_thread():
276
+ self._thread.join(timeout=min(1.0, self._timeout_s))
277
+
278
+
279
+ class JointDataLoader(webdataset.WebLoader):
280
+ r"""
281
+ A joint dataloader that supports loading both images and videos.
282
+ """
283
+
284
+ _DEFAULT_LOOKAHEAD_LIMIT: ClassVar[int] = 10
285
+
286
+ def __init__(
287
+ self,
288
+ dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]],
289
+ tokenizer_spatial_compression_factor: int,
290
+ tokenizer_temporal_compression_factor: int,
291
+ patch_spatial: int,
292
+ max_sequence_length: int | None,
293
+ max_samples_per_batch: int | None,
294
+ sound_latent_fps: float = 0,
295
+ audio_sample_rate: int = 48000,
296
+ prewarm: bool = True,
297
+ default_lookahead_limit: int = _DEFAULT_LOOKAHEAD_LIMIT,
298
+ lookahead_limits: Dict[str, int] | None = None,
299
+ uniae_chunk_frames: int | Mapping[str, int] | None = None,
300
+ uniae_pad_frames: int | None = None,
301
+ ):
302
+ """
303
+ Initialize the JointDataLoader with multiple datasets.
304
+
305
+ The effective mini-batch size can be controlled with either max_sequence_length or
306
+ max_samples_per_batch. To use max_sequence_length, max_samples_per_batch needs to be None.
307
+ Vice versa, to use max_samples_per_batch, max_sequence_length needs to be None.
308
+ max_sequence_length and max_samples_per_batch cannot both be None simultaneously.
309
+
310
+ Args:
311
+ dataloaders: key - dataset_name; value - {"dataloader": dataloader, "ratio": data_ratio}
312
+ tokenizer_spatial_compression_factor: The spatial compression factor of the tokenizer.
313
+ tokenizer_temporal_compression_factor: The temporal compression factor of the tokenizer.
314
+ patch_spatial: Spatial pathification factor.
315
+ max_samples_per_batch: Max number of samples per packed batch (alternative to max_sequence_length).
316
+ sound_latent_fps: Sound tokenizer latent rate in Hz (e.g. 25). If 0, sound tokens are not counted.
317
+ audio_sample_rate: Audio sample rate in Hz (e.g. 48000). Used with sound_latent_fps to estimate
318
+ sound token count.
319
+ default_lookahead_limit: Packing-loop look-ahead fallback for dataloaders not in
320
+ ``lookahead_limits``.
321
+ lookahead_limits: Optional ``{dataset_name: int}`` per-dataloader override.
322
+ uniae_chunk_frames: Optional UniAE full chunk size, or resolution-keyed chunk sizes.
323
+ uniae_pad_frames: Optional UniAE boundary padding frames per chunk.
324
+
325
+ Example:
326
+ joint_loader = IterativeJointDataLoader(
327
+ dataloaders{
328
+ "image_data": {
329
+ "dataloader": webdataset.WebLoader(...),
330
+ "ratio": 4,
331
+ },
332
+ "video_data": {
333
+ "dataloader": torch.utils.data.DataLoader(...),
334
+ "ratio": 1,
335
+ },
336
+ }
337
+ )
338
+ """
339
+ self.dataloader_list, self.dataset_name_list, self.data_ratios = [], [], []
340
+ self.lookahead_limits: list[int] = []
341
+ self.tokenizer_spatial_compression_factor = tokenizer_spatial_compression_factor
342
+ self.tokenizer_temporal_compression_factor = tokenizer_temporal_compression_factor
343
+ self.patch_spatial = patch_spatial
344
+ self.max_sequence_length = max_sequence_length
345
+ self.max_samples_per_batch = max_samples_per_batch
346
+ self.sound_latent_fps = sound_latent_fps
347
+ self.audio_sample_rate = audio_sample_rate
348
+ self.default_lookahead_limit = int(default_lookahead_limit)
349
+ self.uniae_pad_frames = int(uniae_pad_frames) if uniae_pad_frames is not None else None
350
+ self.uniae_chunk_frames = self._normalize_uniae_chunk_frames(uniae_chunk_frames)
351
+
352
+ assert (self.max_sequence_length is None) != (self.max_samples_per_batch is None), (
353
+ "Exactly one of max_sequence_length or max_samples_per_batch must be None, but not both."
354
+ )
355
+
356
+ _lookahead_overrides: Dict[str, int] = dict(lookahead_limits) if lookahead_limits else {}
357
+ unknown = set(_lookahead_overrides) - set(dataloaders)
358
+ assert not unknown, f"lookahead_limits references unknown dataloaders {unknown}; valid: {sorted(dataloaders)}"
359
+
360
+ for dataset_name, dataloader_data in dataloaders.items():
361
+ if dataloader_data is None:
362
+ continue
363
+ assert set(dataloader_data.keys()) == {"dataloader", "ratio"}, f"Invalid config: {dataloader_data}"
364
+ if dataloader_data["ratio"] <= 0:
365
+ continue
366
+ self.dataset_name_list.append(dataset_name)
367
+ self.dataloader_list.append(instantiate(dataloader_data["dataloader"], collate_fn=custom_collate_fn))
368
+ self.data_ratios.append(dataloader_data["ratio"])
369
+ self.lookahead_limits.append(int(_lookahead_overrides.get(dataset_name, self.default_lookahead_limit)))
370
+
371
+ self.global_id = 0
372
+ self.ratio_sum = sum(self.data_ratios)
373
+
374
+ total = self.ratio_sum if self.ratio_sum > 0 else 1.0
375
+ lines = [f"JointDataLoader: {len(self.dataset_name_list)} streams"]
376
+ for name, ratio in zip(self.dataset_name_list, self.data_ratios):
377
+ lines.append(f" {name}: ratio={ratio:.4g} ({ratio / total:.1%})")
378
+ log.info("\n".join(lines))
379
+
380
+ self.data_len = 0
381
+ self.dataloaders = [iter(dataloader) for dataloader in self.dataloader_list]
382
+ self.buffers = [deque() for _ in range(len(self.dataloader_list))]
383
+ for data in self.dataloader_list:
384
+ self.data_len += len(data)
385
+
386
+ # Pre-warm all dataloaders: force worker process spawning and first
387
+ # batch loading so that slow dataset initialisation (e.g. action
388
+ # datasets with spawn workers) happens here rather than mid-training
389
+ # where it would cause NCCL collective timeouts.
390
+ if prewarm:
391
+ self._prewarm_dataloaders()
392
+ else:
393
+ log.info(
394
+ "JointDataLoader: prewarm DISABLED (debug mode); first iteration may incur per-stream cold-load cost"
395
+ )
396
+
397
+ def _normalize_uniae_chunk_frames(
398
+ self, uniae_chunk_frames: int | Mapping[str, int] | None
399
+ ) -> int | dict[str, int] | None:
400
+ return normalize_uniae_chunk_frames(
401
+ uniae_chunk_frames,
402
+ pad_frames=self.uniae_pad_frames,
403
+ temporal_compression_factor=self.tokenizer_temporal_compression_factor,
404
+ temporal_divisibility_name="tokenizer_temporal_compression_factor",
405
+ )
406
+
407
+ def _get_uniae_chunk_frames(self, spatial_shape: tuple[int, int]) -> int:
408
+ assert self.uniae_chunk_frames is not None
409
+ return get_uniae_chunk_frames(self.uniae_chunk_frames, spatial_shape=spatial_shape)
410
+
411
+ def _compute_vision_latent_t_shape(self, T: int, H: int, W: int) -> int:
412
+ if T < 1:
413
+ raise ValueError(f"Vision media must contain at least one frame, got {T}.")
414
+ if T == 1 or self.uniae_chunk_frames is None:
415
+ return 1 + (T - 1) // self.tokenizer_temporal_compression_factor
416
+
417
+ assert self.uniae_pad_frames is not None
418
+ return get_uniae_latent_num_frames(
419
+ T,
420
+ self.uniae_chunk_frames,
421
+ pad_frames=self.uniae_pad_frames,
422
+ temporal_compression_factor=self.tokenizer_temporal_compression_factor,
423
+ spatial_shape=(H, W),
424
+ )
425
+
426
+ def _prewarm_dataloaders(self) -> None:
427
+ """Force all dataloader iterators to spawn workers and produce one batch.
428
+
429
+ The first ``next()`` call on an ``InfiniteDataLoader`` iterator triggers
430
+ ``DataLoader.__iter__()`` which spawns worker processes. For action
431
+ dataloaders using ``multiprocessing_context='spawn'``, each worker must
432
+ fully initialise heavy datasets (BridgeOrigLeRobotDataset, embodiment_a, etc.)
433
+ from scratch. If this happens lazily during training, the resulting
434
+ delay (potentially minutes) causes NCCL collective timeouts when faster
435
+ ranks enter the forward pass while slower ranks are still loading data.
436
+
437
+ By pulling one batch from every dataloader here — before any training
438
+ iteration — we ensure all workers are alive and warmed up. The fetched
439
+ samples are pushed into the per-dataloader buffer so they are consumed
440
+ normally by the first iteration that selects that dataloader.
441
+
442
+ A ``dist.barrier()`` at the end synchronises all ranks so that training
443
+ only begins once every rank has finished pre-warming.
444
+ """
445
+ import time
446
+
447
+ for i, (name, dl_iter) in enumerate(zip(self.dataset_name_list, self.dataloaders)):
448
+ t0 = time.monotonic()
449
+ try:
450
+ batch = next(dl_iter)
451
+ except StopIteration:
452
+ log.warning(f"Pre-warm: dataloader {name!r} is empty, skipping")
453
+ continue
454
+ elapsed = time.monotonic() - t0
455
+
456
+ # Split the collated batch into individual samples and push them
457
+ # into the buffer — identical to the splitting logic in
458
+ # _get_next_sample — so the samples are not wasted.
459
+ is_image_batch = "images" in batch
460
+ input_images_or_videos = batch["images" if is_image_batch else "video"]
461
+ batch_size = len(input_images_or_videos)
462
+
463
+ for j in range(batch_size):
464
+ sample = {}
465
+ for k, v in batch.items():
466
+ if k in _BATCH_TIMING_KEYS:
467
+ sample[k] = v
468
+ elif isinstance(v, list) and k in self._MULTI_ITEM_KEYS:
469
+ elem = v[j]
470
+ if isinstance(elem, list):
471
+ sample[k] = elem
472
+ else:
473
+ sample[k] = v[j : j + 1]
474
+ elif isinstance(v, list):
475
+ sample[k] = v[j]
476
+ elif isinstance(v, torch.Tensor) and v.dim() > 0:
477
+ sample[k] = v[j : j + 1]
478
+ else:
479
+ sample[k] = v[j : j + 1]
480
+ self.buffers[i].append(sample)
481
+
482
+ log.info(
483
+ f"Pre-warm: dataloader {name!r} ready — {batch_size} samples buffered in {elapsed:.1f}s",
484
+ rank0_only=False,
485
+ )
486
+
487
+ # Synchronise so training only starts once every rank is warmed up.
488
+ if torch.distributed.is_initialized():
489
+ log.info("Pre-warm: waiting at barrier for all ranks …")
490
+ torch.distributed.barrier()
491
+ log.info("Pre-warm: all ranks ready")
492
+
493
+ def _compute_num_tokens_per_sample(self, data_batch: dict) -> int:
494
+ """
495
+ This function computes the number of tokens per sample in the data batch.
496
+ This includes text + vision generation tokens + action tokens.
497
+
498
+ Args:
499
+ data_batch (dict): The data batch containing the text tokens.
500
+
501
+ Returns:
502
+ int: The number of tokens per sample.
503
+ """
504
+
505
+ # The token sequence we have is
506
+ # <text tokens> <eos> <vision_start> <image tokens> <vision_end> [<action tokens>]
507
+ # The spatial dimension of image tokens is compressed by
508
+ # vae spatial downsampling factor + pathification
509
+ # The temporal dimension of image tokens is compressed by
510
+ # vae temporal downsampling factor
511
+ # Action tokens have 1 token per time step (no spatial dimension)
512
+
513
+ has_text_tokens = "text_token_ids" in data_batch
514
+ num_tokens = 0
515
+ if has_text_tokens:
516
+ text_token_ids = data_batch["text_token_ids"]
517
+ if isinstance(text_token_ids, list):
518
+ num_text_tokens = text_token_ids[0].shape[0]
519
+ else:
520
+ num_text_tokens = text_token_ids.shape[1]
521
+ num_tokens += num_text_tokens + 1
522
+
523
+ # Vision part
524
+ is_image_batch = "images" in data_batch
525
+ input_images_or_videos = data_batch["images" if is_image_batch else "video"]
526
+ if "enable_per_camera_vae_encoding" in data_batch:
527
+ sample_n_views_values = read_positive_int_metadata(data_batch, "sample_n_views", expected_count=1)
528
+ frames_per_view_values = read_positive_int_metadata(
529
+ data_batch,
530
+ "num_video_frames_per_view",
531
+ expected_count=1,
532
+ )
533
+ if sample_n_views_values is None or frames_per_view_values is None:
534
+ raise ValueError(
535
+ "sample_n_views and num_video_frames_per_view are required when per-camera VAE encoding is enabled."
536
+ )
537
+ sample_n_views = sample_n_views_values[0]
538
+ frames_per_view = frames_per_view_values[0]
539
+ else:
540
+ sample_n_views = None
541
+ frames_per_view = None
542
+
543
+ # iterate over all the media in the batch
544
+ for media in input_images_or_videos if isinstance(input_images_or_videos, list) else [input_images_or_videos]:
545
+ if is_image_batch:
546
+ _, H, W = media.shape
547
+ T = 1
548
+ else:
549
+ _, T, H, W = media.shape
550
+
551
+ latent_h_shape = H // self.tokenizer_spatial_compression_factor
552
+ latent_w_shape = W // self.tokenizer_spatial_compression_factor
553
+ patch_h_shape = math.ceil(latent_h_shape / self.patch_spatial)
554
+ patch_w_shape = math.ceil(latent_w_shape / self.patch_spatial)
555
+ if sample_n_views is not None and frames_per_view is not None and not is_image_batch:
556
+ # The multiview dataset resizes every camera to the same H/W and
557
+ # selects the same synchronized frame count before concatenating
558
+ # the camera-major clips along T.
559
+ expected_frames = sample_n_views * frames_per_view
560
+ if T != expected_frames:
561
+ raise ValueError(
562
+ "Multiview vision length must equal sample_n_views * num_video_frames_per_view: "
563
+ f"got T={T}, sample_n_views={sample_n_views}, num_video_frames_per_view={frames_per_view}."
564
+ )
565
+ latent_t_shape = sample_n_views * self._compute_vision_latent_t_shape(frames_per_view, H, W)
566
+ else:
567
+ latent_t_shape = self._compute_vision_latent_t_shape(T, H, W)
568
+
569
+ num_vision_tokens = patch_h_shape * patch_w_shape * latent_t_shape
570
+ if has_text_tokens:
571
+ num_vision_tokens += 2
572
+ num_tokens += num_vision_tokens
573
+
574
+ # Action part: each action time step is 1 token.
575
+ # Action tensor shape is (T_action, D) per sample; stored as a single-element list.
576
+ if "action" in data_batch:
577
+ list_of_actions = data_batch["action"]
578
+ for action in list_of_actions:
579
+ # skip None actions
580
+ if action is None:
581
+ continue
582
+ num_action_tokens = action.shape[0]
583
+ num_tokens += num_action_tokens
584
+
585
+ # Sound part — estimate sound tokens from audio waveform length
586
+ if self.sound_latent_fps > 0 and "sound" in data_batch:
587
+ sound_data = data_batch["sound"]
588
+ if isinstance(sound_data, list) and len(sound_data) > 0:
589
+ first_sound = sound_data[0]
590
+ # Unwrap nested list if needed
591
+ if isinstance(first_sound, list):
592
+ first_sound = first_sound[0]
593
+ if first_sound is not None and isinstance(first_sound, torch.Tensor):
594
+ num_audio_samples = first_sound.shape[-1]
595
+ audio_duration = num_audio_samples / self.audio_sample_rate
596
+ num_sound_tokens = int(audio_duration * self.sound_latent_fps)
597
+ num_tokens += num_sound_tokens
598
+
599
+ return num_tokens
600
+
601
+ # Keys whose value per sample is a list of tensors to be flattened into one list in the batch
602
+ _FLATTEN_LIST_KEYS = {"image_size"}
603
+
604
+ def _update_output_batch(self, output_batch: dict, output: dict):
605
+ for key, value in output.items():
606
+ if key in _BATCH_TIMING_KEYS:
607
+ if key not in output_batch:
608
+ output_batch[key] = value
609
+ elif key in self._FLATTEN_LIST_KEYS and isinstance(value, list):
610
+ if key not in output_batch:
611
+ output_batch[key] = value
612
+ else:
613
+ output_batch[key].extend(value)
614
+ elif key not in output_batch:
615
+ output_batch[key] = [value]
616
+ else:
617
+ output_batch[key].append(value)
618
+
619
+ def __len__(self) -> int:
620
+ return self.data_len
621
+
622
+ # Keys where each sample may hold multiple tensors (e.g. multiple video
623
+ # clips in a packed sequence). Kept as single-element lists per sample
624
+ # via v[i:i+1] so that _update_output_batch yields list[list[Tensor]].
625
+ _MULTI_ITEM_KEYS = {"text_token_ids", "images", "video", "action", "action_raw", "sound"}
626
+
627
+ def _get_next_sample(self, index_id: int) -> dict:
628
+ """Pop the next single-sample dict from the buffer for the given dataloader.
629
+
630
+ If the buffer is empty, fetches the next collated batch from the inner
631
+ dataloader and splits it into individual samples.
632
+
633
+ Splitting rules:
634
+ - Multi-item list values (keys in ``_MULTI_ITEM_KEYS``): sliced
635
+ via ``v[i:i+1]`` to yield a single-element list ``[tensor]``.
636
+ A packed sequence can contain multiple items per key.
637
+ - Per-sequence metadata list values (all other list keys, e.g.
638
+ ``sequence_plan``, ``domain_id``): direct-indexed via ``v[i]``
639
+ to yield the bare element.
640
+ - Tensor values ``(B, ...)``: sliced to ``(1, ...)`` via
641
+ ``v[i : i + 1]`` to preserve the batch dimension.
642
+
643
+ After ``_update_output_batch`` accumulates samples, the packed output
644
+ batch has the following shapes:
645
+ - Multi-item keys (``text_token_ids``, ``video``, ``images``,
646
+ ``action_raw``, ``action``): ``list[list[Tensor]]`` — each inner
647
+ list has one element from one sub-sample.
648
+ - Per-sequence metadata keys (``sequence_plan``, ``domain_id``,
649
+ ``dataset_name``, etc.): ``list[element]`` — flat list.
650
+ - Tensor-origin keys: ``list[Tensor(1, ...)]``.
651
+
652
+ Args:
653
+ index_id: Index of the dataloader to fetch from.
654
+
655
+ Returns:
656
+ A single-sample dictionary.
657
+ """
658
+ buffer = self.buffers[index_id]
659
+ if not buffer:
660
+ try:
661
+ batch = next(self.dataloaders[index_id])
662
+ except StopIteration:
663
+ raise
664
+
665
+ is_image_batch = "images" in batch
666
+ input_images_or_videos = batch["images" if is_image_batch else "video"]
667
+ batch_size = len(input_images_or_videos)
668
+
669
+ for i in range(batch_size):
670
+ sample = {}
671
+ for k, v in batch.items():
672
+ if k in _BATCH_TIMING_KEYS:
673
+ sample[k] = v
674
+ elif isinstance(v, list) and k in self._MULTI_ITEM_KEYS:
675
+ # For multi-item keys (images, video, etc.), the collated
676
+ # value is a list with one element per sample. If the element
677
+ # is itself a list (e.g. image editing: [src, tgt]), use v[i]
678
+ # directly to avoid wrapping it in a redundant single-element
679
+ # list. Otherwise keep the v[i:i+1] slice so that
680
+ # _update_output_batch produces list[list[Tensor]].
681
+ elem = v[i]
682
+ if isinstance(elem, list):
683
+ sample[k] = elem
684
+ else:
685
+ sample[k] = v[i : i + 1]
686
+ elif isinstance(v, list):
687
+ sample[k] = v[i]
688
+ else:
689
+ sample[k] = v[i : i + 1]
690
+ buffer.append(sample)
691
+
692
+ return buffer.popleft()
693
+
694
+ def set_start_iteration(self, iteration: int):
695
+ self.global_id = iteration
696
+
697
+ def __iter__(self):
698
+ raise NotImplementedError("__iter__ function is not implemented yet")
699
+
700
+
701
+ class IterativeJointDataLoader(JointDataLoader):
702
+ r"""
703
+ An iterative joint dataloader that supports loading multiple modalities.
704
+
705
+ The behavior depends on the ``seed`` parameter:
706
+
707
+ - **seed is not None** (Default):
708
+ The modality is randomly selected at each iteration based on the probability distribution
709
+ derived from the ratios. The random state is seeded with ``seed + global_id``, ensuring
710
+ that all ranks select the same modality at the same iteration (assuming synchronized global_id).
711
+ This prevents load imbalance due to mixed resolutions across ranks.
712
+
713
+ - **seed is None**:
714
+ The modality selection follows a deterministic round-robin pattern based on the ratios.
715
+ For example, with 2 modalities (image and video) and ratio 2:1:
716
+ - Iterations 0, 1: all ranks process images
717
+ - Iteration 2: all ranks process videos
718
+ - ... and so on.
719
+ This also ensures all ranks process the same modality at the same iteration.
720
+
721
+ ``enable_async_batch_building=True`` advances this same batch iterator on
722
+ one daemon thread and builds exactly one future batch. The default keeps
723
+ the original calling-thread behavior.
724
+ """
725
+
726
+ def __init__(
727
+ self,
728
+ dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]],
729
+ tokenizer_spatial_compression_factor: int,
730
+ tokenizer_temporal_compression_factor: int,
731
+ patch_spatial: int,
732
+ max_sequence_length: int | None = None,
733
+ max_samples_per_batch: int | None = None,
734
+ sound_latent_fps: float = 0,
735
+ audio_sample_rate: int = 48000,
736
+ seed: int | None = 42,
737
+ prewarm: bool = True,
738
+ default_lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT,
739
+ lookahead_limits: Dict[str, int] | None = None,
740
+ uniae_chunk_frames: int | Mapping[str, int] | None = None,
741
+ uniae_pad_frames: int | None = None,
742
+ enable_async_batch_building: bool = False,
743
+ async_batch_building_timeout_s: float = 1200.0,
744
+ ) -> None:
745
+ if async_batch_building_timeout_s <= 0:
746
+ raise ValueError(f"async_batch_building_timeout_s must be positive, got {async_batch_building_timeout_s}.")
747
+ # Keep PyTorch/WebDataset/Lance worker creation on the main thread.
748
+ # The packer thread starts only after constructor prewarm has completed.
749
+ if enable_async_batch_building and not prewarm:
750
+ raise ValueError("enable_async_batch_building=True requires prewarm=True.")
751
+
752
+ super().__init__(
753
+ dataloaders,
754
+ tokenizer_spatial_compression_factor,
755
+ tokenizer_temporal_compression_factor,
756
+ patch_spatial,
757
+ max_sequence_length,
758
+ max_samples_per_batch,
759
+ sound_latent_fps=sound_latent_fps,
760
+ audio_sample_rate=audio_sample_rate,
761
+ prewarm=prewarm,
762
+ default_lookahead_limit=default_lookahead_limit,
763
+ lookahead_limits=lookahead_limits,
764
+ uniae_chunk_frames=uniae_chunk_frames,
765
+ uniae_pad_frames=uniae_pad_frames,
766
+ )
767
+ self.seed = seed
768
+ # Calculate probabilities for random sampling
769
+ total_ratio = sum(self.data_ratios)
770
+ self.data_probs = np.array([ratio / total_ratio for ratio in self.data_ratios])
771
+ # Async batch builder state
772
+ self.enable_async_batch_building: bool = enable_async_batch_building
773
+ self.async_batch_building_timeout_s: float = float(async_batch_building_timeout_s)
774
+ self._async_batch_builder: _AsyncBatchBuilder | None = None
775
+
776
+ def __iter__(self) -> Iterator[dict[str, Any]]:
777
+ if not self.enable_async_batch_building:
778
+ return self._iter_synchronous()
779
+ return self._iter_asynchronous()
780
+
781
+ def _iter_synchronous(self) -> Iterator[dict[str, Any]]:
782
+ """Run the existing packing path on the calling thread."""
783
+ while True:
784
+ if self.seed is not None:
785
+ rng = np.random.RandomState(self.seed + self.global_id)
786
+ index_id = rng.choice(len(self.dataloader_list), p=self.data_probs)
787
+ else:
788
+ data_id = self.global_id % self.ratio_sum
789
+ index_id = self._get_dataloader_index(data_id)
790
+
791
+ metrics = _PackingMetrics()
792
+ output_batch = dict()
793
+ skipped_samples = deque()
794
+ lookahead_limit = self.lookahead_limits[index_id]
795
+ lookahead_count = 0
796
+
797
+ while True:
798
+ # Check max samples limit first
799
+ if self.max_samples_per_batch is not None and metrics.num_samples >= self.max_samples_per_batch:
800
+ break
801
+
802
+ # If we have started packing and tried lookahead_limit times to find a fitting sample but failed, stop.
803
+ if len(output_batch) > 0 and lookahead_count >= lookahead_limit:
804
+ break
805
+
806
+ had_buffer = len(self.buffers[index_id]) > 0
807
+ try:
808
+ output = self._get_next_sample(index_id)
809
+ except StopIteration:
810
+ break # No more data in this dataloader
811
+
812
+ if had_buffer:
813
+ metrics.from_buffer += 1
814
+ else:
815
+ metrics.from_workers += 1
816
+
817
+ num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output)
818
+
819
+ if (
820
+ self.max_sequence_length is not None
821
+ and metrics.current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length
822
+ ):
823
+ if len(output_batch) == 0:
824
+ # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length
825
+ # In this case, we should simply discard the current sample and get the next sample.
826
+ log.info(
827
+ f"Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}",
828
+ rank0_only=False,
829
+ )
830
+ metrics.dropped_count += 1
831
+ continue
832
+
833
+ # current_sequence_length > 0 and selected sample is too large to fit in the remaining space.
834
+ # Instead of stopping immediately (creating large padding), we buffer this large sample
835
+ # and try to find a smaller one that fits in the remaining space.
836
+ skipped_samples.append(output)
837
+ lookahead_count += 1
838
+ continue
839
+
840
+ metrics.current_sequence_length += num_tokens_in_current_sample
841
+ metrics.num_samples += 1
842
+ output["dataset_name"] = self.dataset_name_list[index_id]
843
+ self._update_output_batch(output_batch, output)
844
+
845
+ # Add back skipped samples to the buffer for the next batch.
846
+ # appendleft puts item at HEAD. So we insert S3, then S2, then S1.
847
+ for sample in reversed(skipped_samples):
848
+ self.buffers[index_id].appendleft(sample)
849
+
850
+ if len(output_batch) == 0:
851
+ return
852
+
853
+ metrics.attach_to(output_batch, buffer_size=len(self.buffers[index_id]))
854
+ self.global_id += 1
855
+ yield output_batch
856
+
857
+ def _start_async_batch_builder(self) -> _AsyncBatchBuilder:
858
+ """Create the async builder and request its first batch."""
859
+ if self._async_batch_builder is not None:
860
+ raise RuntimeError("Async batch building has already started.")
861
+ async_batch_builder = _AsyncBatchBuilder(
862
+ self._iter_synchronous(),
863
+ timeout_s=self.async_batch_building_timeout_s,
864
+ )
865
+ self._async_batch_builder = async_batch_builder
866
+ async_batch_builder.request_batch(self.global_id)
867
+ return async_batch_builder
868
+
869
+ def _iter_asynchronous(self) -> Iterator[dict[str, Any]]:
870
+ """Yield each completed batch while the async builder builds its replacement."""
871
+ async_batch_builder = self._async_batch_builder or self._start_async_batch_builder()
872
+
873
+ while True:
874
+ try:
875
+ batch = async_batch_builder.get_batch()
876
+ except StopIteration:
877
+ break
878
+
879
+ # Request one replacement before yielding. The request queue contains only a
880
+ # sequence ID, and the result queue returns this exact batch object by reference.
881
+ async_batch_builder.request_batch(self.global_id)
882
+ yield batch
883
+
884
+ def set_start_iteration(self, iteration: int) -> None:
885
+ """Set the batch sequence and eagerly request the first async batch."""
886
+ if self._async_batch_builder is not None:
887
+ raise RuntimeError("set_start_iteration must be called before async batch building starts.")
888
+ self.global_id = iteration
889
+ if self.enable_async_batch_building:
890
+ self._start_async_batch_builder()
891
+
892
+ def close(self) -> None:
893
+ """Permanently stop the optional async batch builder."""
894
+ if self._async_batch_builder is not None:
895
+ # Keep the closed builder reference: its bounded join may return while the daemon
896
+ # is still blocked in data loading, so starting a replacement could race with it.
897
+ self._async_batch_builder.close()
898
+
899
+ def _get_dataloader_index(self, data_id: float) -> int:
900
+ """Maps global id to the corresponding dataloader index based on ratio."""
901
+ for i, r in enumerate(self.data_ratios):
902
+ if data_id < r:
903
+ return i
904
+ data_id -= r
905
+ raise ValueError("Invalid data_id")
906
+
907
+
908
+ class RankPartitionedDataLoader:
909
+ """Assigns each rank to exactly one dataset based on ratios.
910
+
911
+ For N GPUs with datasets having ratios r_1:r_2:...:r_k, the first
912
+ N * r_1 / sum(r) ranks are assigned dataset 1, the next N * r_2 / sum(r)
913
+ ranks are assigned dataset 2, etc. Each rank instantiates a single
914
+ PyTorch DataLoader for its assigned dataset.
915
+
916
+ The sharding information (``shard_world_size`` and ``shard_rank``) is set
917
+ on each dataset so that it shards data only across ranks that share the
918
+ same dataset, rather than across the full world.
919
+
920
+ Example:
921
+ With 128 GPUs and datasets ``{"video": {"dataset": ..., "ratio": 3},
922
+ "image": {"dataset": ..., "ratio": 1}}``:
923
+
924
+ - Ranks 0-95 -> video (shard_world_size=96, shard_rank=0..95)
925
+ - Ranks 96-127 -> image (shard_world_size=32, shard_rank=0..31)
926
+ """
927
+
928
+ def __init__(
929
+ self,
930
+ datasets: dict[str, dict[str, Any]],
931
+ **dataloader_kwargs: Any,
932
+ ):
933
+ """
934
+ Args:
935
+ datasets: Mapping of dataset name to config dict with keys:
936
+
937
+ - ``"dataset"`` (required): a lazy config or dataset instance.
938
+ - ``"ratio"`` (required): positive int weight.
939
+ - ``"dataloader_kwargs"`` (optional): dict of keyword arguments
940
+ that override the top-level ``**dataloader_kwargs`` for this
941
+ dataset only (e.g. different ``num_workers`` or ``batch_size``).
942
+
943
+ **dataloader_kwargs: Default kwargs forwarded to
944
+ ``torch.utils.data.DataLoader``. ``collate_fn`` defaults to
945
+ ``custom_collate_fn`` if not given.
946
+ """
947
+ world_size = torch.distributed.get_world_size()
948
+ rank = torch.distributed.get_rank()
949
+ log.info(f"RankPartitionedDataLoader: world_size: {world_size} and rank: {rank}", rank0_only=False)
950
+
951
+ _VALID_KEYS = {"dataset", "ratio", "dataloader_kwargs"}
952
+ names: list[str] = []
953
+ dataset_configs: list[Any] = []
954
+ ratios: list[int] = []
955
+ per_dataset_kwargs: list[dict[str, Any]] = []
956
+ for name, cfg in datasets.items():
957
+ extra = set(cfg.keys()) - _VALID_KEYS
958
+ assert not extra, f"Dataset {name!r}: unexpected keys {extra}. Allowed: {_VALID_KEYS}"
959
+ if cfg["ratio"] <= 0:
960
+ log.warning(
961
+ f"RankPartitionedDataLoader: Skipping dataset {name} with ratio {cfg['ratio']}", rank0_only=False
962
+ )
963
+ continue
964
+ names.append(name)
965
+ dataset_configs.append(cfg["dataset"])
966
+ ratios.append(cfg["ratio"])
967
+ per_dataset_kwargs.append(cfg.get("dataloader_kwargs", {}))
968
+
969
+ assert len(names) > 0, "No datasets with positive ratios provided."
970
+ assert world_size >= len(names), (
971
+ f"world_size ({world_size}) must be >= number of datasets ({len(names)}) "
972
+ f"so each dataset gets at least one rank."
973
+ )
974
+
975
+ total_ratio = sum(ratios)
976
+ ideal = [r / total_ratio * world_size for r in ratios]
977
+ allocations = [max(1, int(q)) for q in ideal]
978
+ remaining = world_size - sum(allocations)
979
+ if remaining > 0:
980
+ remainders = sorted(range(len(ratios)), key=lambda i: ideal[i] - allocations[i], reverse=True)
981
+ for j in range(remaining):
982
+ allocations[remainders[j]] += 1
983
+ elif remaining < 0:
984
+ deficit = -remaining
985
+ while deficit > 0:
986
+ best = max(
987
+ (i for i in range(len(allocations)) if allocations[i] > 1),
988
+ key=lambda i: (allocations[i] - ideal[i], allocations[i]),
989
+ )
990
+ allocations[best] -= 1
991
+ deficit -= 1
992
+
993
+ expected_ratios = [r / total_ratio for r in ratios]
994
+ actual_ratios = [a / world_size for a in allocations]
995
+ lines = [f"RankPartitionedDataLoader allocation ({world_size} GPUs):"]
996
+ start = 0
997
+ for i, (name, alloc) in enumerate(zip(names, allocations)):
998
+ end = start + alloc - 1
999
+ lines.append(
1000
+ f" {name} (ratio {ratios[i]}): ranks {start}-{end} ({alloc} GPUs) "
1001
+ f"| expected {expected_ratios[i]:.2%}, actual {actual_ratios[i]:.2%}"
1002
+ )
1003
+ start += alloc
1004
+ log.info("\n".join(lines), rank0_only=False)
1005
+
1006
+ cumulative = 0
1007
+ my_dataset_idx = -1
1008
+ for i, alloc in enumerate(allocations):
1009
+ if rank < cumulative + alloc:
1010
+ my_dataset_idx = i
1011
+ break
1012
+ cumulative += alloc
1013
+ assert my_dataset_idx >= 0
1014
+
1015
+ shard_rank = rank - cumulative
1016
+ shard_world_size = allocations[my_dataset_idx]
1017
+
1018
+ dataset: Any = instantiate(dataset_configs[my_dataset_idx])
1019
+ dataset.shard_world_size = shard_world_size
1020
+ dataset.shard_rank = shard_rank
1021
+ dataset.shard_id = my_dataset_idx
1022
+
1023
+ merged_kwargs = {**dataloader_kwargs, **per_dataset_kwargs[my_dataset_idx]}
1024
+ merged_kwargs.setdefault("collate_fn", custom_collate_fn)
1025
+ self.dataloader = torch.utils.data.DataLoader(dataset, **merged_kwargs)
1026
+ self.dataset_name = names[my_dataset_idx]
1027
+ self.dataset = dataset
1028
+
1029
+ def __iter__(self):
1030
+ return iter(self.dataloader)
1031
+
1032
+ def __len__(self) -> int:
1033
+ return len(self.dataloader)
1034
+
1035
+
1036
+ class PackingDataLoader(JointDataLoader):
1037
+ """Packs multiple samples from a single dataloader into token-budget-constrained batches.
1038
+
1039
+ Unlike the other ``JointDataLoader`` subclasses which manage multiple
1040
+ dataloaders with configurable ratios, this class wraps a single dataloader
1041
+ and greedily packs consecutive samples until the token budget
1042
+ (``max_sequence_length``) or sample count limit (``max_samples_per_batch``)
1043
+ is reached.
1044
+ """
1045
+
1046
+ def __init__(
1047
+ self,
1048
+ dataloader: torch.utils.data.DataLoader | webdataset.WebLoader,
1049
+ tokenizer_spatial_compression_factor: int,
1050
+ tokenizer_temporal_compression_factor: int,
1051
+ patch_spatial: int,
1052
+ max_sequence_length: int | None = None,
1053
+ max_samples_per_batch: int | None = None,
1054
+ sound_latent_fps: float = 0,
1055
+ audio_sample_rate: int = 48000,
1056
+ dataset_name: str = "default",
1057
+ lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT,
1058
+ uniae_chunk_frames: int | Mapping[str, int] | None = None,
1059
+ uniae_pad_frames: int | None = None,
1060
+ ):
1061
+ """
1062
+ Args:
1063
+ dataloader: A single dataloader (or lazy config) to draw samples from.
1064
+ tokenizer_spatial_compression_factor: Spatial compression factor of the tokenizer.
1065
+ tokenizer_temporal_compression_factor: Temporal compression factor of the tokenizer.
1066
+ patch_spatial: Spatial patchification factor.
1067
+ max_sequence_length: Max total tokens per packed batch. Mutually exclusive with
1068
+ ``max_samples_per_batch``.
1069
+ max_samples_per_batch: Max number of samples per packed batch. Mutually exclusive
1070
+ with ``max_sequence_length``.
1071
+ sound_latent_fps: Sound tokenizer latent rate in Hz. If 0, sound tokens are not counted.
1072
+ audio_sample_rate: Audio sample rate in Hz.
1073
+ dataset_name: Name tag attached to every sample in the output batch.
1074
+ lookahead_limit: Packing-loop look-ahead for the wrapped dataloader.
1075
+ uniae_chunk_frames: Optional UniAE full chunk size, or resolution-keyed chunk sizes.
1076
+ uniae_pad_frames: Optional UniAE boundary padding frames per chunk.
1077
+ """
1078
+ wrapped = {dataset_name: {"dataloader": dataloader, "ratio": 1}}
1079
+ super().__init__(
1080
+ dataloaders=wrapped,
1081
+ tokenizer_spatial_compression_factor=tokenizer_spatial_compression_factor,
1082
+ tokenizer_temporal_compression_factor=tokenizer_temporal_compression_factor,
1083
+ patch_spatial=patch_spatial,
1084
+ max_sequence_length=max_sequence_length,
1085
+ max_samples_per_batch=max_samples_per_batch,
1086
+ sound_latent_fps=sound_latent_fps,
1087
+ audio_sample_rate=audio_sample_rate,
1088
+ lookahead_limits={dataset_name: int(lookahead_limit)},
1089
+ uniae_chunk_frames=uniae_chunk_frames,
1090
+ uniae_pad_frames=uniae_pad_frames,
1091
+ )
1092
+
1093
+ def __iter__(self):
1094
+ inner = self.dataloader_list[0]
1095
+ ds_name = getattr(inner, "dataset_name", self.dataset_name_list[0])
1096
+
1097
+ while True:
1098
+ current_sequence_length = 0
1099
+ num_samples = 0
1100
+ output_batch: dict = {}
1101
+
1102
+ skipped_samples: deque = deque()
1103
+ # PackingDataLoader wraps a single dataloader, so lookahead_limits has one entry.
1104
+ lookahead_limit = self.lookahead_limits[0]
1105
+ lookahead_count = 0
1106
+
1107
+ while True:
1108
+ if self.max_samples_per_batch is not None and num_samples >= self.max_samples_per_batch:
1109
+ break
1110
+
1111
+ if len(output_batch) > 0 and lookahead_count >= lookahead_limit:
1112
+ break
1113
+
1114
+ try:
1115
+ output = self._get_next_sample(0)
1116
+ except StopIteration:
1117
+ break
1118
+
1119
+ num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output)
1120
+
1121
+ if (
1122
+ self.max_sequence_length is not None
1123
+ and current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length
1124
+ ):
1125
+ if len(output_batch) == 0:
1126
+ # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length
1127
+ # In this case, we should simply discard the current sample and get the next sample.
1128
+ log.error(
1129
+ f"PackingDataLoader: Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}",
1130
+ rank0_only=False,
1131
+ )
1132
+ continue
1133
+
1134
+ skipped_samples.append(output)
1135
+ lookahead_count += 1
1136
+ continue
1137
+
1138
+ current_sequence_length += num_tokens_in_current_sample
1139
+ num_samples += 1
1140
+ # Allows the dataset name to be overridden by the sample itself.
1141
+ output["dataset_name"] = output.get("dataset_name", ds_name)
1142
+ self._update_output_batch(output_batch, output)
1143
+
1144
+ for sample in reversed(skipped_samples):
1145
+ self.buffers[0].appendleft(sample)
1146
+
1147
+ if len(output_batch) == 0:
1148
+ return
1149
+
1150
+ self.global_id += 1
1151
+ yield output_batch
1152
+
1153
+
1154
+ class RandomJointDataLoader(JointDataLoader):
1155
+ r"""
1156
+ A random joint dataloader that supports loading multiple modalities with stochastic sampling.
1157
+
1158
+ In this dataloader, the modality is randomly selected at each iteration based on the
1159
+ probability distribution derived from the ratios. Each rank independently samples a
1160
+ modality, so different ranks may process different modalities at the same iteration.
1161
+
1162
+ For example, with 2 modalities (image and video) and ratio 2:1:
1163
+ - Each iteration has 66.7% probability of selecting images
1164
+ - Each iteration has 33.3% probability of selecting videos
1165
+ - The selection is independent across iterations and ranks
1166
+
1167
+ Note: Unlike IterativeJointDataLoader, this does not guarantee synchronized modality
1168
+ selection across ranks.
1169
+ """
1170
+
1171
+ def __init__(
1172
+ self,
1173
+ dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]],
1174
+ tokenizer_spatial_compression_factor: int,
1175
+ tokenizer_temporal_compression_factor: int,
1176
+ patch_spatial: int,
1177
+ max_sequence_length: int | None = None,
1178
+ max_samples_per_batch: int | None = None,
1179
+ sound_latent_fps: float = 0,
1180
+ audio_sample_rate: int = 48000,
1181
+ default_lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT,
1182
+ lookahead_limits: Dict[str, int] | None = None,
1183
+ uniae_chunk_frames: int | Mapping[str, int] | None = None,
1184
+ uniae_pad_frames: int | None = None,
1185
+ ):
1186
+ super().__init__(
1187
+ dataloaders,
1188
+ tokenizer_spatial_compression_factor,
1189
+ tokenizer_temporal_compression_factor,
1190
+ patch_spatial,
1191
+ max_sequence_length,
1192
+ max_samples_per_batch,
1193
+ sound_latent_fps=sound_latent_fps,
1194
+ audio_sample_rate=audio_sample_rate,
1195
+ default_lookahead_limit=default_lookahead_limit,
1196
+ lookahead_limits=lookahead_limits,
1197
+ uniae_chunk_frames=uniae_chunk_frames,
1198
+ uniae_pad_frames=uniae_pad_frames,
1199
+ )
1200
+
1201
+ # Convert data ratios to probabilities
1202
+ self.data_ratios = np.array([ratio / sum(self.data_ratios) for ratio in self.data_ratios])
1203
+
1204
+ def __iter__(self):
1205
+ while True:
1206
+ index_id = np.random.choice(len(self.dataloader_list), p=self.data_ratios)
1207
+
1208
+ metrics = _PackingMetrics()
1209
+ output_batch = dict()
1210
+ skipped_samples = deque()
1211
+ lookahead_limit = self.lookahead_limits[index_id]
1212
+ lookahead_count = 0
1213
+
1214
+ while True:
1215
+ # Check max samples limit first
1216
+ if self.max_samples_per_batch is not None and metrics.num_samples >= self.max_samples_per_batch:
1217
+ break
1218
+
1219
+ # If we have started packing and tried lookahead_limit times to find a fitting sample but failed, stop.
1220
+ if len(output_batch) > 0 and lookahead_count >= lookahead_limit:
1221
+ break
1222
+
1223
+ had_buffer = len(self.buffers[index_id]) > 0
1224
+ try:
1225
+ output = self._get_next_sample(index_id)
1226
+ except StopIteration:
1227
+ break # No more data in this dataloader
1228
+
1229
+ if had_buffer:
1230
+ metrics.from_buffer += 1
1231
+ else:
1232
+ metrics.from_workers += 1
1233
+
1234
+ num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output)
1235
+
1236
+ if (
1237
+ self.max_sequence_length is not None
1238
+ and metrics.current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length
1239
+ ):
1240
+ if len(output_batch) == 0:
1241
+ # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length
1242
+ # In this case, we should simply discard the current sample and get the next sample.
1243
+ log.info(
1244
+ f"Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}",
1245
+ rank0_only=False,
1246
+ )
1247
+ metrics.dropped_count += 1
1248
+ continue
1249
+
1250
+ # current_sequence_length > 0 and selected sample is too large to fit in the remaining space.
1251
+ # Instead of stopping immediately (creating large padding), we buffer this large sample
1252
+ # and try to find a smaller one that fits in the remaining space.
1253
+ skipped_samples.append(output)
1254
+ lookahead_count += 1
1255
+ continue
1256
+
1257
+ metrics.current_sequence_length += num_tokens_in_current_sample
1258
+ metrics.num_samples += 1
1259
+ output["dataset_name"] = self.dataset_name_list[index_id]
1260
+ self._update_output_batch(output_batch, output)
1261
+
1262
+ # Add back skipped samples to the buffer for the next batch.
1263
+ # appendleft puts item at HEAD. So we insert S3, then S2, then S1.
1264
+ for sample in reversed(skipped_samples):
1265
+ self.buffers[index_id].appendleft(sample)
1266
+
1267
+ if len(output_batch) == 0:
1268
+ return
1269
+
1270
+ metrics.attach_to(output_batch, buffer_size=len(self.buffers[index_id]))
1271
+ yield output_batch
releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/modality.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Modality utility helpers for VFM sequence packing."""
5
+
6
+ from dataclasses import dataclass, field
7
+
8
+ import torch
9
+
10
+
11
+ def _empty_long_tensor() -> torch.Tensor:
12
+ return torch.empty(0, dtype=torch.long) # [0]
13
+
14
+
15
+ def _empty_float_tensor() -> torch.Tensor:
16
+ return torch.empty(0, dtype=torch.float32) # [0]
17
+
18
+
19
+ @dataclass
20
+ class ModalitySpan:
21
+ """One contiguous packed span paired with its logical modality payload slice.
22
+
23
+ Attributes:
24
+ sequence_start: First global packed-sequence index owned by this span.
25
+ sequence_len: Number of contiguous tokens in the packed sequence.
26
+ payload_index: Index into ``ModalityData.tokens`` for the backing payload tensor.
27
+ payload_start: First flattened token offset within the backing payload tensor.
28
+ payload_len: Number of flattened payload tokens covered by this span.
29
+ payload_shape: Logical payload slice shape. For example, vision frame spans use
30
+ ``(1, patch_h, patch_w)``, action spans use ``(tcf,)``, and sound spans use
31
+ ``(1, 1, 1)``.
32
+ """
33
+
34
+ sequence_start: int
35
+ sequence_len: int
36
+ payload_index: int
37
+ payload_start: int
38
+ payload_len: int
39
+ payload_shape: tuple[int, ...]
40
+
41
+
42
+ @dataclass
43
+ class ModalityDataBuilder:
44
+ """Append-only construction state for a single generation modality.
45
+
46
+ Attributes:
47
+ spans: Contiguous packed spans pointing back into grouped payload tensors.
48
+ sequence_indexes: Global packed-sequence indexes for all tokens of this modality.
49
+ timesteps: Diffusion timesteps for noised tokens only.
50
+ mse_loss_indexes: Global packed-sequence indexes where MSE loss should be computed.
51
+ token_shapes: Shape metadata for each payload tensor. Vision and sound use
52
+ ``(T, H, W)``-style tuples; action uses ``(T,)``.
53
+ tokens: Original modality payload tensors, kept grouped by sample/item.
54
+ condition_mask: Per-payload masks where 1 indicates clean/conditioning tokens
55
+ and 0 indicates noised/supervised tokens.
56
+ noisy_frame_indexes: Per-payload indexes of noised frames. These are constructed
57
+ during packing to avoid GPU-to-CPU synchronization later.
58
+ """
59
+
60
+ spans: list[ModalitySpan] = field(default_factory=list)
61
+ sequence_indexes: list[int] = field(default_factory=list)
62
+ timesteps: list[float] = field(default_factory=list)
63
+ mse_loss_indexes: list[int] = field(default_factory=list)
64
+ # list[tuple[int,int,int]] for vision, list[tuple[int]] for action, list[tuple[int,int,int]] for sound
65
+ token_shapes: list[tuple[int, ...]] = field(default_factory=list)
66
+ tokens: list[torch.Tensor] = field(default_factory=list)
67
+ condition_mask: list[torch.Tensor] = field(default_factory=list)
68
+ noisy_frame_indexes: list[torch.Tensor] = field(default_factory=list)
69
+
70
+
71
+ @dataclass
72
+ class ModalityData:
73
+ """Finalized model-facing data for a single generation modality.
74
+
75
+ Index-like fields are tensors after packing. Payload fields remain lists
76
+ because samples may have variable token shapes and because downstream code
77
+ mutates token payloads after noise injection and clean replay.
78
+
79
+ Attributes:
80
+ sequence_indexes: Tensor of global packed-sequence indexes for all tokens of
81
+ this modality.
82
+ timesteps: Tensor of diffusion timesteps for noised tokens only.
83
+ mse_loss_indexes: Tensor of global packed-sequence indexes where MSE loss
84
+ should be computed.
85
+ spans: Contiguous packed spans pointing back into grouped payload tensors.
86
+ token_shapes: Shape metadata for each payload tensor. Vision and sound use
87
+ ``(T, H, W)``-style tuples; action uses ``(T,)``.
88
+ tokens: Original modality payload tensors, kept grouped by sample/item.
89
+ condition_mask: Per-payload masks where 1 indicates clean/conditioning tokens
90
+ and 0 indicates noised/supervised tokens.
91
+ noisy_frame_indexes: Per-payload indexes of noised frames.
92
+ domain_id: Domain IDs for multi-domain training. Only used for action.
93
+ raw_action_dim: Raw action dimensions. Only used for action-channel masking.
94
+ """
95
+
96
+ sequence_indexes: torch.Tensor = field(default_factory=_empty_long_tensor)
97
+ timesteps: torch.Tensor = field(default_factory=_empty_float_tensor)
98
+ mse_loss_indexes: torch.Tensor = field(default_factory=_empty_long_tensor)
99
+ spans: list[ModalitySpan] = field(default_factory=list)
100
+ # list[tuple[int,int,int]] for vision, list[tuple[int]] for action, list[tuple[int,int,int]] for sound
101
+ token_shapes: list[tuple[int, ...]] = field(default_factory=list)
102
+ tokens: list[torch.Tensor] = field(default_factory=list)
103
+ condition_mask: list[torch.Tensor] = field(default_factory=list)
104
+ noisy_frame_indexes: list[torch.Tensor] = field(default_factory=list)
105
+ domain_id: list[torch.Tensor] = field(default_factory=list)
106
+ raw_action_dim: list[torch.Tensor | None] | None = field(default_factory=list)
107
+ # Optional per-token gate for an action residual expert.
108
+ residual_gate: list[torch.Tensor] | None = None
109
+
110
+ def __post_init__(self) -> None:
111
+ assert isinstance(self.sequence_indexes, torch.Tensor), "ModalityData.sequence_indexes must be finalized"
112
+ assert isinstance(self.timesteps, torch.Tensor), "ModalityData.timesteps must be finalized"
113
+ assert isinstance(self.mse_loss_indexes, torch.Tensor), "ModalityData.mse_loss_indexes must be finalized"
114
+
115
+ def to_cuda(self) -> None:
116
+ """Move all tensor fields to CUDA in-place."""
117
+ self.sequence_indexes = self.sequence_indexes.cuda()
118
+ self.timesteps = self.timesteps.cuda()
119
+ self.mse_loss_indexes = self.mse_loss_indexes.cuda()
120
+ self.tokens = [token.cuda() for token in self.tokens]
121
+ self.condition_mask = [cm.cuda() for cm in self.condition_mask]
122
+ self.noisy_frame_indexes = [ni.cuda() for ni in self.noisy_frame_indexes]
123
+ self.domain_id = [d.cuda() for d in self.domain_id]
124
+ # raw_action_dim is optional (e.g., when action-channel masking is disabled).
125
+ if self.raw_action_dim is not None:
126
+ self.raw_action_dim = [d.cuda() if d is not None else None for d in self.raw_action_dim]
127
+ if self.residual_gate is not None:
128
+ self.residual_gate = [gate.cuda() for gate in self.residual_gate]
129
+
130
+
131
+ def prepare_attention_mask_per_sample(split_lens, attn_modes, device="cpu"):
132
+ """Prepare dense attention mask for a single sample with multiple splits.
133
+
134
+ Args:
135
+ split_lens: List of integers indicating length of each split within the sample
136
+ attn_modes: List of attention modes for each split ('causal', 'full', or 'noise')
137
+ device: Device to place the attention mask tensor on
138
+
139
+ Returns:
140
+ Attention mask tensor of shape (sample_len, sample_len) with -inf for masked positions
141
+ """
142
+ sample_len = sum(split_lens)
143
+ attention_mask = torch.zeros((sample_len, sample_len), dtype=torch.bool, device=device) # [sample_len,sample_len]
144
+
145
+ # First pass: Set up basic attention patterns for each split
146
+ current_pos = 0
147
+ for split_len, attn_mode in zip(split_lens, attn_modes):
148
+ assert attn_mode in ["causal", "full", "noise"], f"Invalid attention mode: {attn_mode}"
149
+
150
+ split_start = current_pos
151
+ split_end = current_pos + split_len
152
+
153
+ if attn_mode == "causal":
154
+ # Causal: lower triangular within split + full attention to previous splits
155
+ attention_mask[split_start:split_end, split_start:split_end] = torch.ones(
156
+ (split_len, split_len), device=device
157
+ ).tril() # [split_len,split_len]
158
+ attention_mask[split_start:split_end, :split_start] = 1
159
+ else: # "full" or "noise"
160
+ # Full attention within split and to previous splits
161
+ attention_mask[split_start:split_end, split_start:split_end] = torch.ones(
162
+ (split_len, split_len), device=device
163
+ ) # [split_len,split_len]
164
+ attention_mask[split_start:split_end, :split_start] = 1
165
+
166
+ current_pos += split_len
167
+
168
+ # Second pass: Handle noise mode - mask out noise columns except within same split
169
+ current_pos = 0
170
+ for split_len, attn_mode in zip(split_lens, attn_modes):
171
+ if attn_mode == "noise":
172
+ split_start = current_pos
173
+ split_end = current_pos + split_len
174
+
175
+ # Zero out the entire column for noise tokens
176
+ attention_mask[:, split_start:split_end] = 0
177
+ # But allow self-attention within the noise split
178
+ attention_mask[split_start:split_end, split_start:split_end] = 1
179
+
180
+ current_pos += split_len
181
+
182
+ # Convert boolean mask to float with -inf for masked positions
183
+ attention_mask = torch.zeros_like(attention_mask, dtype=torch.float).masked_fill_(
184
+ ~attention_mask, float("-inf")
185
+ ) # [sample_len,sample_len]
186
+
187
+ return attention_mask
188
+
189
+
190
+ # ============================================================================
191
+ # Tokenizer utilities
192
+ # ============================================================================
193
+
194
+
195
+ def add_special_tokens(tokenizer):
196
+ """Add image-related special tokens to tokenizer if not already present.
197
+
198
+ Args:
199
+ tokenizer: Tokenizer to add special tokens to
200
+
201
+ Returns:
202
+ Tuple of (modified tokenizer, dict of new token IDs)
203
+ """
204
+ # Collect existing special tokens
205
+ existing_special_tokens = []
206
+ for key, value in tokenizer.special_tokens_map.items():
207
+ if isinstance(value, str):
208
+ existing_special_tokens.append(value)
209
+ elif isinstance(value, list):
210
+ existing_special_tokens.extend(value)
211
+
212
+ # Define image boundary tokens to add if missing
213
+ tokens_to_add = []
214
+ if "<|vision_start|>" not in existing_special_tokens:
215
+ tokens_to_add.append("<|vision_start|>")
216
+ if "<|vision_end|>" not in existing_special_tokens:
217
+ tokens_to_add.append("<|vision_end|>")
218
+
219
+ # Add new tokens to tokenizer vocabulary
220
+ if tokens_to_add:
221
+ tokenizer.add_tokens(tokens_to_add)
222
+
223
+ # Get token IDs for image boundary tokens
224
+ new_token_ids = {
225
+ "start_of_generation": tokenizer.convert_tokens_to_ids("<|vision_start|>"),
226
+ "end_of_generation": tokenizer.convert_tokens_to_ids("<|vision_end|>"),
227
+ }
228
+
229
+ return tokenizer, new_token_ids
230
+
231
+
232
+ def compute_text_split_length(
233
+ num_caption_tokens: int,
234
+ special_tokens: dict[str, int],
235
+ has_generation: bool = True,
236
+ ) -> int:
237
+ """Compute the total text split length without mutating any state.
238
+
239
+ This is the number of token positions occupied by the text split in a
240
+ packed sequence: caption tokens + optional BOS + EOS + optional BOV.
241
+
242
+ Args:
243
+ num_caption_tokens: Number of raw caption token IDs (before special tokens).
244
+ special_tokens: Dictionary of special token IDs (checked for ``"bos_token_id"``).
245
+ has_generation: Whether a start-of-generation (BOV) token follows text.
246
+
247
+ Returns:
248
+ Total text split length (positions consumed in the packed sequence).
249
+ """
250
+ n = num_caption_tokens
251
+ if "bos_token_id" in special_tokens:
252
+ n += 1
253
+ n += 1 # EOS
254
+ if has_generation:
255
+ n += 1 # start-of-generation / BOV
256
+ return n
releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/packers.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Top-level input sequence packing orchestration."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ import torch
11
+
12
+ from cosmos_framework.data.generator.sequence_packing.sequence import PackedSequence, PackedSequenceBuilder, SequencePlan
13
+ from cosmos_framework.data.generator.sequence_packing.temporal_causal import pack_supertokens_temporal_causal
14
+
15
+ if TYPE_CHECKING:
16
+ from cosmos_framework.model.generator.utils.data_and_condition import GenerationDataClean
17
+
18
+
19
+ def _get_optional_fps(
20
+ fps_values: torch.Tensor | list[torch.Tensor | float] | None,
21
+ index: int,
22
+ ) -> float | None:
23
+ """Return an optional FPS value as a float.
24
+
25
+ Args:
26
+ fps_values: Optional tensor/list of per-sample or per-item FPS values.
27
+ index: Entry to read from ``fps_values``.
28
+
29
+ Returns:
30
+ FPS value as ``float`` when present, otherwise ``None``.
31
+ """
32
+ if fps_values is None or index >= len(fps_values):
33
+ return None
34
+ fps_value = fps_values[index] # []
35
+ if isinstance(fps_value, torch.Tensor):
36
+ return float(fps_value.item())
37
+ return float(fps_value)
38
+
39
+
40
+ def pack_input_sequence(
41
+ sequence_plans: list[SequencePlan],
42
+ input_text_indexes: list[list[int]],
43
+ gen_data_clean: GenerationDataClean,
44
+ input_timesteps: torch.Tensor,
45
+ special_tokens: dict[str, int],
46
+ max_num_tokens: int | None = None,
47
+ latent_patch_size: int = 1,
48
+ skip_text_tokens: bool = False,
49
+ include_end_of_generation_token: bool = False,
50
+ unified_3d_mrope_reset_spatial_ids: bool = True,
51
+ unified_3d_mrope_temporal_modality_margin: int = 0,
52
+ enable_fps_modulation: bool = False,
53
+ base_fps: float = 24.0,
54
+ sound_base_temporal_compression_factor: int | None = None,
55
+ temporal_compression_factor: int = 4,
56
+ vision_temporal_position_mode: str = "latent_index",
57
+ video_temporal_causal: bool = False,
58
+ action_dim: int = 32,
59
+ initial_mrope_temporal_offset: int | float = 0,
60
+ num_behavior_prefix_tokens: int = 0,
61
+ num_online_memory_prefix_tokens: int = 0,
62
+ num_proprio_prefix_tokens: int = 0,
63
+ ) -> PackedSequence:
64
+ """
65
+ Pack a sequence of input strings and VAE latents into a packed tensor format.
66
+ Uses SequencePlan to determine which modalities are present for each sample,
67
+ and maintains separate indices for text, vision, action, and sound to handle variable modality presence.
68
+
69
+ Args:
70
+ sequence_plans: List of SequencePlan items describing which modalities are present.
71
+ input_text_indexes: List of text token ID sequences (only for samples where has_text=True).
72
+ gen_data_clean: GenerationDataClean containing vision, action, and sound tensors.
73
+ - x0_tokens_vision: Vision tensors for samples where has_vision=True
74
+ - x0_tokens_action: Action tensors for samples where has_action=True
75
+ - x0_tokens_sound: Sound tensors (list of [C, T]) for samples where has_sound=True
76
+ input_timesteps: Diffusion timesteps for each sample. Shape (B,) or (B, 1) for
77
+ teacher_forcing/none (all frames share the same sigma), or (B, T_max) for
78
+ diffusion_forcing (per-frame independent sigma). Entries are extracted per
79
+ sample as a float (numel==1) or Tensor(T_max,) for per-frame indexing.
80
+ special_tokens: Dictionary containing special token IDs (eos_token_id, start_of_generation, end_of_generation)
81
+ max_num_tokens: Maximum number of tokens in the packed sequence
82
+ latent_patch_size: Patch size used by the network to pack latents
83
+ skip_text_tokens: If True, skip packing text tokens
84
+ include_end_of_generation_token: If True, append end-of-generation token
85
+ unified_3d_mrope_reset_spatial_ids: If True (default), spatial (H, W) indices
86
+ start from 0 for each vision segment. If False, spatial indices are offset
87
+ by the temporal offset (Qwen2VL-style).
88
+ unified_3d_mrope_temporal_modality_margin: Extra temporal offset inserted between
89
+ text and generation modalities.
90
+ enable_fps_modulation: If True, scale temporal position IDs based on video FPS
91
+ to reflect real time. Requires fps_vision in gen_data_clean.
92
+ Uses the same flag as diffusion_expert_config.enable_fps_modulation.
93
+ base_fps: Base FPS for normalization (default 24.0).
94
+ Uses the same value as diffusion_expert_config.base_fps.
95
+ sound_base_temporal_compression_factor: Base temporal compression factor for sound FPS scaling.
96
+ ``None`` preserves the current behavior where sound advances at ``base_fps`` positions/sec.
97
+ temporal_compression_factor: VAE temporal compression factor (default 4).
98
+ Obtained from the VAE tokenizer at runtime.
99
+ vision_temporal_position_mode: Temporal coordinates used for unified_3d_mrope vision tokens.
100
+ "latent_index" keeps legacy positions; "uniae_source_right_edge" uses
101
+ per-latent positions from gen_data_clean.temporal_positions_vision.
102
+ video_temporal_causal: If True, pack vision and optional action as temporal-causal
103
+ supertokens instead of separate modality blocks.
104
+ action_dim: Action feature dimension used when temporal-causal packing creates
105
+ null action tokens.
106
+ initial_mrope_temporal_offset: Initial temporal cursor for each sample, used by
107
+ autoregressive inference to seed mRoPE positions.
108
+ num_behavior_prefix_tokens: Number of learned causal condition slots to
109
+ reserve before text for each action-bearing sample. Zero preserves
110
+ every existing Cosmos packing layout.
111
+
112
+ Returns:
113
+ PackedSequence containing all packed tensors and metadata. See PackedSequence for field details.
114
+ """
115
+ del max_num_tokens
116
+
117
+ assert special_tokens is not None, "Special tokens must be provided"
118
+ assert isinstance(input_timesteps, torch.Tensor), "input_timesteps must be a tensor"
119
+ if input_timesteps.is_cuda:
120
+ raise ValueError("input_timesteps must be on CPU, not CUDA")
121
+ if isinstance(input_text_indexes, torch.Tensor):
122
+ raise ValueError("input_text_tokens must be a list, not a tensor")
123
+
124
+ supported_vision_temporal_position_modes = {"latent_index", "uniae_source_right_edge"}
125
+ if vision_temporal_position_mode not in supported_vision_temporal_position_modes:
126
+ raise ValueError(
127
+ "Unsupported vision_temporal_position_mode: "
128
+ f"{vision_temporal_position_mode}. Supported modes: {supported_vision_temporal_position_modes}."
129
+ )
130
+ has_any_vision = any(plan.has_vision for plan in sequence_plans)
131
+ explicit_vision_temporal_positions_active = vision_temporal_position_mode != "latent_index" and has_any_vision
132
+ if explicit_vision_temporal_positions_active:
133
+ if gen_data_clean.temporal_positions_vision is None:
134
+ raise ValueError(
135
+ f"vision_temporal_position_mode={vision_temporal_position_mode} requires "
136
+ "gen_data_clean.temporal_positions_vision."
137
+ )
138
+ if gen_data_clean.x0_tokens_vision is not None and len(gen_data_clean.temporal_positions_vision) != len(
139
+ gen_data_clean.x0_tokens_vision
140
+ ):
141
+ raise ValueError(
142
+ "temporal_positions_vision must have one entry per x0_tokens_vision item, "
143
+ f"got {len(gen_data_clean.temporal_positions_vision)} positions for "
144
+ f"{len(gen_data_clean.x0_tokens_vision)} vision items."
145
+ )
146
+ if video_temporal_causal:
147
+ raise NotImplementedError(
148
+ "video_temporal_causal=True is not wired for explicit UniAE vision temporal positions yet."
149
+ )
150
+ if any(plan.has_action for plan in sequence_plans):
151
+ raise NotImplementedError("Action packing is not wired for explicit UniAE vision temporal positions yet.")
152
+ if initial_mrope_temporal_offset != 0:
153
+ raise NotImplementedError(
154
+ "Autoregressive mRoPE temporal offsets are not wired for explicit UniAE vision temporal positions yet."
155
+ )
156
+ use_float_mrope_positions = enable_fps_modulation or explicit_vision_temporal_positions_active
157
+
158
+ # Initialize mutable builder state for sequence construction.
159
+ seq_builder = PackedSequenceBuilder(uses_single_timestep=input_timesteps.numel() == 1)
160
+
161
+ # Configure 3D mRoPE on the builder.
162
+ seq_builder._mrope_reset_spatial = unified_3d_mrope_reset_spatial_ids
163
+
164
+ # Maintain separate indices for each modality
165
+ idx_text = 0
166
+ idx_vision = 0
167
+ idx_action = 0
168
+ idx_sound = 0
169
+ null_action_flags: list[bool] = [] # collected from TC path; asserted consistent after the loop
170
+
171
+ # Validate: all samples must have text (causal split is always required for two-way attention).
172
+ # CFG dropout only drops text *content*, not the structural text split.
173
+ if not skip_text_tokens:
174
+ for plan in sequence_plans:
175
+ assert plan.has_text, "All sequence plans must have has_text=True when skip_text_tokens=False"
176
+
177
+ # Pack each sample based on its sequence plan
178
+ for sample_idx, sequence_plan in enumerate(sequence_plans):
179
+ sample_len = 0
180
+
181
+ # mRoPE temporal offset resets per sample.
182
+ # initial_mrope_temporal_offset is non-zero only for AR inference (frame N seeds at N*tcf).
183
+ seq_builder.begin_sample(initial_mrope_temporal_offset)
184
+
185
+ _ts = input_timesteps[sample_idx]
186
+ input_timestep = _ts.item() if _ts.numel() == 1 else _ts # float (TF) or Tensor(T_max,) (DF)
187
+
188
+ # Pack text tokens if has_text=True and not skipped
189
+ if sequence_plan.has_text and not skip_text_tokens:
190
+ text_ids = input_text_indexes[idx_text]
191
+ idx_text += 1
192
+
193
+ has_generation_for_sample = sequence_plan.has_vision or sequence_plan.has_action or sequence_plan.has_sound
194
+ text_sample_len = seq_builder.pack_text_tokens(
195
+ text_ids,
196
+ special_tokens,
197
+ has_generation=has_generation_for_sample,
198
+ use_float_positions=use_float_mrope_positions,
199
+ num_behavior_prefix_tokens=(
200
+ num_behavior_prefix_tokens if sequence_plan.has_action else 0
201
+ ),
202
+ num_online_memory_prefix_tokens=(
203
+ num_online_memory_prefix_tokens if sequence_plan.has_action else 0
204
+ ),
205
+ num_proprio_prefix_tokens=(
206
+ num_proprio_prefix_tokens if sequence_plan.has_action else 0
207
+ ),
208
+ )
209
+ sample_len += text_sample_len
210
+
211
+ # End of text modality, add an offset as the boundary between text and vision.
212
+ seq_builder.advance_mrope_temporal_offset(unified_3d_mrope_temporal_modality_margin)
213
+
214
+ # Save temporal offset before vision for action tokens (action uses same offset as vision start)
215
+ vision_start_temporal_offset = seq_builder.mrope_temporal_offset
216
+
217
+ # Pack vision (and optionally action) tokens
218
+ if video_temporal_causal and sequence_plan.has_vision:
219
+ # Temporal causal path: when sequence_plan.has_action=True, interleaved supertokens
220
+ # [action_t, vision_t]; when False, supertokens are just vision patches.
221
+ input_vision_tokens = gen_data_clean.x0_tokens_vision[idx_vision]
222
+ idx_vision += 1
223
+
224
+ vision_fps = _get_optional_fps(gen_data_clean.fps_vision, idx_vision - 1)
225
+
226
+ input_action_tokens_tc: torch.Tensor | None = None
227
+ action_fps_tc = None
228
+ if sequence_plan.has_action:
229
+ input_action_tokens_tc = gen_data_clean.x0_tokens_action[idx_action]
230
+ action_fps_tc = _get_optional_fps(gen_data_clean.fps_action, idx_action)
231
+ idx_action += 1
232
+
233
+ supertoken_split_len, null_flag = pack_supertokens_temporal_causal(
234
+ seq_builder=seq_builder,
235
+ input_vision_tokens=input_vision_tokens,
236
+ input_action_tokens=input_action_tokens_tc,
237
+ condition_frame_indexes_vision=sequence_plan.condition_frame_indexes_vision,
238
+ input_timestep=input_timestep,
239
+ latent_patch_size=latent_patch_size,
240
+ temporal_compression_factor=temporal_compression_factor,
241
+ action_dim=action_dim,
242
+ vision_fps=vision_fps,
243
+ action_fps=action_fps_tc,
244
+ enable_fps_modulation=enable_fps_modulation,
245
+ base_fps=base_fps,
246
+ pack_action_tokens=sequence_plan.has_action,
247
+ )
248
+ null_action_flags.append(null_flag)
249
+ # We assume all samples in a batch share the same has_action layout, so
250
+ # stamp the supertoken layout constant directly here. This is the
251
+ # single source of truth read by downstream attention / KV-cache
252
+ # code (no recomputation in the network).
253
+ seq_builder.num_action_tokens_per_supertoken = (
254
+ temporal_compression_factor if sequence_plan.has_action else 0
255
+ )
256
+ sample_len += supertoken_split_len
257
+ vision_split_len = supertoken_split_len
258
+ action_split_len = 0 # Already absorbed into supertoken_split_len
259
+
260
+ else:
261
+ # Standard path: vision and action packed separately
262
+ if sequence_plan.has_vision:
263
+ # Determine how many vision items this sample owns.
264
+ # For multi-item samples (e.g. image editing), num_vision_items_per_sample
265
+ # records [2, 2, ...]; for standard T2I/T2V it is None (1 item per sample).
266
+ num_vis = (
267
+ gen_data_clean.num_vision_items_per_sample[sample_idx]
268
+ if gen_data_clean.num_vision_items_per_sample is not None
269
+ else 1
270
+ )
271
+
272
+ vision_split_len = 0
273
+ # Per-item split lengths for multi-control attention routing.
274
+ # Only tracked when control_weights are present (inference-only);
275
+ # skipped during training to avoid unnecessary side effects.
276
+ track_item_split_lens = gen_data_clean.control_weights is not None
277
+ sample_item_split_lens: list[int] = []
278
+ # Controlnet-style transfer: when set, all vision items share the same
279
+ # temporal mRoPE grid. We snapshot the offset before the loop and
280
+ # rewind to it before each item, so every item produces identical
281
+ # temporal IDs. Each pack_vision_tokens call still advances the
282
+ # offset by latent_t internally; in shared-grid mode the post-loop
283
+ # offset equals snapshot + latent_t (single-clip semantics for
284
+ # downstream EOV / next-modality tokens).
285
+ shared_grid = sequence_plan.share_vision_temporal_positions and num_vis > 1
286
+ items_temporal_offset_snapshot = seq_builder.mrope_temporal_offset
287
+ shared_latent_t: int | None = None
288
+ shared_patch_h: int | None = None
289
+ shared_patch_w: int | None = None
290
+ shared_temporal_positions: torch.Tensor | None = None
291
+ # FPS is recorded per-sample (shape [B]); for multi-item samples
292
+ # (transfer / image-edit) every vision item in this sample shares
293
+ # the same conditioning FPS, so we read by sample_idx, not by the
294
+ # flat idx_vision counter (which would alias to a neighbor sample's
295
+ # fps and corrupt RoPE FPS modulation).
296
+ sample_vision_fps = _get_optional_fps(gen_data_clean.fps_vision, sample_idx)
297
+
298
+ for item_idx in range(num_vis):
299
+ flat_vision_idx = idx_vision
300
+ input_vision_tokens = gen_data_clean.x0_tokens_vision[flat_vision_idx] # [1,C,T,H,W]
301
+ vision_temporal_positions: torch.Tensor | None = None
302
+ if explicit_vision_temporal_positions_active:
303
+ assert gen_data_clean.temporal_positions_vision is not None
304
+ vision_temporal_positions = gen_data_clean.temporal_positions_vision[flat_vision_idx] # [T]
305
+ if vision_temporal_positions.shape[0] != input_vision_tokens.shape[2]:
306
+ raise ValueError(
307
+ "vision_temporal_positions must match latent_t for each vision item, "
308
+ f"got {vision_temporal_positions.shape[0]} positions and "
309
+ f"latent_t={input_vision_tokens.shape[2]} for item {flat_vision_idx}."
310
+ )
311
+ idx_vision += 1
312
+
313
+ # Determine conditioning for this vision item.
314
+ # For multi-item mode: all items except the last are fully conditioned
315
+ # (all frames are clean); the last item uses the SequencePlan's
316
+ # condition_frame_indexes_vision (typically [] = fully generated).
317
+ if num_vis > 1 and item_idx < num_vis - 1:
318
+ # Conditioning item (e.g. source image): mark all frames as clean
319
+ latent_t = input_vision_tokens.shape[2]
320
+ item_condition_frames = list(range(latent_t))
321
+ else:
322
+ # Generation item (single-item mode or last item in multi-item)
323
+ item_condition_frames = sequence_plan.condition_frame_indexes_vision
324
+
325
+ if shared_grid:
326
+ item_latent_t = input_vision_tokens.shape[2]
327
+ item_latent_h = input_vision_tokens.shape[3]
328
+ item_latent_w = input_vision_tokens.shape[4]
329
+ if shared_latent_t is None:
330
+ shared_latent_t = item_latent_t
331
+ shared_patch_h = item_latent_h
332
+ shared_patch_w = item_latent_w
333
+ else:
334
+ assert item_latent_t == shared_latent_t, (
335
+ f"share_vision_temporal_positions requires equal latent_t across items, "
336
+ f"got item {item_idx} latent_t={item_latent_t} vs first={shared_latent_t}"
337
+ )
338
+ assert item_latent_h == shared_patch_h and item_latent_w == shared_patch_w, (
339
+ f"share_vision_temporal_positions requires equal spatial grid across items, "
340
+ f"got item {item_idx} (H,W)=({item_latent_h},{item_latent_w}) "
341
+ f"vs first=({shared_patch_h},{shared_patch_w})"
342
+ )
343
+ if vision_temporal_positions is not None:
344
+ if shared_temporal_positions is None:
345
+ shared_temporal_positions = vision_temporal_positions
346
+ else:
347
+ comparison_temporal_positions = vision_temporal_positions.to(
348
+ device=shared_temporal_positions.device
349
+ ) # [T]
350
+ assert torch.allclose(comparison_temporal_positions, shared_temporal_positions), (
351
+ "share_vision_temporal_positions requires equal explicit temporal positions "
352
+ f"across vision items, got item {item_idx} positions "
353
+ f"{vision_temporal_positions.tolist()} vs first "
354
+ f"{shared_temporal_positions.tolist()}."
355
+ )
356
+ # Rewind so this item starts at the same temporal offset as item 0.
357
+ seq_builder.set_mrope_temporal_offset(items_temporal_offset_snapshot)
358
+
359
+ item_split_len = seq_builder.pack_vision_tokens(
360
+ input_vision_tokens=input_vision_tokens,
361
+ condition_frame_indexes_vision=item_condition_frames,
362
+ input_timestep=input_timestep,
363
+ latent_patch_size=latent_patch_size,
364
+ vision_fps=sample_vision_fps,
365
+ enable_fps_modulation=enable_fps_modulation,
366
+ base_fps=base_fps,
367
+ temporal_compression_factor=temporal_compression_factor,
368
+ vision_temporal_positions=vision_temporal_positions,
369
+ )
370
+ vision_split_len += item_split_len
371
+ if track_item_split_lens:
372
+ sample_item_split_lens.append(item_split_len)
373
+
374
+ if track_item_split_lens:
375
+ seq_builder.vision_item_split_lens.append(sample_item_split_lens)
376
+ sample_len += vision_split_len
377
+
378
+ else:
379
+ vision_split_len = 0
380
+
381
+ # Pack action tokens if has_action=True
382
+ if sequence_plan.has_action:
383
+ input_action_tokens = gen_data_clean.x0_tokens_action[idx_action]
384
+ action_fps = _get_optional_fps(gen_data_clean.fps_action, idx_action)
385
+ idx_action += 1
386
+
387
+ action_split_len = seq_builder.pack_action_tokens(
388
+ input_action_tokens=input_action_tokens,
389
+ condition_frame_indexes_action=sequence_plan.condition_frame_indexes_action,
390
+ input_timestep=input_timestep,
391
+ action_temporal_offset=vision_start_temporal_offset,
392
+ enable_fps_modulation=enable_fps_modulation,
393
+ base_fps=base_fps,
394
+ action_fps=action_fps,
395
+ base_temporal_compression_factor=temporal_compression_factor,
396
+ action_start_frame_offset=sequence_plan.action_start_frame_offset,
397
+ )
398
+ sample_len += action_split_len
399
+ else:
400
+ action_split_len = 0
401
+
402
+ # Pack sound tokens if has_sound=True
403
+ if sequence_plan.has_sound:
404
+ input_sound_tokens = gen_data_clean.x0_tokens_sound[idx_sound]
405
+ sound_fps = _get_optional_fps(gen_data_clean.fps_sound, idx_sound)
406
+ idx_sound += 1
407
+
408
+ sound_split_len = seq_builder.pack_sound_tokens(
409
+ input_sound_tokens=input_sound_tokens,
410
+ condition_frame_indexes_sound=sequence_plan.condition_frame_indexes_sound,
411
+ input_timestep=input_timestep,
412
+ sound_temporal_offset=vision_start_temporal_offset,
413
+ enable_fps_modulation=enable_fps_modulation,
414
+ base_fps=base_fps,
415
+ sound_fps=sound_fps,
416
+ sound_base_temporal_compression_factor=sound_base_temporal_compression_factor,
417
+ )
418
+ sample_len += sound_split_len
419
+ else:
420
+ sound_split_len = 0
421
+
422
+ # Add end-of-generation token if needed
423
+ eov_len = 0
424
+ has_any_generation = sequence_plan.has_vision or sequence_plan.has_action or sequence_plan.has_sound
425
+ if include_end_of_generation_token and has_any_generation:
426
+ eov_len = seq_builder.append_end_of_generation_token(
427
+ token_id=special_tokens["end_of_generation"],
428
+ use_float_mrope_positions=use_float_mrope_positions,
429
+ )
430
+ sample_len += eov_len
431
+
432
+ combined_split_len = vision_split_len + action_split_len + sound_split_len + eov_len
433
+ seq_builder.finish_sample(combined_split_len, sample_len)
434
+
435
+ # Assert consistent null_action_supertokens across all TC samples, then set once
436
+ if null_action_flags:
437
+ assert len(set(null_action_flags)) == 1, (
438
+ f"Inconsistent null_action_supertokens across samples: {null_action_flags}. "
439
+ "All samples in a batch must have the same structure (all training or all AR inference)."
440
+ )
441
+ seq_builder.null_action_supertokens = null_action_flags[0]
442
+
443
+ # Finalize and return packed data
444
+ return seq_builder.finalize(
445
+ gen_data_clean=gen_data_clean,
446
+ )
releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/sequence.py ADDED
@@ -0,0 +1,1203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Sequence builder/output and plan helpers for VFM sequence packing."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING
11
+
12
+ import torch
13
+
14
+ from cosmos_framework.data.generator.sequence_packing.modality import ModalityData, ModalityDataBuilder, ModalitySpan
15
+ from cosmos_framework.data.generator.sequence_packing.mrope import (
16
+ get_3d_mrope_ids_text_tokens,
17
+ get_3d_mrope_ids_vae_tokens,
18
+ )
19
+ from cosmos_framework.data.generator.sequence_packing.runtime import (
20
+ SequencePackMetadata,
21
+ prepare_sequence_pack_metadata,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from cosmos_framework.model.generator.utils.data_and_condition import GenerationDataClean
26
+
27
+
28
+ def _empty_long_tensor() -> torch.Tensor:
29
+ return torch.empty(0, dtype=torch.long) # [0]
30
+
31
+
32
+ @dataclass
33
+ class PackedSequenceBuilder:
34
+ """Mutable construction state for sequence packing.
35
+
36
+ Attributes:
37
+ sample_lens: Length of each sample in the packed sequence.
38
+ split_lens: Length of each attention split. Each sample contributes a causal
39
+ text split and a full generation split.
40
+ attn_modes: Attention mode for each split, such as ``"causal"`` or ``"full"``.
41
+ is_image_batch: Whether this batch contains images rather than videos.
42
+ uses_single_timestep: Whether all noised tokens share one input timestep scalar.
43
+ sequence_length: Total packed sequence length. Filled during finalization.
44
+ current_seq_index: Next global packed-sequence index to append.
45
+ text_ids: Text token IDs accumulated during packing, including special tokens.
46
+ text_indexes: Global packed-sequence indexes for text tokens.
47
+ position_ids: mRoPE position ID blocks accumulated as ``[3, N]`` tensors.
48
+ label_ids: Label IDs for text cross-entropy loss.
49
+ ce_loss_indexes: Global packed-sequence indexes for text cross-entropy loss.
50
+ ce_loss_weights: Per-token weights for text cross-entropy loss.
51
+ _mrope_temporal_offset: Running temporal cursor for per-sample mRoPE generation.
52
+ _mrope_reset_spatial: Whether spatial mRoPE IDs reset for each segment.
53
+ null_action_supertokens: Whether temporal-causal supertoken 0 contains null
54
+ action tokens.
55
+ num_action_tokens_per_supertoken: Number of action tokens prefixing each
56
+ temporal-causal vision supertoken.
57
+ vision: Vision modality construction state, or ``None`` if no vision was appended.
58
+ action: Action modality construction state, or ``None`` if no action was appended.
59
+ sound: Sound modality construction state, or ``None`` if no sound was appended.
60
+ vision_item_split_lens: Per-sample per-vision-item token counts for multi-control
61
+ transfer.
62
+ control_weights: Per-sample per-control weights for multi-control weighted V-scaling.
63
+ """
64
+
65
+ # Sequence structure
66
+ sample_lens: list[int] = field(default_factory=list)
67
+ split_lens: list[int] = field(default_factory=list)
68
+ attn_modes: list[str] = field(default_factory=list)
69
+ is_image_batch: bool = False
70
+ uses_single_timestep: bool = False
71
+ sequence_length: int = 0
72
+
73
+ # Build-time tracking (used during packing, not after finalize)
74
+ current_seq_index: int = 0
75
+
76
+ # Text modality append state
77
+ text_ids: list[int] = field(default_factory=list)
78
+ text_indexes: list[int] = field(default_factory=list)
79
+ # Learned non-text condition slots that share the causal/understanding
80
+ # attention split with text. Zeva reserves one such slot per action-bearing
81
+ # sample and overwrites it with the projected task context.
82
+ behavior_indexes: list[int] = field(default_factory=list)
83
+ online_memory_indexes: list[int] = field(default_factory=list)
84
+ proprio_indexes: list[int] = field(default_factory=list)
85
+ position_ids: list[torch.Tensor] = field(default_factory=list)
86
+
87
+ # Loss computation - Cross Entropy (text)
88
+ label_ids: list[int] = field(default_factory=list)
89
+ ce_loss_indexes: list[int] = field(default_factory=list)
90
+ ce_loss_weights: list[float] = field(default_factory=list)
91
+
92
+ # Build-time mRoPE tracking (used during packing, not after finalize).
93
+ # position_ids accumulates (3, N) tensors and finalize() produces a
94
+ # (3, total_seq_len) tensor.
95
+ # Running temporal index for mRoPE position ID generation within a single sample.
96
+ # Reset to 0 at the start of each sample, then advanced by text and vision helpers
97
+ # as segments are packed. Action reuses the pre-vision snapshot (parallel temporal
98
+ # range) without advancing it. Float when FPS modulation is enabled.
99
+ # E.g. offset=0 -> text(4 tokens) -> offset=4 -> vision(3 frames) -> offset=7.
100
+ _mrope_temporal_offset: int | float = 0
101
+ _mrope_reset_spatial: bool = True
102
+
103
+ # Temporal causal: whether supertoken 0's action slot contains null tokens.
104
+ # True for all training calls and AR frame 0; False for AR frame N>0 (real actions).
105
+ # Used by three_way_attention to zero out V for null action tokens (inline when attention_meta.null_action_supertokens=True).
106
+ null_action_supertokens: bool = False
107
+
108
+ # Temporal causal: number of action tokens prefixing each vision supertoken.
109
+ # Equals temporal_compression_factor when actions are packed inline; 0 when
110
+ # action_gen=False or for non-temporal-causal layouts. Single source of truth
111
+ # for downstream attention/KV-cache code (per-supertoken layout is
112
+ # num_action_tokens_per_supertoken + H_p * W_p).
113
+ num_action_tokens_per_supertoken: int = 0
114
+
115
+ # Generation modality construction state
116
+ vision: ModalityDataBuilder | None = None
117
+ action: ModalityDataBuilder | None = None
118
+ sound: ModalityDataBuilder | None = None
119
+
120
+ def ensure_vision(self) -> ModalityDataBuilder:
121
+ """Return the vision builder, creating it on first use.
122
+
123
+ Returns:
124
+ Vision ``ModalityDataBuilder`` for subsequent append operations.
125
+ """
126
+ if self.vision is None:
127
+ self.vision = ModalityDataBuilder()
128
+ return self.vision
129
+
130
+ def ensure_action(self) -> ModalityDataBuilder:
131
+ """Return the action builder, creating it on first use.
132
+
133
+ Returns:
134
+ Action ``ModalityDataBuilder`` for subsequent append operations.
135
+ """
136
+ if self.action is None:
137
+ self.action = ModalityDataBuilder()
138
+ return self.action
139
+
140
+ def ensure_sound(self) -> ModalityDataBuilder:
141
+ """Return the sound builder, creating it on first use.
142
+
143
+ Returns:
144
+ Sound ``ModalityDataBuilder`` for subsequent append operations.
145
+ """
146
+ if self.sound is None:
147
+ self.sound = ModalityDataBuilder()
148
+ return self.sound
149
+
150
+ # Multi-control transfer: per-sample list of per-vision-item token counts.
151
+ # For a multi-control transfer sample with N controls + 1 noisy target,
152
+ # vision_item_split_lens[i] = [L_ctrl0, L_ctrl1, ..., L_ctrlN-1, L_noisy].
153
+ # Used by cosmos3_vfm_network.py to derive gen-relative control/noisy ranges
154
+ # for multi_control_two_way_attention.
155
+ vision_item_split_lens: list[list[int]] = field(default_factory=list)
156
+
157
+ # Per-sample per-control weights for multi-control weighted V-scaling.
158
+ # Parallel to vision_item_split_lens[i][:-1] (excludes noisy item).
159
+ # None for non-transfer or standard single-control samples.
160
+ control_weights: list[list[float]] | None = None
161
+
162
+ @property
163
+ def mrope_temporal_offset(self) -> int | float:
164
+ """Current per-sample temporal cursor used for mRoPE position generation."""
165
+ return self._mrope_temporal_offset
166
+
167
+ def set_mrope_temporal_offset(self, temporal_offset: int | float) -> None:
168
+ """Set the per-sample temporal cursor used for mRoPE position generation.
169
+
170
+ Args:
171
+ temporal_offset: New mRoPE temporal cursor value.
172
+ """
173
+ self._mrope_temporal_offset = temporal_offset
174
+
175
+ def advance_mrope_temporal_offset(self, delta: int | float) -> None:
176
+ """Advance the per-sample temporal cursor by ``delta``.
177
+
178
+ Args:
179
+ delta: Amount to add to the current mRoPE temporal cursor.
180
+ """
181
+ self._mrope_temporal_offset += delta
182
+
183
+ def begin_sample(self, initial_mrope_temporal_offset: int | float) -> None:
184
+ """Reset per-sample position cursors before appending sample tokens.
185
+
186
+ Args:
187
+ initial_mrope_temporal_offset: Initial mRoPE temporal cursor for this sample.
188
+ """
189
+ self._mrope_temporal_offset = initial_mrope_temporal_offset
190
+
191
+ def pack_text_tokens(
192
+ self,
193
+ text_ids: list[int],
194
+ special_tokens: dict[str, int],
195
+ has_generation: bool,
196
+ use_float_positions: bool = False,
197
+ num_behavior_prefix_tokens: int = 0,
198
+ num_online_memory_prefix_tokens: int = 0,
199
+ num_proprio_prefix_tokens: int = 0,
200
+ ) -> int:
201
+ """Pack text tokens into the sequence.
202
+
203
+ Args:
204
+ text_ids: List of text token IDs (integers).
205
+ special_tokens: Dictionary of special token IDs.
206
+ has_generation: Whether there's media/action after text.
207
+ use_float_positions: If True, generate float position IDs for 3D mRoPE
208
+ (for consistency with FPS-modulated vision tokens).
209
+
210
+ Returns:
211
+ Text sample length.
212
+ """
213
+ if num_behavior_prefix_tokens < 0:
214
+ raise ValueError("num_behavior_prefix_tokens must be non-negative")
215
+ if num_online_memory_prefix_tokens < 0:
216
+ raise ValueError("num_online_memory_prefix_tokens must be non-negative")
217
+ if num_proprio_prefix_tokens < 0:
218
+ raise ValueError("num_proprio_prefix_tokens must be non-negative")
219
+
220
+ # Reserve learned causal-prompt slots before BOS, matching Zeva's
221
+ # [causal prompt | image | language] prefix order. They are deliberately not
222
+ # represented by tokenizer IDs and therefore do not participate in CE.
223
+ if num_behavior_prefix_tokens:
224
+ behavior_position_ids, self._mrope_temporal_offset = get_3d_mrope_ids_text_tokens(
225
+ num_tokens=num_behavior_prefix_tokens,
226
+ temporal_offset=self._mrope_temporal_offset,
227
+ use_float_positions=use_float_positions,
228
+ )
229
+ behavior_start = self.current_seq_index
230
+ self.behavior_indexes.extend(
231
+ range(behavior_start, behavior_start + num_behavior_prefix_tokens)
232
+ )
233
+ self._append_position_ids(behavior_position_ids, num_behavior_prefix_tokens)
234
+ self.current_seq_index += num_behavior_prefix_tokens
235
+
236
+ if num_online_memory_prefix_tokens:
237
+ online_position_ids, self._mrope_temporal_offset = get_3d_mrope_ids_text_tokens(
238
+ num_tokens=num_online_memory_prefix_tokens,
239
+ temporal_offset=self._mrope_temporal_offset,
240
+ use_float_positions=use_float_positions,
241
+ )
242
+ online_start = self.current_seq_index
243
+ self.online_memory_indexes.extend(
244
+ range(online_start, online_start + num_online_memory_prefix_tokens)
245
+ )
246
+ self._append_position_ids(online_position_ids, num_online_memory_prefix_tokens)
247
+ self.current_seq_index += num_online_memory_prefix_tokens
248
+
249
+ if num_proprio_prefix_tokens:
250
+ proprio_position_ids, self._mrope_temporal_offset = get_3d_mrope_ids_text_tokens(
251
+ num_tokens=num_proprio_prefix_tokens,
252
+ temporal_offset=self._mrope_temporal_offset,
253
+ use_float_positions=use_float_positions,
254
+ )
255
+ proprio_start = self.current_seq_index
256
+ self.proprio_indexes.extend(range(proprio_start, proprio_start + num_proprio_prefix_tokens))
257
+ self._append_position_ids(proprio_position_ids, num_proprio_prefix_tokens)
258
+ self.current_seq_index += num_proprio_prefix_tokens
259
+
260
+ # Prepend BOS token if available
261
+ if "bos_token_id" in special_tokens:
262
+ shifted_text_ids = [special_tokens["bos_token_id"]] + text_ids
263
+ else:
264
+ shifted_text_ids = text_ids
265
+
266
+ span_token_ids = shifted_text_ids + [special_tokens["eos_token_id"]]
267
+ # Add start-of-generation token, but only if there's media/action present.
268
+ if has_generation:
269
+ span_token_ids.append(special_tokens["start_of_generation"])
270
+
271
+ text_split_len = len(span_token_ids)
272
+ split_len = (
273
+ num_behavior_prefix_tokens
274
+ + num_online_memory_prefix_tokens
275
+ + num_proprio_prefix_tokens
276
+ + text_split_len
277
+ )
278
+ expected_split_len = len(text_ids)
279
+ if "bos_token_id" in special_tokens:
280
+ expected_split_len += 1
281
+ expected_split_len += 1 # EOS
282
+ if has_generation:
283
+ expected_split_len += 1 # start-of-generation / BOV
284
+ assert text_split_len == expected_split_len
285
+
286
+ position_ids, self._mrope_temporal_offset = get_3d_mrope_ids_text_tokens(
287
+ num_tokens=text_split_len,
288
+ temporal_offset=self._mrope_temporal_offset,
289
+ use_float_positions=use_float_positions,
290
+ ) # position_ids: [3,text_split_len]
291
+
292
+ span_start, _ = self.append_text_span(span_token_ids, position_ids)
293
+
294
+ # Configure loss computation for text tokens
295
+ self.ce_loss_indexes.extend(range(span_start, span_start + len(shifted_text_ids)))
296
+ self.ce_loss_weights.extend([1.0] * len(shifted_text_ids))
297
+ self.label_ids.extend(text_ids[1:] + [special_tokens["eos_token_id"]])
298
+
299
+ self.attn_modes.append("causal")
300
+ self.split_lens.append(split_len)
301
+
302
+ return split_len
303
+
304
+ def pack_vision_tokens(
305
+ self,
306
+ input_vision_tokens: torch.Tensor,
307
+ condition_frame_indexes_vision: list[int],
308
+ input_timestep: float | torch.Tensor,
309
+ latent_patch_size: int = 1,
310
+ vision_fps: float | None = None,
311
+ enable_fps_modulation: bool = False,
312
+ base_fps: float = 24.0,
313
+ temporal_compression_factor: int = 4,
314
+ vision_temporal_positions: torch.Tensor | None = None,
315
+ ) -> int:
316
+ """Pack vision tokens into the sequence.
317
+
318
+ Args:
319
+ input_vision_tokens: Vision latent tokens (C, T, H, W).
320
+ condition_frame_indexes_vision: Indexes of conditioning frames.
321
+ input_timestep: Diffusion timestep. Either a float (teacher_forcing/none — all frames
322
+ share the same sigma) or a Tensor(T_max,) (diffusion_forcing — per-frame sigma;
323
+ indexed as input_timestep[frame_idx] for each noisy frame).
324
+ latent_patch_size: Patch size for latent patchification.
325
+ vision_fps: Frames per second of the video. Used when enable_fps_modulation=True.
326
+ enable_fps_modulation: If True, scale temporal position IDs based on video FPS.
327
+ base_fps: Base FPS for normalization (default 24.0).
328
+ temporal_compression_factor: VAE temporal compression factor (default 4).
329
+ vision_temporal_positions: Optional explicit temporal coordinate per latent
330
+ frame, shape ``(T,)``. Used by UniAE to account for kept boundary latents.
331
+
332
+ Returns:
333
+ Vision split length.
334
+ """
335
+ vision = self.ensure_vision()
336
+
337
+ # Compute position IDs for image patches
338
+ _, _, latent_t, latent_h, latent_w = input_vision_tokens.shape
339
+ if latent_patch_size < 1:
340
+ raise ValueError(f"latent_patch_size must be >= 1, got {latent_patch_size}")
341
+ # Use ceil to support latent dims not divisible by patch size (padding handled in network)
342
+ patch_h = math.ceil(latent_h / latent_patch_size)
343
+ patch_w = math.ceil(latent_w / latent_patch_size)
344
+ vision.token_shapes.append((latent_t, patch_h, patch_w))
345
+ vision.tokens.append(input_vision_tokens)
346
+ vision_payload_index = len(vision.tokens) - 1
347
+
348
+ # Supervise vision tokens based on conditioning frames
349
+ condition_set = {idx for idx in condition_frame_indexes_vision if 0 <= idx < latent_t}
350
+
351
+ vision_condition_mask = torch.zeros(
352
+ (latent_t, 1, 1), device=input_vision_tokens.device, dtype=input_vision_tokens.dtype
353
+ ) # [T,1,1]
354
+ for frame_idx in condition_set:
355
+ vision_condition_mask[frame_idx, 0, 0] = 1.0
356
+ vision.condition_mask.append(vision_condition_mask)
357
+
358
+ vision_noisy_frame_indexes = torch.tensor(
359
+ [idx for idx in range(latent_t) if idx not in condition_set],
360
+ device=input_vision_tokens.device,
361
+ dtype=torch.long,
362
+ ) # [N_noisy_frames]
363
+ vision.noisy_frame_indexes.append(vision_noisy_frame_indexes)
364
+
365
+ frame_token_stride = patch_h * patch_w
366
+ effective_fps = vision_fps if enable_fps_modulation else None
367
+ if vision_temporal_positions is not None:
368
+ vision_temporal_positions = vision_temporal_positions.to(device="cpu", dtype=torch.float32) # [T]
369
+
370
+ vision_mrope_ids, self._mrope_temporal_offset = get_3d_mrope_ids_vae_tokens(
371
+ grid_t=latent_t,
372
+ grid_h=patch_h,
373
+ grid_w=patch_w,
374
+ temporal_offset=self._mrope_temporal_offset,
375
+ reset_spatial_indices=self._mrope_reset_spatial,
376
+ fps=effective_fps,
377
+ base_fps=base_fps,
378
+ temporal_compression_factor=temporal_compression_factor,
379
+ temporal_positions=vision_temporal_positions,
380
+ actual_temporal_compression_factor=temporal_compression_factor,
381
+ ) # vision_mrope_ids: [3,N_vision_tokens]
382
+ vision_mrope_ids = vision_mrope_ids.reshape(3, latent_t, frame_token_stride) # [3,T,H*W]
383
+
384
+ vision_split_len = 0
385
+ for frame_idx in range(latent_t):
386
+ position_ids = vision_mrope_ids[:, frame_idx, :] # [3,H*W]
387
+ frame_indexes = self.append_vision_span(
388
+ frame_token_stride,
389
+ position_ids,
390
+ payload_index=vision_payload_index,
391
+ payload_start=frame_idx * frame_token_stride,
392
+ payload_shape=(1, patch_h, patch_w),
393
+ )
394
+ vision_split_len += frame_token_stride
395
+
396
+ if frame_idx in condition_set:
397
+ continue
398
+ vision.mse_loss_indexes.extend(frame_indexes)
399
+ if isinstance(input_timestep, torch.Tensor):
400
+ frame_ts = input_timestep[frame_idx].item()
401
+ else:
402
+ frame_ts = input_timestep
403
+ vision.timesteps.extend([frame_ts] * frame_token_stride)
404
+
405
+ return vision_split_len
406
+
407
+ def pack_action_tokens(
408
+ self,
409
+ input_action_tokens: torch.Tensor,
410
+ condition_frame_indexes_action: list[int],
411
+ input_timestep: float,
412
+ action_temporal_offset: int | float = 0,
413
+ enable_fps_modulation: bool = False,
414
+ base_fps: float = 24.0,
415
+ action_fps: float | None = None,
416
+ base_temporal_compression_factor: int | None = None,
417
+ action_start_frame_offset: int = 1,
418
+ ) -> int:
419
+ """Pack action tokens into the sequence.
420
+
421
+ Args:
422
+ input_action_tokens: Action latent tokens (T, D).
423
+ condition_frame_indexes_action: Indexes of conditioning action steps.
424
+ input_timestep: Diffusion timestep.
425
+ action_temporal_offset: Temporal offset for action mRoPE IDs (typically
426
+ the vision start offset so action aligns temporally with vision).
427
+ enable_fps_modulation: If True, scale temporal position IDs based on FPS.
428
+ base_fps: Base FPS for normalization (default 24.0).
429
+ action_fps: Frames per second of the action data. Used when enable_fps_modulation=True.
430
+ base_temporal_compression_factor: Base temporal compression factor for FPS scaling.
431
+ Should be set to the vision temporal compression factor (e.g. 4) so that action
432
+ tokens advance at frame rate (4x finer) relative to vision latent frames.
433
+ Only affects behavior when FPS modulation is enabled.
434
+ action_start_frame_offset: Frame offset for aligning action[0] with the
435
+ corresponding vision frame. Default 1 aligns action[0] with vision frame 1.
436
+
437
+ Returns:
438
+ Number of action tokens added.
439
+ """
440
+ action_split_len = input_action_tokens.shape[0]
441
+
442
+ action = self.ensure_action()
443
+
444
+ # Add token indexes and loss information
445
+ action.token_shapes.append((action_split_len,))
446
+ action.tokens.append(input_action_tokens)
447
+ action_payload_index = len(action.tokens) - 1
448
+
449
+ condition_set = {idx for idx in condition_frame_indexes_action if 0 <= idx < action_split_len}
450
+
451
+ action_condition_mask = torch.zeros(
452
+ (action_split_len, 1), device=input_action_tokens.device, dtype=input_action_tokens.dtype
453
+ ) # [T_action,1]
454
+ for frame_idx in condition_set:
455
+ action_condition_mask[frame_idx, 0] = 1.0
456
+ action.condition_mask.append(action_condition_mask)
457
+
458
+ action_noisy_frame_indexes = torch.tensor(
459
+ [idx for idx in range(action_split_len) if idx not in condition_set],
460
+ device=input_action_tokens.device,
461
+ dtype=torch.long,
462
+ ) # [N_noisy_action_frames]
463
+ action.noisy_frame_indexes.append(action_noisy_frame_indexes)
464
+
465
+ # Action tokens use a 1x1 spatial grid with start_frame_offset=1 by default,
466
+ # so action[0] (null token) aligns with vision frame 1, not frame 0.
467
+ effective_fps = action_fps if enable_fps_modulation else None
468
+
469
+ action_mrope_ids, _ = get_3d_mrope_ids_vae_tokens(
470
+ grid_t=action_split_len,
471
+ grid_h=1,
472
+ grid_w=1,
473
+ temporal_offset=action_temporal_offset,
474
+ reset_spatial_indices=self._mrope_reset_spatial,
475
+ fps=effective_fps,
476
+ base_fps=base_fps,
477
+ temporal_compression_factor=1, # Action is at frame rate (no temporal compression)
478
+ base_temporal_compression_factor=base_temporal_compression_factor,
479
+ start_frame_offset=action_start_frame_offset, # Align action[0] with vision frame action_start_frame_offset
480
+ ) # action_mrope_ids: [3,N_action_tokens]
481
+ # Note: we don't update _mrope_temporal_offset here because action tokens
482
+ # share the temporal space with vision tokens (they run in parallel).
483
+
484
+ for frame_idx in range(action_split_len):
485
+ position_ids = action_mrope_ids[:, frame_idx : frame_idx + 1] # [3,1]
486
+ frame_indexes = self.append_action_span(
487
+ 1,
488
+ position_ids,
489
+ payload_index=action_payload_index,
490
+ payload_start=frame_idx,
491
+ payload_shape=(1,),
492
+ )
493
+
494
+ if frame_idx in condition_set:
495
+ continue
496
+ action.mse_loss_indexes.extend(frame_indexes)
497
+ action.timesteps.extend([input_timestep])
498
+
499
+ return action_split_len
500
+
501
+ def pack_sound_tokens(
502
+ self,
503
+ input_sound_tokens: torch.Tensor,
504
+ condition_frame_indexes_sound: list[int],
505
+ input_timestep: float,
506
+ sound_temporal_offset: int | float = 0,
507
+ enable_fps_modulation: bool = False,
508
+ base_fps: float = 24.0,
509
+ sound_fps: float | None = None,
510
+ sound_base_temporal_compression_factor: int | None = None,
511
+ ) -> int:
512
+ """Pack sound/audio tokens into the sequence.
513
+
514
+ Sound latents have shape [C, T] where C is channels and T is temporal frames.
515
+ Sound tokens are added to the unified generation split to maintain SequencePack's
516
+ 2-split invariant (causal + full).
517
+
518
+ Args:
519
+ input_sound_tokens: Sound latent tokens (C, T).
520
+ condition_frame_indexes_sound: Indexes of conditioning frames.
521
+ [] means all frames are noised/supervised.
522
+ All frames specified means all frames are clean (no MSE supervision).
523
+ input_timestep: Diffusion timestep.
524
+ sound_temporal_offset: Temporal offset for m-RoPE position IDs (aligned with vision start).
525
+ enable_fps_modulation: If True, scale temporal positions by FPS ratio.
526
+ base_fps: Base FPS for normalization (default 24.0).
527
+ sound_fps: Sound latent FPS (e.g., 25.0). Used for FPS-aware m-RoPE positions.
528
+ sound_base_temporal_compression_factor: Base temporal compression factor for sound FPS scaling.
529
+ ``None`` preserves the current behavior where sound advances at ``base_fps`` positions/sec.
530
+
531
+ Returns:
532
+ Number of sound tokens added.
533
+ """
534
+ # Sound latent shape: [C, T] → T tokens
535
+ _, sound_split_len = input_sound_tokens.shape
536
+
537
+ sound = self.ensure_sound()
538
+
539
+ # Add token indexes - sound uses (T, 1, 1) shape for compatibility with 3D RoPE
540
+ sound.token_shapes.append((sound_split_len, 1, 1))
541
+ sound.tokens.append(input_sound_tokens)
542
+ sound_payload_index = len(sound.tokens) - 1
543
+
544
+ # Supervise sound tokens based on conditioning frames
545
+ condition_set = {idx for idx in condition_frame_indexes_sound if 0 <= idx < sound_split_len}
546
+
547
+ # Condition mask: shape (T, 1) — 1 = clean/conditioning, 0 = noised/supervised
548
+ sound_condition_mask = torch.zeros(
549
+ (sound_split_len, 1), device=input_sound_tokens.device, dtype=input_sound_tokens.dtype
550
+ ) # [T_sound,1]
551
+ for frame_idx in condition_set:
552
+ sound_condition_mask[frame_idx, 0] = 1.0
553
+ sound.condition_mask.append(sound_condition_mask)
554
+
555
+ sound_noisy_frame_indexes = torch.tensor(
556
+ [idx for idx in range(sound_split_len) if idx not in condition_set],
557
+ device=input_sound_tokens.device,
558
+ dtype=torch.long,
559
+ ) # [N_noisy_sound_frames]
560
+ sound.noisy_frame_indexes.append(sound_noisy_frame_indexes)
561
+
562
+ # Sound tokens use a 1x1 spatial grid, aligned with vision temporal positions.
563
+ # sound[0] aligns with vision frame 0 (start_frame_offset=0, unlike action which offsets by 1).
564
+ effective_fps = sound_fps if enable_fps_modulation else None
565
+
566
+ sound_mrope_ids, _ = get_3d_mrope_ids_vae_tokens(
567
+ grid_t=sound_split_len,
568
+ grid_h=1,
569
+ grid_w=1,
570
+ temporal_offset=sound_temporal_offset,
571
+ reset_spatial_indices=self._mrope_reset_spatial,
572
+ fps=effective_fps,
573
+ base_fps=base_fps,
574
+ temporal_compression_factor=1, # Sound latent is already at sound_latent_fps (no further compression)
575
+ base_temporal_compression_factor=sound_base_temporal_compression_factor,
576
+ start_frame_offset=0, # Sound[0] aligns with vision frame 0
577
+ ) # sound_mrope_ids: [3,N_sound_tokens]
578
+ # Note: we don't update _mrope_temporal_offset here because sound tokens
579
+ # share the temporal space with vision tokens (they run in parallel).
580
+
581
+ # Add to MSE loss indexes and timesteps for non-conditioning frames
582
+ for frame_idx in range(sound_split_len):
583
+ position_ids = sound_mrope_ids[:, frame_idx : frame_idx + 1] # [3,1]
584
+ frame_indexes = self.append_sound_span(
585
+ 1,
586
+ position_ids,
587
+ payload_index=sound_payload_index,
588
+ payload_start=frame_idx,
589
+ payload_shape=(1, 1, 1),
590
+ )
591
+
592
+ if frame_idx in condition_set:
593
+ continue
594
+ sound.mse_loss_indexes.extend(frame_indexes)
595
+ sound.timesteps.extend([input_timestep])
596
+
597
+ return sound_split_len
598
+
599
+ def _append_position_ids(self, position_ids: torch.Tensor, span_len: int) -> None:
600
+ """Append one block of position IDs.
601
+
602
+ Args:
603
+ position_ids: Position ID tensor with shape ``[3, span_len]``.
604
+ span_len: Number of token positions described by ``position_ids``.
605
+ """
606
+ assert position_ids.shape[-1] == span_len, (
607
+ f"position_ids last dimension must match span_len={span_len}; got {tuple(position_ids.shape)}"
608
+ )
609
+ self.position_ids.append(position_ids)
610
+
611
+ def _append_modality_span(
612
+ self,
613
+ modality: ModalityDataBuilder,
614
+ span_len: int,
615
+ position_ids: torch.Tensor,
616
+ *,
617
+ payload_index: int,
618
+ payload_start: int,
619
+ payload_shape: tuple[int, ...],
620
+ ) -> list[int]:
621
+ """Append one modality span and return its global sequence indexes.
622
+
623
+ Args:
624
+ modality: Modality builder receiving sequence indexes and span metadata.
625
+ span_len: Number of contiguous tokens to append.
626
+ position_ids: Position ID tensor with shape ``[3, span_len]``.
627
+ payload_index: Index into ``modality.tokens`` for the backing payload tensor.
628
+ payload_start: First flattened token offset within the backing payload tensor.
629
+ payload_shape: Logical payload slice shape represented by this span.
630
+
631
+ Returns:
632
+ Global packed-sequence indexes covered by the appended span.
633
+ """
634
+ assert payload_index >= 0, f"payload_index must be non-negative, got {payload_index}"
635
+ assert payload_start >= 0, f"payload_start must be non-negative, got {payload_start}"
636
+ payload_len = 1
637
+ for dim in payload_shape:
638
+ assert dim > 0, f"payload_shape dimensions must be positive, got {payload_shape}"
639
+ payload_len *= dim
640
+ assert payload_len == span_len, (
641
+ f"payload_shape={payload_shape} describes {payload_len} tokens, but span_len={span_len}"
642
+ )
643
+
644
+ span_start = self.current_seq_index
645
+ span_indexes = list(range(span_start, span_start + span_len))
646
+ modality.spans.append(
647
+ ModalitySpan(
648
+ sequence_start=span_start,
649
+ sequence_len=span_len,
650
+ payload_index=payload_index,
651
+ payload_start=payload_start,
652
+ payload_len=payload_len,
653
+ payload_shape=payload_shape,
654
+ )
655
+ )
656
+ modality.sequence_indexes.extend(span_indexes)
657
+ self._append_position_ids(position_ids, span_len)
658
+ self.current_seq_index += span_len
659
+ return span_indexes
660
+
661
+ def append_text_span(
662
+ self,
663
+ token_ids: list[int],
664
+ position_ids: torch.Tensor,
665
+ ) -> tuple[int, int]:
666
+ """Append one contiguous text span.
667
+
668
+ Args:
669
+ token_ids: Text token IDs to append.
670
+ position_ids: Position ID tensor with shape ``[3, len(token_ids)]``.
671
+
672
+ Returns:
673
+ Tuple of ``(start_index, span_len)`` for the appended span.
674
+ """
675
+ span_start = self.current_seq_index
676
+ span_len = len(token_ids)
677
+ self.text_ids.extend(token_ids)
678
+ self.text_indexes.extend(range(span_start, span_start + span_len))
679
+ self._append_position_ids(position_ids, span_len)
680
+ self.current_seq_index += span_len
681
+ return span_start, span_len
682
+
683
+ def append_vision_span(
684
+ self,
685
+ span_len: int,
686
+ position_ids: torch.Tensor,
687
+ *,
688
+ payload_index: int,
689
+ payload_start: int,
690
+ payload_shape: tuple[int, int, int],
691
+ ) -> list[int]:
692
+ """Append one contiguous vision span.
693
+
694
+ Args:
695
+ span_len: Number of contiguous vision tokens to append.
696
+ position_ids: Position ID tensor with shape ``[3, span_len]``.
697
+ payload_index: Index into ``vision.tokens`` for the backing payload tensor.
698
+ payload_start: First flattened token offset within the backing vision payload.
699
+ payload_shape: Logical vision slice shape, usually ``(1, patch_h, patch_w)``.
700
+
701
+ Returns:
702
+ Global packed-sequence indexes covered by the appended vision span.
703
+ """
704
+ return self._append_modality_span(
705
+ self.ensure_vision(),
706
+ span_len,
707
+ position_ids,
708
+ payload_index=payload_index,
709
+ payload_start=payload_start,
710
+ payload_shape=payload_shape,
711
+ )
712
+
713
+ def append_action_span(
714
+ self,
715
+ span_len: int,
716
+ position_ids: torch.Tensor,
717
+ *,
718
+ payload_index: int,
719
+ payload_start: int,
720
+ payload_shape: tuple[int],
721
+ ) -> list[int]:
722
+ """Append one contiguous action span.
723
+
724
+ Args:
725
+ span_len: Number of contiguous action tokens to append.
726
+ position_ids: Position ID tensor with shape ``[3, span_len]``.
727
+ payload_index: Index into ``action.tokens`` for the backing payload tensor.
728
+ payload_start: First flattened token offset within the backing action payload.
729
+ payload_shape: Logical action slice shape, usually ``(span_len,)``.
730
+
731
+ Returns:
732
+ Global packed-sequence indexes covered by the appended action span.
733
+ """
734
+ return self._append_modality_span(
735
+ self.ensure_action(),
736
+ span_len,
737
+ position_ids,
738
+ payload_index=payload_index,
739
+ payload_start=payload_start,
740
+ payload_shape=payload_shape,
741
+ )
742
+
743
+ def append_sound_span(
744
+ self,
745
+ span_len: int,
746
+ position_ids: torch.Tensor,
747
+ *,
748
+ payload_index: int,
749
+ payload_start: int,
750
+ payload_shape: tuple[int, int, int],
751
+ ) -> list[int]:
752
+ """Append one contiguous sound span.
753
+
754
+ Args:
755
+ span_len: Number of contiguous sound tokens to append.
756
+ position_ids: Position ID tensor with shape ``[3, span_len]``.
757
+ payload_index: Index into ``sound.tokens`` for the backing payload tensor.
758
+ payload_start: First flattened token offset within the backing sound payload.
759
+ payload_shape: Logical sound slice shape, usually ``(1, 1, 1)``.
760
+
761
+ Returns:
762
+ Global packed-sequence indexes covered by the appended sound span.
763
+ """
764
+ return self._append_modality_span(
765
+ self.ensure_sound(),
766
+ span_len,
767
+ position_ids,
768
+ payload_index=payload_index,
769
+ payload_start=payload_start,
770
+ payload_shape=payload_shape,
771
+ )
772
+
773
+ def append_end_of_generation_token(
774
+ self,
775
+ token_id: int,
776
+ use_float_mrope_positions: bool,
777
+ ) -> int:
778
+ """Append an end-of-generation text token.
779
+
780
+ Args:
781
+ token_id: Token ID for the end-of-generation marker.
782
+ use_float_mrope_positions: Whether to write float position IDs for this token.
783
+
784
+ Returns:
785
+ Span length for the appended end-of-generation token.
786
+ """
787
+ eov_dtype = torch.float32 if use_float_mrope_positions else torch.long
788
+ position_ids = torch.full((3, 1), self._mrope_temporal_offset, dtype=eov_dtype) # [3,1]
789
+ self._mrope_temporal_offset += 1
790
+
791
+ _, span_len = self.append_text_span([token_id], position_ids)
792
+ return span_len
793
+
794
+ def finish_sample(self, generation_split_len: int, sample_len: int) -> None:
795
+ """Record the non-causal generation split and total sample length.
796
+
797
+ Args:
798
+ generation_split_len: Length of the full-attention generation split.
799
+ sample_len: Total number of tokens appended for this sample.
800
+ """
801
+ self.attn_modes.append("full")
802
+ self.split_lens.append(generation_split_len)
803
+ self.sample_lens.append(sample_len)
804
+
805
+ def _finalize_modality(
806
+ self,
807
+ modality: ModalityDataBuilder | None,
808
+ *,
809
+ domain_id: list[torch.Tensor] | None = None,
810
+ raw_action_dim: list[torch.Tensor | None] | None = None,
811
+ residual_gate: list[torch.Tensor] | None = None,
812
+ include_raw_action_dim: bool = False,
813
+ ) -> ModalityData | None:
814
+ """Finalize one modality builder into model-facing modality data.
815
+
816
+ Args:
817
+ modality: Modality builder to finalize, or ``None`` if no tokens were appended.
818
+ domain_id: Optional action domain IDs to attach to finalized action data.
819
+ raw_action_dim: Optional raw action dimensions for action-channel masking.
820
+ include_raw_action_dim: Whether to include ``raw_action_dim`` in the finalized data.
821
+
822
+ Returns:
823
+ Finalized ``ModalityData`` or ``None`` when the modality has no sequence indexes.
824
+ """
825
+ if modality is None or len(modality.sequence_indexes) == 0:
826
+ return None
827
+
828
+ kwargs = {
829
+ "sequence_indexes": torch.tensor(modality.sequence_indexes, dtype=torch.long), # [N_modality_tokens]
830
+ "timesteps": torch.tensor(modality.timesteps, dtype=torch.float32), # [N_modality_noisy_tokens]
831
+ "mse_loss_indexes": torch.tensor(modality.mse_loss_indexes, dtype=torch.long), # [N_modality_noisy_tokens]
832
+ "spans": list(modality.spans),
833
+ "token_shapes": list(modality.token_shapes),
834
+ "tokens": modality.tokens,
835
+ "condition_mask": list(modality.condition_mask),
836
+ "noisy_frame_indexes": list(modality.noisy_frame_indexes),
837
+ }
838
+ if domain_id is not None:
839
+ kwargs["domain_id"] = domain_id
840
+ if include_raw_action_dim:
841
+ kwargs["raw_action_dim"] = raw_action_dim
842
+ if residual_gate is not None:
843
+ kwargs["residual_gate"] = residual_gate
844
+ return ModalityData(**kwargs)
845
+
846
+ def finalize(
847
+ self,
848
+ gen_data_clean: GenerationDataClean,
849
+ ) -> "PackedSequence":
850
+ """Convert all lists to tensors and compute derived values.
851
+
852
+ Args:
853
+ gen_data_clean: GenerationDataClean for metadata (e.g., action domain IDs).
854
+
855
+ Returns:
856
+ New PackedSequence instance with tensors instead of lists.
857
+ """
858
+ # Compute sequence length
859
+ sequence_length = sum(self.sample_lens)
860
+ sample_lens = self.sample_lens.copy()
861
+ split_lens = self.split_lens.copy()
862
+ attn_modes = self.attn_modes.copy()
863
+
864
+ # Prepare loss-related tensors (cross-entropy)
865
+ label_ids: torch.Tensor | None = None
866
+ ce_loss_indexes: torch.Tensor | None = None
867
+ ce_loss_weights: torch.Tensor | None = None
868
+ if self.label_ids and len(self.label_ids) > 0:
869
+ label_ids = torch.tensor(self.label_ids) # [N_ce_tokens]
870
+ ce_loss_indexes = torch.tensor(self.ce_loss_indexes) # [N_ce_tokens]
871
+ ce_loss_weights = torch.tensor(self.ce_loss_weights) # [N_ce_tokens]
872
+
873
+ # The condition_mask and noisy_frame_indexes are kept as lists to support variable shapes.
874
+
875
+ vision = self._finalize_modality(self.vision)
876
+ action_domain_id = None
877
+ if self.action is not None:
878
+ if gen_data_clean.action_domain_id is not None:
879
+ action_domain_id = gen_data_clean.action_domain_id
880
+ else:
881
+ default_action_domain_id = torch.zeros(1, dtype=torch.long) # [1]
882
+ action_domain_id = [default_action_domain_id] * len(self.action.token_shapes)
883
+ action = self._finalize_modality(
884
+ self.action,
885
+ domain_id=action_domain_id,
886
+ raw_action_dim=gen_data_clean.raw_action_dim,
887
+ residual_gate=gen_data_clean.action_residual_gate,
888
+ include_raw_action_dim=True,
889
+ )
890
+ sound = self._finalize_modality(self.sound)
891
+
892
+ # Finalize position IDs.
893
+ assert isinstance(self.position_ids, list)
894
+ assert all(isinstance(position_id, torch.Tensor) for position_id in self.position_ids)
895
+ if len(self.position_ids) > 0:
896
+ position_ids = torch.cat(self.position_ids, dim=1) # [3,actual_seq_len]
897
+ else:
898
+ position_ids = torch.empty((3, 0), dtype=torch.long) # [3,0]
899
+
900
+ return PackedSequence(
901
+ # Sequence structure
902
+ sequence_length=sequence_length,
903
+ sample_lens=sample_lens,
904
+ split_lens=split_lens,
905
+ attn_modes=attn_modes,
906
+ is_image_batch=gen_data_clean.is_image_batch,
907
+ uses_single_timestep=self.uses_single_timestep,
908
+ # Text modality (converted to tensors)
909
+ text_ids=torch.tensor(self.text_ids, dtype=torch.long), # [N_text_tokens]
910
+ text_indexes=torch.tensor(self.text_indexes, dtype=torch.long), # [N_text_tokens]
911
+ behavior_indexes=torch.tensor(self.behavior_indexes, dtype=torch.long),
912
+ online_memory_indexes=torch.tensor(self.online_memory_indexes, dtype=torch.long),
913
+ proprio_indexes=torch.tensor(self.proprio_indexes, dtype=torch.long),
914
+ position_ids=position_ids, # [3,seq_len]
915
+ # Loss computation - Cross Entropy
916
+ label_ids=label_ids,
917
+ ce_loss_indexes=ce_loss_indexes,
918
+ ce_loss_weights=ce_loss_weights,
919
+ # Generation modalities
920
+ vision=vision,
921
+ action=action,
922
+ sound=sound,
923
+ # Temporal causal
924
+ null_action_supertokens=self.null_action_supertokens,
925
+ num_action_tokens_per_supertoken=self.num_action_tokens_per_supertoken,
926
+ # Multi-control transfer
927
+ vision_item_split_lens=list(self.vision_item_split_lens),
928
+ control_weights=gen_data_clean.control_weights,
929
+ )
930
+
931
+
932
+ @dataclass
933
+ class PackedSequence:
934
+ """Finalized model-facing packed sequence.
935
+
936
+ The object remains mutable for model-side payload replacement, clean replay,
937
+ and in-place device transfer. Build-only cursor and mRoPE state live on
938
+ ``PackedSequenceBuilder``.
939
+
940
+ Attributes:
941
+ sample_lens: Length of each sample in the packed sequence.
942
+ split_lens: Length of each attention split. Each sample contributes a causal
943
+ text split and a full generation split.
944
+ attn_modes: Attention mode for each split, such as ``"causal"`` or ``"full"``.
945
+ is_image_batch: Whether this batch contains images rather than videos.
946
+ uses_single_timestep: Whether all noised tokens share one input timestep scalar.
947
+ sequence_length: Total length of the packed sequence.
948
+ text_ids: Tensor of all text token IDs, including special tokens.
949
+ text_indexes: Tensor of global packed-sequence indexes for text tokens.
950
+ position_ids: Tensor of mRoPE position IDs for all packed tokens with shape
951
+ ``[3, sequence_length]``.
952
+ label_ids: Optional tensor of label IDs for text cross-entropy loss.
953
+ ce_loss_indexes: Optional tensor of global packed-sequence indexes for text
954
+ cross-entropy loss.
955
+ ce_loss_weights: Optional tensor of per-token weights for text cross-entropy loss.
956
+ null_action_supertokens: Whether temporal-causal supertoken 0 contains null
957
+ action tokens.
958
+ num_action_tokens_per_supertoken: Number of action tokens prefixing each
959
+ temporal-causal vision supertoken.
960
+ vision: Finalized vision modality data, or ``None`` if no vision is present.
961
+ action: Finalized action modality data, or ``None`` if no action is present.
962
+ sound: Finalized sound modality data, or ``None`` if no sound is present.
963
+ vision_item_split_lens: Per-sample per-vision-item token counts for multi-control
964
+ transfer.
965
+ control_weights: Per-sample per-control weights for multi-control weighted V-scaling.
966
+ """
967
+
968
+ # Sequence structure
969
+ sample_lens: list[int] = field(default_factory=list)
970
+ split_lens: list[int] = field(default_factory=list)
971
+ attn_modes: list[str] = field(default_factory=list)
972
+ is_image_batch: bool = False
973
+ uses_single_timestep: bool = False
974
+ sequence_length: int = 0
975
+
976
+ # Text modality
977
+ text_ids: torch.Tensor = field(default_factory=_empty_long_tensor)
978
+ text_indexes: torch.Tensor = field(default_factory=_empty_long_tensor)
979
+ behavior_indexes: torch.Tensor = field(default_factory=_empty_long_tensor)
980
+ online_memory_indexes: torch.Tensor = field(default_factory=_empty_long_tensor)
981
+ proprio_indexes: torch.Tensor = field(default_factory=_empty_long_tensor)
982
+ position_ids: torch.Tensor = field(default_factory=_empty_long_tensor)
983
+
984
+ # Loss computation - Cross Entropy (text)
985
+ label_ids: torch.Tensor | None = None
986
+ ce_loss_indexes: torch.Tensor | None = None
987
+ ce_loss_weights: torch.Tensor | None = None
988
+
989
+ # Temporal causal: whether supertoken 0's action slot contains null tokens.
990
+ # True for all training calls and AR frame 0; False for AR frame N>0 (real actions).
991
+ # Used by three_way_attention to zero out V for null action tokens (inline when attention_meta.null_action_supertokens=True).
992
+ null_action_supertokens: bool = False
993
+
994
+ # Temporal causal: number of action tokens prefixing each vision supertoken.
995
+ # Equals temporal_compression_factor when actions are packed inline; 0 when
996
+ # action_gen=False or for non-temporal-causal layouts. Single source of truth
997
+ # for downstream attention/KV-cache code (per-supertoken layout is
998
+ # num_action_tokens_per_supertoken + H_p * W_p).
999
+ num_action_tokens_per_supertoken: int = 0
1000
+
1001
+ # Generation modalities - NAMED FIELDS for type safety
1002
+ vision: ModalityData | None = None
1003
+ action: ModalityData | None = None
1004
+ sound: ModalityData | None = None
1005
+
1006
+ # Multi-control transfer: per-sample list of per-vision-item token counts.
1007
+ # For a multi-control transfer sample with N controls + 1 noisy target,
1008
+ # vision_item_split_lens[i] = [L_ctrl0, L_ctrl1, ..., L_ctrlN-1, L_noisy].
1009
+ # Used by cosmos3_vfm_network.py to derive gen-relative control/noisy ranges
1010
+ # for multi_control_two_way_attention.
1011
+ vision_item_split_lens: list[list[int]] = field(default_factory=list)
1012
+
1013
+ # Per-sample per-control weights for multi-control weighted V-scaling.
1014
+ # Parallel to vision_item_split_lens[i][:-1] (excludes noisy item).
1015
+ # None for non-transfer or standard single-control samples.
1016
+ control_weights: list[list[float]] | None = None
1017
+
1018
+ def __post_init__(self) -> None:
1019
+ self._sequence_pack_metadata: SequencePackMetadata | None = None
1020
+ assert isinstance(self.text_ids, torch.Tensor), "PackedSequence.text_ids must be finalized"
1021
+ assert isinstance(self.text_indexes, torch.Tensor), "PackedSequence.text_indexes must be finalized"
1022
+ assert isinstance(self.behavior_indexes, torch.Tensor), "PackedSequence.behavior_indexes must be finalized"
1023
+ assert isinstance(self.online_memory_indexes, torch.Tensor), "PackedSequence.online_memory_indexes must be finalized"
1024
+ assert isinstance(self.proprio_indexes, torch.Tensor), "PackedSequence.proprio_indexes must be finalized"
1025
+ assert isinstance(self.position_ids, torch.Tensor), "PackedSequence.position_ids must be finalized"
1026
+ if self.label_ids is not None:
1027
+ assert isinstance(self.label_ids, torch.Tensor), "PackedSequence.label_ids must be finalized"
1028
+ if self.ce_loss_indexes is not None:
1029
+ assert isinstance(self.ce_loss_indexes, torch.Tensor), "PackedSequence.ce_loss_indexes must be finalized"
1030
+ if self.ce_loss_weights is not None:
1031
+ assert isinstance(self.ce_loss_weights, torch.Tensor), "PackedSequence.ce_loss_weights must be finalized"
1032
+ for modality in [self.vision, self.action, self.sound]:
1033
+ assert modality is None or isinstance(modality, ModalityData), (
1034
+ "PackedSequence modality fields must be finalized ModalityData"
1035
+ )
1036
+
1037
+ def to_cuda(self) -> None:
1038
+ """Move all tensor fields to CUDA in-place."""
1039
+ self.text_ids = self.text_ids.cuda()
1040
+ self.text_indexes = self.text_indexes.cuda()
1041
+ self.behavior_indexes = self.behavior_indexes.cuda()
1042
+ self.online_memory_indexes = self.online_memory_indexes.cuda()
1043
+ self.proprio_indexes = self.proprio_indexes.cuda()
1044
+ self.position_ids = self.position_ids.cuda()
1045
+ if isinstance(self.label_ids, torch.Tensor):
1046
+ self.label_ids = self.label_ids.cuda()
1047
+ if isinstance(self.ce_loss_indexes, torch.Tensor):
1048
+ self.ce_loss_indexes = self.ce_loss_indexes.cuda()
1049
+ if isinstance(self.ce_loss_weights, torch.Tensor):
1050
+ self.ce_loss_weights = self.ce_loss_weights.cuda()
1051
+ if self.vision is not None:
1052
+ self.vision.to_cuda()
1053
+ if self.action is not None:
1054
+ self.action.to_cuda()
1055
+ if self.sound is not None:
1056
+ self.sound.to_cuda()
1057
+ self.prepare_sequence_pack_metadata()
1058
+
1059
+ def prepare_sequence_pack_metadata(self) -> None:
1060
+ """Validate and prepare device-specific metadata for this layout."""
1061
+ understanding_indexes = torch.cat(
1062
+ (self.behavior_indexes, self.online_memory_indexes, self.proprio_indexes, self.text_indexes)
1063
+ ).sort().values
1064
+ self._sequence_pack_metadata = prepare_sequence_pack_metadata(
1065
+ sample_lens=self.sample_lens,
1066
+ split_lens=self.split_lens,
1067
+ attn_modes=self.attn_modes,
1068
+ packed_und_token_indexes=understanding_indexes,
1069
+ device=self.text_indexes.device,
1070
+ )
1071
+
1072
+ def get_sequence_pack_metadata(self) -> SequencePackMetadata | None:
1073
+ """Return metadata prepared after the packed input reached its device."""
1074
+ return self._sequence_pack_metadata
1075
+
1076
+
1077
+ @dataclass
1078
+ class SequencePlan:
1079
+ """Plan describing which modalities are present in a sample.
1080
+
1081
+ This dataclass tracks the presence of different modalities (text, vision, action)
1082
+ and their conditioning configurations for a dataset sample. Unlike SequencePlan
1083
+ which holds the actual tensor data, this class provides a lightweight summary
1084
+ of what modalities exist and how they should be conditioned.
1085
+
1086
+ Attributes:
1087
+ has_text: Whether text/caption tokens are present for this sample.
1088
+ Used for text-conditioned generation (e.g., text-to-image/video).
1089
+ has_vision: Whether vision input (image or video latents) is present.
1090
+ Defaults to False.
1091
+ condition_frame_indexes_vision: Indexes of latent vision frames that are clean/conditioning.
1092
+ [] means all frames are noised/supervised.
1093
+ All frames specified means all frames are clean (no MSE supervision).
1094
+ For multi-item samples (e.g. image editing where each sample has multiple
1095
+ separately-encoded images), this applies to each vision item individually.
1096
+ The number of items per sample is tracked by
1097
+ ``GenerationDataClean.num_vision_items_per_sample``.
1098
+ share_vision_temporal_positions: Whether all vision items in this sample share
1099
+ the same temporal mRoPE grid.
1100
+ has_action: Whether action input is present for robotics/embodied AI tasks.
1101
+ Defaults to False.
1102
+ condition_frame_indexes_action: Indexes of action steps that are clean/conditioning.
1103
+ [] means all steps are noised/supervised.
1104
+ All steps specified means all steps are clean (no MSE supervision).
1105
+ action_start_frame_offset: Frame offset for aligning action[0] to vision frames.
1106
+ has_sound: Whether sound/audio input is present.
1107
+ condition_frame_indexes_sound: Indexes of sound frames that are clean/conditioning.
1108
+ [] means all frames are noised/supervised.
1109
+ All frames specified means all frames are clean (no MSE supervision).
1110
+ """
1111
+
1112
+ # -- understanding (text conditioning) --
1113
+ has_text: bool
1114
+
1115
+ # -- vision modality --
1116
+ has_vision: bool = False
1117
+ condition_frame_indexes_vision: list[int] = field(default_factory=list)
1118
+ # If True, all vision items in this sample share the same temporal mRoPE grid
1119
+ # (controlnet-style transfer: target frame i is spatio-temporally aligned with
1120
+ # control frame i). Each item gets the same temporal_offset; spatial reset
1121
+ # behavior is unchanged. Requires num_vision_items_per_sample > 1, equal latent_t,
1122
+ # and equal fps across items. Default False preserves single-clip and
1123
+ # image-editing semantics where items represent distinct time states.
1124
+ share_vision_temporal_positions: bool = False
1125
+
1126
+ # -- action modality --
1127
+ has_action: bool = False
1128
+ condition_frame_indexes_action: list[int] = field(default_factory=list)
1129
+ action_start_frame_offset: int = 1
1130
+
1131
+ # -- sound modality --
1132
+ has_sound: bool = False
1133
+ condition_frame_indexes_sound: list[int] = field(default_factory=list)
1134
+
1135
+ def as_dict(self) -> dict:
1136
+ return {
1137
+ "has_text": self.has_text,
1138
+ "has_vision": self.has_vision,
1139
+ "has_action": self.has_action,
1140
+ "has_sound": self.has_sound,
1141
+ "condition_frame_indexes_vision": self.condition_frame_indexes_vision,
1142
+ "condition_frame_indexes_action": self.condition_frame_indexes_action,
1143
+ "condition_frame_indexes_sound": self.condition_frame_indexes_sound,
1144
+ "share_vision_temporal_positions": self.share_vision_temporal_positions,
1145
+ }
1146
+
1147
+
1148
+ def build_sequence_plans_from_data_batch(
1149
+ data_batch: dict,
1150
+ input_video_key,
1151
+ input_image_key: str,
1152
+ ) -> list[SequencePlan]:
1153
+ """Build or retrieve sequence plans from a data batch dictionary.
1154
+
1155
+ This function extracts sequence plans from the data batch if they exist,
1156
+ otherwise creates default SequencePlan objects for each sample
1157
+ in the batch.
1158
+
1159
+ Args:
1160
+ data_batch: Dictionary containing the data batch from the dataloader.
1161
+ Expected keys include 'video' or other tensors to determine batch size.
1162
+ If 'sequence_plan' key exists, those plans are returned directly.
1163
+ input_video_key: Data-batch key used to find video tensors when inferring batch size.
1164
+ input_image_key: Data-batch key used to find image tensors when inferring batch size.
1165
+
1166
+ Returns:
1167
+ List of SequencePlan objects, one per sample in the batch.
1168
+ """
1169
+ # For new modalities, please generate the sequence_plan in the dataset class!!!!
1170
+
1171
+ # If sequence_plan already exists in data_batch, return it
1172
+ if "sequence_plan" in data_batch:
1173
+ return data_batch["sequence_plan"]
1174
+
1175
+ assert "action" not in data_batch or data_batch["action"] is None, "Action data SHOULD have sequence_plans!"
1176
+ assert "sound" not in data_batch or data_batch["sound"] is None, "Sound data SHOULD have sequence_plans!"
1177
+
1178
+ # Determine batch size from available tensors
1179
+ batch_size = 0
1180
+ for key in [input_video_key, input_image_key]:
1181
+ if key in data_batch:
1182
+ val = data_batch[key]
1183
+ if isinstance(val, torch.Tensor):
1184
+ batch_size = val.shape[0]
1185
+ break
1186
+ elif isinstance(val, list):
1187
+ batch_size = len(val)
1188
+ break
1189
+
1190
+ if batch_size == 0:
1191
+ raise ValueError(
1192
+ f"Cannot determine batch size from data_batch. Expected {input_video_key}, {input_image_key}, or similar key."
1193
+ )
1194
+
1195
+ # Build default SequencePlan objects
1196
+ return [
1197
+ SequencePlan(
1198
+ has_text=True, # Has text prompt!
1199
+ has_vision=True,
1200
+ condition_frame_indexes_vision=[], # No conditioning frames!
1201
+ )
1202
+ for _ in range(batch_size)
1203
+ ]
releases/bridge-native-v3/code/cosmos_framework/inference/common/public_model_config.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ from __future__ import annotations
5
+
6
+ import copy
7
+ import importlib.util
8
+ from typing import Any
9
+
10
+ _PUBLIC_TARGET_KEY = "_target"
11
+ _PUBLIC_TYPE_KEY = "_type"
12
+ _CLASS_NAME_KEY = "class_name"
13
+ _CONFIG_NAME_KEY = "config_name"
14
+ _TYPE_KEY = "_type"
15
+ _TARGET_KEY = "_target_"
16
+
17
+ _PUBLIC_VFM_URI_PREFIX = "cosmos3://vfm/"
18
+
19
+ _TARGET_ALIASES = {
20
+ "projects.cosmos3.vfm.configs.base.defaults.vlm.create_qwen2_tokenizer_with_download": "create_qwen2_tokenizer_with_download",
21
+ "projects.cosmos3.vfm.configs.base.defaults.vlm.create_vlm_config": "create_vlm_config",
22
+ "projects.cosmos3.vfm.models.mot.unified_mot.Nemotron3DenseVLMoTConfig.from_json_file": "nemotron3_dense_vl_mot_config_from_json_file",
23
+ "projects.cosmos3.vfm.models.mot.unified_mot.Nemotron3DenseVLTextForCausalLM": "nemotron3_dense_vl_text_for_causal_lm",
24
+ "projects.cosmos3.vfm.models.mot.unified_mot.Qwen3VLMoTConfig.from_json_file": "qwen3_vl_mot_config_from_json_file",
25
+ "projects.cosmos3.vfm.models.mot.unified_mot.Qwen3VLTextForCausalLM": "qwen3_vl_text_for_causal_lm",
26
+ "projects.cosmos3.vfm.models.omni_mot_model.OmniMoTModel": "omni_mot_model",
27
+ "projects.cosmos3.vfm.processors.build_processor_lazy": "build_processor_lazy",
28
+ "projects.cosmos3.vfm.tokenizers.audio.avae.AVAEInterface": "avae_interface",
29
+ "projects.cosmos3.vfm.tokenizers.wan2pt2_vae_4x16x16.Wan2pt2VAEInterface": "wan2pt2_vae_interface",
30
+ }
31
+
32
+ # The Zeva training branch exposes config classes under the
33
+ # cosmos_framework.configs.* package path (mirrors of projects.cosmos3.vfm.*).
34
+ # Map them to the same public aliases so exported HF configs stay identical.
35
+ _CF_TYPE_ALIASES = {
36
+ "cosmos_framework.configs.base.defaults.model_config.ZevaPolicyConfig": "zeva_policy_config",
37
+ "cosmos_framework.configs.base.defaults.model_config.BehaviorStage2Config": "behavior_stage2_config",
38
+ "cosmos_framework.configs.base.defaults.model_config.ProprioConditionConfig": "proprio_condition_config",
39
+ "cosmos_framework.configs.base.defaults.model_config.DiffusionExpertConfig": "diffusion_expert_config",
40
+ "cosmos_framework.configs.base.defaults.model_config.FixedStepSamplerConfig": "fixed_step_sampler_config",
41
+ "cosmos_framework.configs.base.defaults.model_config.LBLConfig": "lbl_config",
42
+ "cosmos_framework.configs.base.defaults.model_config.OmniMoTModelConfig": "omni_mot_model_config",
43
+ "cosmos_framework.configs.base.defaults.model_config.RectifiedFlowInferenceConfig": "rectified_flow_inference_config",
44
+ "cosmos_framework.configs.base.defaults.model_config.RectifiedFlowTrainingConfig": "rectified_flow_training_config",
45
+ "cosmos_framework.configs.base.defaults.activation_checkpointing.ActivationCheckpointingConfig": "activation_checkpointing_config",
46
+ "cosmos_framework.configs.base.defaults.compile.CompileConfig": "compile_config",
47
+ "cosmos_framework.configs.base.defaults.ema.EMAConfig": "ema_config",
48
+ "cosmos_framework.configs.base.defaults.parallelism.ParallelismConfig": "parallelism_config",
49
+ "cosmos_framework.configs.base.defaults.quantization.QuantizationConfig": "quantization_config",
50
+ "cosmos_framework.configs.base.defaults.vlm.PretrainedWeightsConfig": "pretrained_weights_config",
51
+ "cosmos_framework.configs.base.defaults.vlm.VLMConfig": "vlm_config",
52
+ "cosmos_framework.model.generator.tokenizers.wan2pt2_vae_4x16x16.Wan2pt2VAEInterface": "wan2pt2_vae_interface",
53
+ "cosmos_framework.model.generator.mot.unified_mot.Qwen3VLMoTConfig.from_json_file": "qwen3_vl_mot_config_from_json_file",
54
+ "cosmos_framework.model.generator.mot.unified_mot.Qwen3VLTextForCausalLM": "qwen3_vl_text_for_causal_lm",
55
+ "cosmos_framework.model.generator.omni_mot_model.OmniMoTModel": "omni_mot_model",
56
+ "cosmos_framework.configs.base.defaults.reasoner.create_qwen2_tokenizer_with_download": "create_qwen2_tokenizer_with_download",
57
+ "cosmos_framework.configs.base.defaults.reasoner.create_vlm_config": "create_vlm_config",
58
+ }
59
+
60
+ _TYPE_ALIASES = {
61
+ # Zeva branch (canonicalized under projects.cosmos3.vfm.configs.base.*)
62
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.ZevaPolicyConfig": "zeva_policy_config",
63
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.BehaviorStage2Config": "behavior_stage2_config",
64
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.ProprioConditionConfig": "proprio_condition_config",
65
+ "projects.cosmos3.vfm.configs.base.defaults.activation_checkpointing.ActivationCheckpointingConfig": "activation_checkpointing_config",
66
+ "projects.cosmos3.vfm.configs.base.defaults.compile.CompileConfig": "compile_config",
67
+ "projects.cosmos3.vfm.configs.base.defaults.ema.EMAConfig": "ema_config",
68
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.DiffusionExpertConfig": "diffusion_expert_config",
69
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.FixedStepSamplerConfig": "fixed_step_sampler_config",
70
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.LBLConfig": "lbl_config",
71
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.OmniMoTModelConfig": "omni_mot_model_config",
72
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.RectifiedFlowInferenceConfig": "rectified_flow_inference_config",
73
+ "projects.cosmos3.vfm.configs.base.defaults.model_config.RectifiedFlowTrainingConfig": "rectified_flow_training_config",
74
+ "projects.cosmos3.vfm.configs.base.defaults.parallelism.ParallelismConfig": "parallelism_config",
75
+ "projects.cosmos3.vfm.configs.base.defaults.quantization.QuantizationConfig": "quantization_config",
76
+ "projects.cosmos3.vfm.configs.base.defaults.vlm.PretrainedWeightsConfig": "pretrained_weights_config",
77
+ "projects.cosmos3.vfm.configs.base.defaults.vlm.VLMConfig": "vlm_config",
78
+ }
79
+
80
+ # Config objects can be serialized as either `_type` or `_target_` depending on
81
+ # whether they came from structured config metadata or LazyCall construction.
82
+ _TARGET_ALIASES.update(_TYPE_ALIASES)
83
+ _TARGET_ALIASES.update(_CF_TYPE_ALIASES)
84
+ _TYPE_ALIASES.update(_CF_TYPE_ALIASES)
85
+
86
+ _ALIAS_TARGETS = {alias: target for target, alias in _TARGET_ALIASES.items()}
87
+ _ALIAS_TYPES = {alias: target for target, alias in _TYPE_ALIASES.items()}
88
+
89
+
90
+ def build_public_model_config(model_config: dict[str, Any]) -> dict[str, Any]:
91
+ """Build public HF model metadata from the internal LazyConfig model dict."""
92
+
93
+ return _to_public_model_config(model_config)
94
+
95
+
96
+ def restore_model_config_from_public_model_config(model_config: dict[str, Any]) -> dict[str, Any]:
97
+ """Restore the internal LazyConfig model dict from public HF model metadata."""
98
+
99
+ return _from_public_model_config(model_config)
100
+
101
+
102
+ def model_config_uses_public_aliases(model_config: Any) -> bool:
103
+ """Return whether a model config uses public aliases."""
104
+
105
+ return _has_public_alias(model_config)
106
+
107
+
108
+ def load_model_config_from_hf_config(config_dict: dict[str, Any]) -> dict[str, Any]:
109
+ """Load internal model config from either legacy or public HF config."""
110
+
111
+ if "model" in config_dict:
112
+ model_config = config_dict["model"]
113
+ if model_config_uses_public_aliases(model_config):
114
+ return restore_model_config_from_public_model_config(model_config)
115
+ return model_config
116
+ raise KeyError("HF config must contain 'model'")
117
+
118
+
119
+ def _has_key(obj: Any, key: str) -> bool:
120
+ if isinstance(obj, dict):
121
+ return key in obj or any(_has_key(value, key) for value in obj.values())
122
+ if isinstance(obj, list):
123
+ return any(_has_key(value, key) for value in obj)
124
+ return False
125
+
126
+
127
+ def _has_public_alias(obj: Any) -> bool:
128
+ if isinstance(obj, dict):
129
+ for key, value in obj.items():
130
+ if key in {_PUBLIC_TARGET_KEY, _CLASS_NAME_KEY, _CONFIG_NAME_KEY}:
131
+ return True
132
+ if key == _PUBLIC_TYPE_KEY and _is_type_alias(value):
133
+ return True
134
+ if _has_public_alias(value):
135
+ return True
136
+ elif isinstance(obj, list):
137
+ return any(_has_public_alias(value) for value in obj)
138
+ return False
139
+
140
+
141
+ def _to_public_model_config(obj: Any) -> Any:
142
+ if isinstance(obj, list):
143
+ return [_to_public_model_config(item) for item in obj]
144
+ if isinstance(obj, dict):
145
+ data = {}
146
+ for key, value in obj.items():
147
+ if key == _TARGET_KEY:
148
+ data[_PUBLIC_TARGET_KEY] = _target_to_alias(value)
149
+ elif key == _TYPE_KEY:
150
+ data[_PUBLIC_TYPE_KEY] = _type_to_alias(value)
151
+ else:
152
+ data[key] = _to_public_model_config(value)
153
+ return data
154
+ if isinstance(obj, str):
155
+ return _to_public_string(obj)
156
+ return copy.deepcopy(obj)
157
+
158
+
159
+ def _from_public_model_config(obj: Any) -> Any:
160
+ if isinstance(obj, list):
161
+ return [_from_public_model_config(item) for item in obj]
162
+ if isinstance(obj, dict):
163
+ data = {}
164
+ for key, value in obj.items():
165
+ if key in {_PUBLIC_TARGET_KEY, _CLASS_NAME_KEY}:
166
+ data[_TARGET_KEY] = _alias_to_runtime_target(value)
167
+ elif key == _CONFIG_NAME_KEY or (key == _PUBLIC_TYPE_KEY and _is_type_alias(value)):
168
+ data[_TYPE_KEY] = _alias_to_runtime_type(value)
169
+ else:
170
+ data[key] = _from_public_model_config(value)
171
+ return data
172
+ if isinstance(obj, str):
173
+ return _from_public_string(obj)
174
+ return copy.deepcopy(obj)
175
+
176
+
177
+ def _target_to_alias(target: Any) -> str:
178
+ if not isinstance(target, str):
179
+ raise TypeError(f"Expected target path string, got {type(target)}")
180
+ canonical = _canonicalize_module_path(target)
181
+ try:
182
+ return _TARGET_ALIASES[canonical]
183
+ except KeyError as exc:
184
+ raise ValueError(f"No public alias registered for target path: {target}") from exc
185
+
186
+
187
+ def _type_to_alias(tp: Any) -> str:
188
+ if not isinstance(tp, str):
189
+ raise TypeError(f"Expected type path string, got {type(tp)}")
190
+ canonical = _canonicalize_module_path(tp)
191
+ try:
192
+ return _TYPE_ALIASES[canonical]
193
+ except KeyError as exc:
194
+ raise ValueError(f"No public alias registered for type path: {tp}") from exc
195
+
196
+
197
+ def _alias_to_runtime_target(alias: Any) -> str:
198
+ if not isinstance(alias, str):
199
+ raise TypeError(f"Expected target alias string, got {type(alias)}")
200
+ try:
201
+ return _runtime_module_path(_ALIAS_TARGETS[alias])
202
+ except KeyError as exc:
203
+ raise ValueError(f"Unknown Cosmos target alias: {alias}") from exc
204
+
205
+
206
+ def _alias_to_runtime_type(alias: Any) -> str:
207
+ if not isinstance(alias, str):
208
+ raise TypeError(f"Expected type alias string, got {type(alias)}")
209
+ try:
210
+ return _runtime_module_path(_ALIAS_TYPES[alias])
211
+ except KeyError as exc:
212
+ raise ValueError(f"Unknown Cosmos type alias: {alias}") from exc
213
+
214
+
215
+ def _is_type_alias(value: Any) -> bool:
216
+ return isinstance(value, str) and value in _ALIAS_TYPES
217
+
218
+
219
+ def _canonicalize_module_path(path: str) -> str:
220
+ replacements = (
221
+ # vlm → reasoner rename (upstream i4). Longer/more-specific rules first
222
+ # so the reasoner subtree isn't shadowed by the general vfm rules below.
223
+ ("cosmos_framework.configs.base.defaults.reasoner.", "projects.cosmos3.vfm.configs.base.defaults.vlm."),
224
+ ("cosmos_framework.configs.base.reasoner.", "projects.cosmos3.vfm.configs.base.vlm."),
225
+ ("cosmos_framework.model.generator.reasoner.", "projects.cosmos3.vfm.models.vlm."),
226
+ ("cosmos_framework.data.generator.reasoner.", "projects.cosmos3.vfm.datasets.vlm."),
227
+ ("cosmos_framework.data.generator.augmentors.reasoner.", "projects.cosmos3.vfm.datasets.augmentors.vlm."),
228
+ ("cosmos_framework.utils.generator.reasoner.", "projects.cosmos3.vfm.utils.vlm."),
229
+ ("cosmos3._src.vfm.", "projects.cosmos3.vfm."),
230
+ ("cosmos_framework.configs.base.", "projects.cosmos3.vfm.configs.base."),
231
+ ("cosmos_framework.model.generator.tokenizers.", "projects.cosmos3.vfm.tokenizers."),
232
+ ("cosmos_framework.model.generator.diffusion.", "projects.cosmos3.vfm.diffusion."),
233
+ ("cosmos_framework.model.generator.", "projects.cosmos3.vfm.models."),
234
+ ("cosmos_framework.data.generator.processors.", "projects.cosmos3.vfm.processors."),
235
+ ("cosmos.model.vfm.tokenizers.", "projects.cosmos3.vfm.tokenizers."),
236
+ ("cosmos.model.vfm.diffusion.", "projects.cosmos3.vfm.diffusion."),
237
+ ("cosmos.model.vfm.", "projects.cosmos3.vfm.models."),
238
+ ("cosmos.configs.base.", "projects.cosmos3.vfm.configs.base."),
239
+ )
240
+ for old, new in replacements:
241
+ if path.startswith(old):
242
+ return new + path[len(old) :]
243
+ return path
244
+
245
+
246
+ def _runtime_module_path(canonical_path: str) -> str:
247
+ if _module_exists("cosmos_framework.model.generator"):
248
+ return _replace_vfm_module_prefix(canonical_path, package="cosmos_framework")
249
+ if _module_exists("cosmos.model.vfm"):
250
+ return _replace_vfm_module_prefix(canonical_path, package="cosmos")
251
+ if _module_exists("cosmos3._src.vfm"):
252
+ return canonical_path.replace("projects.cosmos3.vfm.", "cosmos3._src.vfm.", 1)
253
+ return canonical_path
254
+
255
+
256
+ def _replace_vfm_module_prefix(canonical_path: str, *, package: str) -> str:
257
+ replacements = (
258
+ # vlm → reasoner (upstream rename). MUST precede the general vfm rules
259
+ # so the specific vlm→reasoner subtree isn't shadowed by them.
260
+ ("projects.cosmos3.vfm.configs.base.defaults.vlm.", f"{package}.configs.base.defaults.reasoner."),
261
+ ("projects.cosmos3.vfm.configs.base.vlm.", f"{package}.configs.base.reasoner."),
262
+ ("projects.cosmos3.vfm.models.vlm.", f"{package}.model.generator.reasoner."),
263
+ ("projects.cosmos3.vfm.datasets.vlm.", f"{package}.data.generator.reasoner."),
264
+ ("projects.cosmos3.vfm.datasets.augmentors.vlm.", f"{package}.data.generator.augmentors.reasoner."),
265
+ ("projects.cosmos3.vfm.utils.vlm.", f"{package}.utils.generator.reasoner."),
266
+ ("projects.cosmos3.vfm.configs.base.", f"{package}.configs.base."),
267
+ ("projects.cosmos3.vfm.models.", f"{package}.model.generator."),
268
+ ("projects.cosmos3.vfm.tokenizers.", f"{package}.model.generator.tokenizers."),
269
+ ("projects.cosmos3.vfm.diffusion.", f"{package}.model.generator.diffusion."),
270
+ ("projects.cosmos3.vfm.processors.", f"{package}.data.generator.processors."),
271
+ ("projects.cosmos3.vfm.datasets.", f"{package}.data.generator."),
272
+ ("projects.cosmos3.vfm.scripts.action.", f"{package}.data.generator.action_scripts."),
273
+ ("projects.cosmos3.vfm.utils.", f"{package}.utils.generator."),
274
+ )
275
+ for old, new in replacements:
276
+ if canonical_path.startswith(old):
277
+ return new + canonical_path[len(old) :]
278
+ return canonical_path
279
+
280
+
281
+ def _to_public_string(value: str) -> str:
282
+ for prefix in ("projects/cosmos3/vfm/", "cosmos3/_src/vfm/"):
283
+ if value.startswith(prefix):
284
+ return _PUBLIC_VFM_URI_PREFIX + value[len(prefix) :]
285
+ for package in ("cosmos_framework", "cosmos"):
286
+ public_value = _public_string_from_runtime_file_prefix(value, package=package)
287
+ if public_value is not None:
288
+ return public_value
289
+ return value
290
+
291
+
292
+ def _public_string_from_runtime_file_prefix(value: str, *, package: str) -> str | None:
293
+ replacements = (
294
+ # reasoner → vlm (inverse of the upstream rename). MUST precede the
295
+ # general vfm rules so the reasoner subtree isn't shadowed by them.
296
+ # Mirrors the module-path rules in ``_canonicalize_module_path``.
297
+ (f"{package}/configs/base/defaults/reasoner/", "configs/base/defaults/vlm/"),
298
+ (f"{package}/configs/base/reasoner/", "configs/base/vlm/"),
299
+ (f"{package}/model/generator/reasoner/", "models/vlm/"),
300
+ (f"{package}/data/generator/augmentors/reasoner/", "datasets/augmentors/vlm/"),
301
+ (f"{package}/data/generator/reasoner/", "datasets/vlm/"),
302
+ (f"{package}/utils/generator/reasoner/", "utils/vlm/"),
303
+ (f"{package}/configs/base/", "configs/base/"),
304
+ (f"{package}/model/generator/tokenizers/", "tokenizers/"),
305
+ (f"{package}/model/generator/diffusion/", "diffusion/"),
306
+ (f"{package}/model/generator/", "models/"),
307
+ (f"{package}/data/generator/processors/", "processors/"),
308
+ (f"{package}/data/generator/action_scripts/", "scripts/action/"),
309
+ (f"{package}/data/generator/", "datasets/"),
310
+ (f"{package}/utils/generator/", "utils/"),
311
+ )
312
+ for old, new in replacements:
313
+ if value.startswith(old):
314
+ return _PUBLIC_VFM_URI_PREFIX + new + value[len(old) :]
315
+ return None
316
+
317
+
318
+ def _from_public_string(value: str) -> str:
319
+ if not value.startswith(_PUBLIC_VFM_URI_PREFIX):
320
+ return value
321
+ suffix = value[len(_PUBLIC_VFM_URI_PREFIX) :]
322
+ if _module_exists("cosmos_framework.model.generator"):
323
+ return _replace_vfm_file_prefix(suffix, package="cosmos_framework")
324
+ if _module_exists("cosmos.model.vfm"):
325
+ return _replace_vfm_file_prefix(suffix, package="cosmos")
326
+ if _module_exists("cosmos3._src.vfm"):
327
+ return f"cosmos3/_src/vfm/{suffix}"
328
+ return f"projects/cosmos3/vfm/{suffix}"
329
+
330
+
331
+ def _replace_vfm_file_prefix(suffix: str, *, package: str) -> str:
332
+ replacements = (
333
+ # vlm → reasoner (upstream rename). MUST precede the general vfm rules
334
+ # so the specific vlm→reasoner subtree isn't shadowed by them. Mirrors
335
+ # the module-path rules in ``_replace_vfm_module_prefix``.
336
+ ("configs/base/defaults/vlm/", f"{package}/configs/base/defaults/reasoner/"),
337
+ ("configs/base/vlm/", f"{package}/configs/base/reasoner/"),
338
+ ("models/vlm/", f"{package}/model/generator/reasoner/"),
339
+ ("datasets/augmentors/vlm/", f"{package}/data/generator/augmentors/reasoner/"),
340
+ ("datasets/vlm/", f"{package}/data/generator/reasoner/"),
341
+ ("utils/vlm/", f"{package}/utils/generator/reasoner/"),
342
+ ("configs/base/", f"{package}/configs/base/"),
343
+ ("models/", f"{package}/model/generator/"),
344
+ ("tokenizers/", f"{package}/model/generator/tokenizers/"),
345
+ ("diffusion/", f"{package}/model/generator/diffusion/"),
346
+ ("processors/", f"{package}/data/generator/processors/"),
347
+ ("datasets/", f"{package}/data/generator/"),
348
+ ("scripts/action/", f"{package}/data/generator/action_scripts/"),
349
+ ("utils/", f"{package}/utils/generator/"),
350
+ )
351
+ for old, new in replacements:
352
+ if suffix.startswith(old):
353
+ return new + suffix[len(old) :]
354
+ return f"{package}/_vfm_unmapped/{suffix}"
355
+
356
+
357
+ def _module_exists(module: str) -> bool:
358
+ try:
359
+ return importlib.util.find_spec(module) is not None
360
+ except ModuleNotFoundError:
361
+ return False
releases/bridge-native-v3/code/cosmos_framework/model/generator/mot/cosmos3_vfm_network.py ADDED
@@ -0,0 +1,1536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ import math
5
+ from typing import List, Tuple
6
+
7
+ import torch
8
+ from torch import nn
9
+ from transformers.configuration_utils import PretrainedConfig
10
+ from transformers.modeling_utils import PreTrainedModel
11
+
12
+ from cosmos_framework.data.generator.sequence_packing import ModalityData, PackedSequence
13
+ from cosmos_framework.data.generator.sequence_packing.natten import verify_natten_parameter_list
14
+ from cosmos_framework.model.zeva import (
15
+ BriefInteractionTrace,
16
+ CausalPromptConfig,
17
+ CausalPromptEncoder,
18
+ CausalPromptPolicyAdapter,
19
+ PolicyInjectionConfig,
20
+ PolicyInjectionPrior,
21
+ inject_causal_prompt,
22
+ )
23
+ from cosmos_framework.model.generator.mot.attention import SplitInfo, build_packed_sequence
24
+ from cosmos_framework.model.generator.mot.context_parallel_utils import (
25
+ get_context_parallel_last_hidden_state,
26
+ get_context_parallel_sharded_sequence,
27
+ )
28
+ from cosmos_framework.model.generator.mot.domain_aware_linear import DomainAwareLinear
29
+ from cosmos_framework.model.generator.mot.modeling_utils import TimestepEmbedder, has_noisy_tokens
30
+ from cosmos_framework.model.generator.utils.memory import MemoryState
31
+
32
+
33
+ class Cosmos3VFMNetworkConfig(PretrainedConfig):
34
+ def __init__(
35
+ self,
36
+ vision_gen=True,
37
+ action_gen=False,
38
+ sound_gen=False,
39
+ vlm_config=None,
40
+ latent_patch_size=2,
41
+ latent_downsample_factor=8,
42
+ latent_channel_size=16,
43
+ max_latent_h=32,
44
+ max_latent_w=32,
45
+ max_latent_t=32,
46
+ enable_fps_modulation=False,
47
+ base_fps=24,
48
+ vit_max_num_patch_per_side=70,
49
+ connector_act="gelu_pytorch_tanh",
50
+ interpolate_pos=False,
51
+ timestep_shift=1.0,
52
+ timestep_scale=0.001,
53
+ predict_text_tokens=False,
54
+ joint_attn_implementation="two_way",
55
+ action_dim=32,
56
+ num_embodiment_domains=32,
57
+ temporal_compression_factor_vision=4,
58
+ temporal_compression_factor_action=1,
59
+ natten_parameter_list=None,
60
+ video_temporal_causal=False,
61
+ # Sound generation parameters
62
+ sound_dim: int | None = None,
63
+ temporal_compression_factor_sound=1,
64
+ sound_latent_fps: int = 25,
65
+ enable_input_bias: bool = True,
66
+ behavior_stage2_config: dict | None = None,
67
+ proprio_condition_config: dict | None = None,
68
+ residual_expert_config: dict | None = None,
69
+ **kwargs,
70
+ ):
71
+ self.vision_gen = vision_gen
72
+ self.sound_gen = sound_gen
73
+ self.vlm_config = vlm_config
74
+ self.latent_patch_size = latent_patch_size
75
+ self.latent_downsample_factor = latent_downsample_factor
76
+ self.latent_channel_size = latent_channel_size
77
+ self.max_latent_h = max_latent_h
78
+ self.max_latent_w = max_latent_w
79
+ self.max_latent_t = max_latent_t
80
+ self.enable_fps_modulation = enable_fps_modulation
81
+ self.base_fps = base_fps
82
+ self.vit_max_num_patch_per_side = vit_max_num_patch_per_side
83
+ self.connector_act = connector_act
84
+ self.interpolate_pos = interpolate_pos
85
+ self.timestep_shift = timestep_shift
86
+ self.timestep_scale = timestep_scale
87
+ self.predict_text_tokens = predict_text_tokens
88
+ self.joint_attn_implementation = joint_attn_implementation
89
+ self.temporal_compression_factor_vision = temporal_compression_factor_vision
90
+ self.natten_parameter_list = natten_parameter_list
91
+ self.video_temporal_causal = video_temporal_causal
92
+ self.enable_input_bias = enable_input_bias
93
+ self.behavior_stage2_config = behavior_stage2_config
94
+ self.proprio_condition_config = proprio_condition_config
95
+ self.residual_expert_config = residual_expert_config
96
+
97
+ # action related parameters
98
+ self.action_gen = action_gen # whether to generate action tokens
99
+ self.action_dim = action_dim
100
+ self.num_embodiment_domains = num_embodiment_domains
101
+ self.temporal_compression_factor_action = temporal_compression_factor_action
102
+ if self.action_gen:
103
+ assert self.vision_gen, (
104
+ "Action generation requires visual generation! We do NOT support action only training!"
105
+ )
106
+
107
+ # sound related parameters
108
+ self.sound_dim = sound_dim
109
+ self.temporal_compression_factor_sound = temporal_compression_factor_sound
110
+ self.sound_latent_fps = sound_latent_fps
111
+ if self.sound_gen:
112
+ assert self.vision_gen, (
113
+ "Sound generation requires visual generation! We do NOT support sound only training!"
114
+ )
115
+
116
+ super().__init__(**kwargs)
117
+
118
+
119
+ class Cosmos3VFMNetwork(PreTrainedModel):
120
+ config_class = Cosmos3VFMNetworkConfig
121
+ base_model_prefix = "cosmos3"
122
+
123
+ def __init__(self, language_model, config: Cosmos3VFMNetworkConfig):
124
+ super().__init__(config)
125
+ self.language_model = language_model
126
+
127
+ text_config = config.vlm_config.text_config if hasattr(config.vlm_config, "text_config") else config.vlm_config
128
+ self.hidden_size = text_config.hidden_size
129
+ self.num_heads = text_config.num_attention_heads
130
+ self.num_kv_heads = text_config.num_key_value_heads
131
+ self.head_dim = text_config.head_dim
132
+ self.num_hidden_layers = text_config.num_hidden_layers
133
+ self.attention_io_layout = "sequence_sharded"
134
+ self.predict_text_tokens = config.predict_text_tokens
135
+
136
+ if config.natten_parameter_list is not None and config.joint_attn_implementation != "three_way":
137
+ raise NotImplementedError(
138
+ f"Sparsity is only supported with 'three_way' attention, but got {config.joint_attn_implementation=}, "
139
+ "and 'natten_parameter_list' was not None."
140
+ )
141
+ self.natten_parameter_list = verify_natten_parameter_list(
142
+ config.natten_parameter_list, num_layers=self.num_hidden_layers
143
+ )
144
+
145
+ if config.video_temporal_causal and config.joint_attn_implementation != "three_way":
146
+ raise ValueError(
147
+ f"video_temporal_causal=True requires joint_attn_implementation='three_way', "
148
+ f"but got {config.joint_attn_implementation!r}."
149
+ )
150
+ self.video_temporal_causal = config.video_temporal_causal
151
+ self.pad_for_cuda_graphs = False
152
+
153
+ if config.vision_gen:
154
+ self.latent_patch_size = config.latent_patch_size
155
+ self.timestep_shift = config.timestep_shift
156
+ self.timestep_scale = config.timestep_scale
157
+ self.latent_downsample = config.latent_downsample_factor * config.latent_patch_size
158
+ self.max_latent_h = config.max_latent_h
159
+ self.max_latent_w = config.max_latent_w
160
+ self.max_latent_t = config.max_latent_t
161
+ self.latent_channel = config.latent_channel_size
162
+ self.patch_latent_dim = self.latent_patch_size**2 * self.latent_channel
163
+
164
+ _input_bias = config.enable_input_bias
165
+ self.time_embedder = TimestepEmbedder(self.hidden_size, bias=_input_bias)
166
+ self.vae2llm = nn.Linear(self.patch_latent_dim, self.hidden_size, bias=_input_bias)
167
+ self.llm2vae = nn.Linear(self.hidden_size, self.patch_latent_dim)
168
+
169
+ if config.action_gen:
170
+ self.action_dim = config.action_dim
171
+ self.num_embodiment_domains = config.num_embodiment_domains
172
+ self.action2llm = DomainAwareLinear(self.action_dim, self.hidden_size, self.num_embodiment_domains)
173
+ self.llm2action = DomainAwareLinear(self.hidden_size, self.action_dim, self.num_embodiment_domains)
174
+
175
+ self.action_modality_embed = nn.Parameter(torch.zeros(self.hidden_size))
176
+ residual_cfg = config.residual_expert_config or {}
177
+ self.action_residual_head: nn.Linear | None = None
178
+ if bool(residual_cfg.get("enabled", False)):
179
+ self.action_residual_head = nn.Linear(self.hidden_size, self.action_dim, bias=True)
180
+ else:
181
+ self.action_residual_head = None
182
+
183
+ behavior_cfg = config.behavior_stage2_config
184
+ if behavior_cfg is not None and behavior_cfg.get("enabled", False):
185
+ if not config.action_gen:
186
+ raise ValueError("Zeva policy injection requires action_gen=True")
187
+ prior_cfg = PolicyInjectionConfig(
188
+ global_dim=int(behavior_cfg["global_dim"]),
189
+ phase_dim=int(behavior_cfg["phase_dim"]),
190
+ effect_dim=int(behavior_cfg.get("effect_dim", 128)),
191
+ effect_history_length=int(behavior_cfg.get("effect_history_length", 4)),
192
+ action_dim=int(behavior_cfg["action_dim"]),
193
+ horizon=int(behavior_cfg["horizon"]),
194
+ num_anchors=int(behavior_cfg["num_anchors"]),
195
+ hidden_dim=int(behavior_cfg["hidden_dim"]),
196
+ num_heads=int(behavior_cfg["num_heads"]),
197
+ )
198
+ # These registered attribute names are retained solely because they
199
+ # are part of the released DCP state-dict schema.
200
+ self.behavior_pbd: PolicyInjectionPrior | None = PolicyInjectionPrior(prior_cfg)
201
+ self.behavior_adapter: CausalPromptPolicyAdapter | None = CausalPromptPolicyAdapter(
202
+ action_dim=prior_cfg.action_dim, hidden_dim=self.hidden_size
203
+ )
204
+ # Zeva's second conditioning path: the episode-level task context
205
+ # is a learned understanding-prefix token visible to every
206
+ # action query through Cosmos two-way attention.
207
+ self.behavior_global_projector: nn.Linear | None = nn.Linear(
208
+ prior_cfg.global_dim, self.hidden_size, bias=True
209
+ )
210
+ pim_enabled = bool(behavior_cfg.get("pim_memory_enabled", False))
211
+ pim_dim = int(behavior_cfg.get("pim_context_dim", 256))
212
+ self.behavior_pim_encoder: CausalPromptEncoder | None = (
213
+ CausalPromptEncoder(
214
+ CausalPromptConfig(
215
+ global_dim=prior_cfg.global_dim,
216
+ phase_dim=prior_cfg.phase_dim,
217
+ effect_dim=prior_cfg.effect_dim,
218
+ brief_length=prior_cfg.effect_history_length,
219
+ persistent_length=int(behavior_cfg.get("pim_persistent_length", 4)),
220
+ hidden_dim=pim_dim,
221
+ num_heads=int(behavior_cfg.get("num_heads", 4)),
222
+ )
223
+ )
224
+ if pim_enabled
225
+ else None
226
+ )
227
+ self.behavior_pim_projector: nn.Linear | None = (
228
+ nn.Linear(pim_dim, self.hidden_size, bias=True) if pim_enabled else None
229
+ )
230
+ self.behavior_pim_gate: nn.Parameter | None = (
231
+ nn.Parameter(torch.empty(1)) if pim_enabled else None
232
+ )
233
+ self.behavior_pim_gate_init = float(behavior_cfg.get("pim_gate_init", 0.0))
234
+ self.behavior_pim_force_bypass = bool(behavior_cfg.get("pim_force_bypass", False))
235
+ online_enabled = bool(behavior_cfg.get("online_memory_enabled", False))
236
+ online_dim = int(behavior_cfg.get("online_context_dim", 256))
237
+ self.behavior_online_projector: nn.Linear | None = (
238
+ nn.Linear(online_dim, self.hidden_size, bias=True) if online_enabled else None
239
+ )
240
+ self.behavior_prior_dropout_rate = float(behavior_cfg["prior_dropout_rate"])
241
+ self.behavior_prior_inference_guidance_scale = float(
242
+ behavior_cfg.get("prior_inference_guidance_scale", 0.5)
243
+ )
244
+ self.behavior_prior_leading_condition_steps = int(
245
+ behavior_cfg.get("leading_condition_steps", 1)
246
+ )
247
+ else:
248
+ self.behavior_pbd = None
249
+ self.behavior_adapter = None
250
+ self.behavior_global_projector = None
251
+ self.behavior_pim_encoder = None
252
+ self.behavior_pim_projector = None
253
+ self.behavior_pim_gate = None
254
+ self.behavior_pim_gate_init = 0.0
255
+ self.behavior_pim_force_bypass = False
256
+ self.behavior_online_projector = None
257
+ self.behavior_prior_dropout_rate = 0.0
258
+ self.behavior_prior_inference_guidance_scale = 1.0
259
+ self.behavior_prior_leading_condition_steps = 1
260
+
261
+ proprio_cfg = config.proprio_condition_config
262
+ if proprio_cfg is not None and proprio_cfg.get("enabled", False):
263
+ if not config.action_gen:
264
+ raise ValueError("Proprio conditioning requires action_gen=True")
265
+ self.proprio_input_dim = int(proprio_cfg["input_dim"])
266
+ self.proprio_prefix_tokens = int(proprio_cfg.get("prefix_tokens", 1))
267
+ if self.proprio_input_dim < 1 or self.proprio_prefix_tokens < 1:
268
+ raise ValueError("Proprio input_dim and prefix_tokens must be positive")
269
+ self.proprio_projector: nn.Linear | None = nn.Linear(
270
+ self.proprio_input_dim,
271
+ self.proprio_prefix_tokens * self.hidden_size,
272
+ bias=True,
273
+ )
274
+ else:
275
+ self.proprio_input_dim = 0
276
+ self.proprio_prefix_tokens = 0
277
+ self.proprio_projector = None
278
+
279
+ if config.sound_gen:
280
+ self.sound_dim = config.sound_dim
281
+ self.sound2llm = nn.Linear(config.sound_dim, self.hidden_size, bias=config.enable_input_bias)
282
+ self.llm2sound = nn.Linear(self.hidden_size, config.sound_dim)
283
+ self.sound_modality_embed = nn.Parameter(torch.zeros(self.hidden_size))
284
+
285
+ self.config = config
286
+ self.parallel_dims = None
287
+
288
+ @property
289
+ def causal_prompt_encoder(self) -> CausalPromptEncoder | None:
290
+ """Paper-facing alias; storage keeps the legacy checkpoint key."""
291
+ return self.behavior_pim_encoder
292
+
293
+ @property
294
+ def causal_prompt_projector(self) -> nn.Linear | None:
295
+ """Project the fused causal prompt into the frozen policy prefix."""
296
+ return self.behavior_pim_projector
297
+
298
+ @property
299
+ def causal_prompt_gate(self) -> nn.Parameter | None:
300
+ """Zero-initialized policy-injection gate for the causal prompt."""
301
+ return self.behavior_pim_gate
302
+
303
+ def init_weights(self, buffer_device: torch.device | None):
304
+ if self.config.vision_gen or self.config.action_gen or self.config.sound_gen:
305
+ self.time_embedder._init_weights(buffer_device=buffer_device)
306
+
307
+ if self.config.vision_gen:
308
+ std = 1.0 / math.sqrt(self.patch_latent_dim)
309
+ torch.nn.init.trunc_normal_(self.vae2llm.weight, std=std, a=-3 * std, b=3 * std)
310
+ if self.config.enable_input_bias:
311
+ torch.nn.init.zeros_(self.vae2llm.bias)
312
+
313
+ std = 1.0 / math.sqrt(self.hidden_size)
314
+ torch.nn.init.trunc_normal_(self.llm2vae.weight, std=std, a=-3 * std, b=3 * std)
315
+ torch.nn.init.zeros_(self.llm2vae.bias)
316
+
317
+ if self.config.action_gen:
318
+ # DomainAwareLinear uses embeddings for weights, so we initialize them differently
319
+ # action2llm: input_size=action_dim, output_size=hidden_size
320
+ std = 1.0 / math.sqrt(self.action_dim)
321
+ torch.nn.init.trunc_normal_(self.action2llm.fc.weight, std=std, a=-3 * std, b=3 * std)
322
+ torch.nn.init.zeros_(self.action2llm.bias.weight)
323
+
324
+ # llm2action: input_size=hidden_size, output_size=action_dim
325
+ std = 1.0 / math.sqrt(self.hidden_size)
326
+ torch.nn.init.trunc_normal_(self.llm2action.fc.weight, std=std, a=-3 * std, b=3 * std)
327
+ torch.nn.init.zeros_(self.llm2action.bias.weight)
328
+
329
+ std = 1.0 / math.sqrt(self.hidden_size)
330
+ torch.nn.init.trunc_normal_(self.action_modality_embed, std=std, a=-3 * std, b=3 * std)
331
+ if self.action_residual_head is not None:
332
+ # Zero initialization makes residual enabling an exact base
333
+ # bypass before the first optimizer update.
334
+ torch.nn.init.zeros_(self.action_residual_head.weight)
335
+ torch.nn.init.zeros_(self.action_residual_head.bias)
336
+
337
+ if (
338
+ self.behavior_pbd is not None
339
+ and self.behavior_adapter is not None
340
+ and self.behavior_global_projector is not None
341
+ ):
342
+ self.behavior_pbd.init_weights()
343
+ self.behavior_adapter.init_weights()
344
+ nn.init.xavier_uniform_(self.behavior_global_projector.weight)
345
+ nn.init.zeros_(self.behavior_global_projector.bias)
346
+ if (
347
+ self.behavior_pim_encoder is not None
348
+ and self.behavior_pim_projector is not None
349
+ and self.behavior_pim_gate is not None
350
+ ):
351
+ self.behavior_pim_encoder.reset_parameters()
352
+ nn.init.xavier_uniform_(self.behavior_pim_projector.weight)
353
+ nn.init.zeros_(self.behavior_pim_projector.bias)
354
+ nn.init.constant_(self.behavior_pim_gate, self.behavior_pim_gate_init)
355
+ if self.behavior_online_projector is not None:
356
+ # Zero initialization makes enabling memory a strict no-op
357
+ # until the support-memory adaptation stage trains this path.
358
+ nn.init.zeros_(self.behavior_online_projector.weight)
359
+ nn.init.zeros_(self.behavior_online_projector.bias)
360
+ if self.proprio_projector is not None:
361
+ nn.init.xavier_uniform_(self.proprio_projector.weight)
362
+ nn.init.zeros_(self.proprio_projector.bias)
363
+
364
+ if self.config.sound_gen:
365
+ # sound2llm: input_size=sound_dim, output_size=hidden_size
366
+ std = 1.0 / math.sqrt(self.sound_dim)
367
+ torch.nn.init.trunc_normal_(self.sound2llm.weight, std=std, a=-3 * std, b=3 * std)
368
+ if self.config.enable_input_bias:
369
+ torch.nn.init.zeros_(self.sound2llm.bias)
370
+
371
+ # llm2sound: input_size=hidden_size, output_size=sound_dim
372
+ std = 1.0 / math.sqrt(self.hidden_size)
373
+ torch.nn.init.trunc_normal_(self.llm2sound.weight, std=std, a=-3 * std, b=3 * std)
374
+ torch.nn.init.zeros_(self.llm2sound.bias)
375
+
376
+ std = 1.0 / math.sqrt(self.hidden_size)
377
+ torch.nn.init.trunc_normal_(self.sound_modality_embed, std=std, a=-3 * std, b=3 * std)
378
+
379
+ self.language_model.init_weights(buffer_device=buffer_device)
380
+
381
+ def generate_reasoner_text(
382
+ self,
383
+ input_ids: torch.Tensor,
384
+ max_new_tokens: int,
385
+ *,
386
+ pixel_values: torch.Tensor | None = None,
387
+ image_grid_thw: torch.Tensor | None = None,
388
+ pixel_values_videos: torch.Tensor | None = None,
389
+ video_grid_thw: torch.Tensor | None = None,
390
+ attention_mask: torch.Tensor | None = None,
391
+ eos_token_id: int | list[int] | None = None,
392
+ pad_token_id: int | None = None,
393
+ do_sample: bool = False,
394
+ temperature: float = 1.0,
395
+ top_k: int | None = None,
396
+ top_p: float | None = None,
397
+ repetition_penalty: float = 1.0,
398
+ presence_penalty: float = 0.0,
399
+ seed: int | None = None,
400
+ return_only_new_tokens: bool = False,
401
+ ) -> torch.Tensor:
402
+ """Autoregressively generate text tokens using only the reasoner tower.
403
+
404
+ Thin pass-through to ``self.language_model.generate_reasoner_text``
405
+ (see ``unified_mot._impl_generate_reasoner_text`` for full argument
406
+ documentation). Handles both text-only and image-conditioned (I2V)
407
+ prompts through this single entry point: pass
408
+ ``pixel_values`` + ``image_grid_thw`` (and optionally
409
+ ``attention_mask``) for image-conditioned prefill via the Qwen3-VL
410
+ visual encoder, or omit them for text-only prefill. Video
411
+ conditioning is also supported via ``pixel_values_videos`` +
412
+ ``video_grid_thw``; the image and video pairs are mutually exclusive.
413
+ Uses the und-pathway weights (those WITHOUT the ``_moe_gen`` suffix)
414
+ plus ``embed_tokens`` / ``norm`` / ``lm_head``; the generation pathway
415
+ and all VFM-level multimodal embedders / heads (``vae2llm``,
416
+ ``llm2vae``, ``sound2llm``, etc.) are bypassed.
417
+
418
+ ``repetition_penalty`` / ``presence_penalty`` are pass-through
419
+ sampling controls applied inside
420
+ :func:`unified_mot._impl_generate_reasoner_text` as logit
421
+ transformations *before* the ``do_sample`` argmax / multinomial
422
+ branch (so they shift the greedy argmax too). Identity defaults
423
+ (``1.0`` / ``0.0``) keep the un-penalized fast path
424
+ bit-identical.
425
+
426
+ ``seed`` is a pass-through sampling-RNG knob: when provided,
427
+ :func:`unified_mot._impl_generate_reasoner_text` allocates a
428
+ device-local ``torch.Generator``, seeds it once with
429
+ ``manual_seed(seed)``, and threads it into every
430
+ ``torch.multinomial`` draw — making the decoded sequence a
431
+ deterministic function of the seed, the prompt, and the
432
+ penalty masks. ``None`` (default) consumes the device's
433
+ default RNG and is bit-identical to the pre-seed call surface.
434
+ Has no effect under greedy decoding (the argmax branch never
435
+ reads the generator).
436
+ """
437
+
438
+ return self.language_model.generate_reasoner_text(
439
+ input_ids=input_ids,
440
+ max_new_tokens=max_new_tokens,
441
+ pixel_values=pixel_values,
442
+ image_grid_thw=image_grid_thw,
443
+ pixel_values_videos=pixel_values_videos,
444
+ video_grid_thw=video_grid_thw,
445
+ attention_mask=attention_mask,
446
+ eos_token_id=eos_token_id,
447
+ pad_token_id=pad_token_id,
448
+ do_sample=do_sample,
449
+ temperature=temperature,
450
+ top_k=top_k,
451
+ top_p=top_p,
452
+ repetition_penalty=repetition_penalty,
453
+ presence_penalty=presence_penalty,
454
+ seed=seed,
455
+ return_only_new_tokens=return_only_new_tokens,
456
+ )
457
+
458
+ def patchify_and_pack_latents(
459
+ self, tokens_vision: torch.Tensor, token_shapes_vision: List[Tuple[int, int, int]]
460
+ ) -> tuple[torch.Tensor, List[Tuple[int, int, int]]]:
461
+ p = self.latent_patch_size
462
+ # Patchify and pack the latents
463
+ packed_latent = []
464
+ original_latent_shapes = [] # Store original shapes for unpadding later
465
+
466
+ # C, T, H, W
467
+ for latent, (t, h, w) in zip(tokens_vision, token_shapes_vision):
468
+ latent = latent.squeeze(0) # [C,T,H,W]
469
+
470
+ # Get original latent dimensions
471
+ _, t_actual, h_actual, w_actual = latent.shape
472
+ original_latent_shapes.append((t_actual, h_actual, w_actual))
473
+
474
+ # Compute padded dimensions (must be divisible by p)
475
+ h_padded = ((h_actual + p - 1) // p) * p
476
+ w_padded = ((w_actual + p - 1) // p) * p
477
+
478
+ # Zero-pad if dimensions are not divisible by p
479
+ if h_padded != h_actual or w_padded != w_actual:
480
+ padded = torch.zeros(
481
+ (self.latent_channel, t_actual, h_padded, w_padded),
482
+ device=latent.device,
483
+ dtype=latent.dtype,
484
+ ) # [C,T,H_padded,W_padded]
485
+ padded[:, :, :h_actual, :w_actual] = latent
486
+ latent = padded # [C,T,H_padded,W_padded]
487
+
488
+ # Compute number of patches after padding
489
+ h_patches = h_padded // p
490
+ w_patches = w_padded // p
491
+
492
+ # Patchify
493
+ latent = latent.reshape(
494
+ self.latent_channel, t_actual, h_patches, p, w_patches, p
495
+ ) # [C,T,h_patches,p,w_patches,p]
496
+ latent = torch.einsum("cthpwq->thwpqc", latent).reshape(
497
+ -1, p * p * self.latent_channel
498
+ ) # [T*h_patches*w_patches,patch_latent_dim]
499
+ packed_latent.append(latent)
500
+
501
+ # We assumed latents we get to the network is already noised
502
+ packed_latent = torch.cat(packed_latent, dim=0) # [total_vision_patches,patch_latent_dim]
503
+ return packed_latent, original_latent_shapes
504
+
505
+ def unpatchify_and_unpack_latents(
506
+ self,
507
+ packed_mse_preds: torch.Tensor,
508
+ token_shapes_vision: List[Tuple[int, int, int]],
509
+ noisy_frame_indexes_vision: list[torch.Tensor],
510
+ original_latent_shapes: List[Tuple[int, int, int]] | None = None,
511
+ ) -> list[torch.Tensor]:
512
+ p = self.latent_patch_size
513
+ unpatchified_latents = []
514
+
515
+ # Split packed_mse_preds back into individual latents based on token_shapes_vision
516
+ start_idx = 0
517
+ for i, (t_c, h_c, w_c) in enumerate(token_shapes_vision):
518
+ # Get original shape for unpadding (if provided)
519
+ if original_latent_shapes is not None:
520
+ t_orig, h_orig, w_orig = original_latent_shapes[i]
521
+ # Compute padded dimensions used during patchify
522
+ h_padded = ((h_orig + p - 1) // p) * p
523
+ w_padded = ((w_orig + p - 1) // p) * p
524
+ h_patches = h_padded // p
525
+ w_patches = w_padded // p
526
+ else:
527
+ # Fallback: use token shapes directly (assumes no padding was needed)
528
+ t_orig, h_orig, w_orig = t_c, h_c * p, w_c * p
529
+ h_patches, w_patches = h_c, w_c
530
+
531
+ # noisy_frame_indexes_vision is a list of tensors, each with shape (T,),
532
+ # where the values are the noisy frame indices.
533
+ noisy_frame_indexes = noisy_frame_indexes_vision[i]
534
+ t_n = len(noisy_frame_indexes)
535
+
536
+ # Initialize with the original shape (after unpadding), zeros for clean frames
537
+ output_tensor = torch.zeros(
538
+ (self.latent_channel, t_c, h_orig, w_orig),
539
+ device=packed_mse_preds.device,
540
+ dtype=packed_mse_preds.dtype,
541
+ ) # [C,T,H_orig,W_orig]
542
+ num_patches = t_n * h_patches * w_patches
543
+ if num_patches > 0:
544
+ end_idx = start_idx + num_patches
545
+ # Extract patches for this latent
546
+ latent_patches = packed_mse_preds[start_idx:end_idx] # [num_patches,patch_latent_dim]
547
+ # Reshape back to [t_n, h_patches, w_patches, p, p, channels]
548
+ latent_patches = latent_patches.reshape(
549
+ t_n, h_patches, w_patches, p, p, self.latent_channel
550
+ ) # [T_n,h_patches,w_patches,p,p,C]
551
+ # Invert the einsum operation: "thwpqc->cthpwq"
552
+ latent = torch.einsum("thwpqc->cthpwq", latent_patches) # [C,T_n,h_patches,p,w_patches,p]
553
+ # Reshape back to [channels, t_n, h_padded, w_padded]
554
+ latent = latent.reshape(
555
+ self.latent_channel, t_n, h_patches * p, w_patches * p
556
+ ) # [C,T_n,H_padded,W_padded]
557
+
558
+ # Crop to original dimensions (unpad the zeros)
559
+ latent = latent[:, :, :h_orig, :w_orig] # [C,T_n,H_orig,W_orig]
560
+
561
+ # Fill only the noisy frame positions using the actual mask indices
562
+ output_tensor[:, noisy_frame_indexes] = latent
563
+
564
+ start_idx = end_idx
565
+
566
+ unpatchified_latents.append(output_tensor.unsqueeze(0)) # [1,C,T,H,W]
567
+
568
+ # Return list of unpatchified latents (supports variable shapes)
569
+ return unpatchified_latents
570
+
571
+ def pack_action(
572
+ self,
573
+ tokens_action: list[torch.Tensor],
574
+ token_shapes_action: list[tuple[int, ...]],
575
+ domain_id_action: list[torch.Tensor],
576
+ ) -> tuple[torch.Tensor, torch.Tensor]:
577
+ """Pack variable-length action tokens into a 1D sequence for transformer input.
578
+
579
+ Args:
580
+ tokens_action: List of action tensors, each [T_i, action_dim] (T_i may vary).
581
+ token_shapes_action: List of (T_i,) tuples per sample.
582
+ domain_id_action: List of domain ID tensors, each of shape [1].
583
+
584
+ Returns:
585
+ Tuple of (packed_tokens, per_token_domain_id):
586
+ packed_tokens: [total_action_tokens, action_dim]
587
+ per_token_domain_id: [total_action_tokens]
588
+ """
589
+ packed: list[torch.Tensor] = []
590
+ domain_ids: list[torch.Tensor] = []
591
+ for tokens, shape, d_id in zip(tokens_action, token_shapes_action, domain_id_action):
592
+ T = shape[0]
593
+ packed.append(tokens[:T])
594
+ domain_ids.append(d_id.expand(T))
595
+ return torch.cat(packed, dim=0), torch.cat(domain_ids, dim=0)
596
+
597
+ def unpack_action(
598
+ self,
599
+ packed_action_preds: torch.Tensor,
600
+ token_shapes_action: list[tuple[int, ...]],
601
+ noisy_frame_indexes_action: list[torch.Tensor],
602
+ ) -> list[torch.Tensor]:
603
+ """Unpack action predictions back into per-sample action tensors.
604
+
605
+ Args:
606
+ packed_action_preds: Packed action predictions of shape (total_noisy_tokens, action_dim)
607
+ token_shapes_action: Per-sample token shapes, each (T_i,) tuple.
608
+ noisy_frame_indexes_action: List of tensors, each with shape (Tn_i,), where the values
609
+ are the noisy frame indices for sample i.
610
+
611
+ Returns:
612
+ List of per-sample tensors, each of shape (T_i, action_dim), with predictions
613
+ placed at noisy positions. Clean positions are left as zeros.
614
+ """
615
+ unpacked: list[torch.Tensor] = []
616
+ start_idx = 0
617
+ for shape, noisy_frame_indexes in zip(token_shapes_action, noisy_frame_indexes_action):
618
+ T = shape[0]
619
+ output = torch.zeros(
620
+ (T, self.action_dim),
621
+ device=packed_action_preds.device,
622
+ dtype=packed_action_preds.dtype,
623
+ )
624
+ t_n = len(noisy_frame_indexes)
625
+ if t_n > 0:
626
+ end_idx = start_idx + t_n
627
+ output[noisy_frame_indexes] = packed_action_preds[start_idx:end_idx]
628
+ start_idx = end_idx
629
+ unpacked.append(output)
630
+ return unpacked
631
+
632
+ def pack_sound_latents(
633
+ self,
634
+ tokens_sound: list[torch.Tensor],
635
+ token_shapes_sound: list[tuple[int, int, int]],
636
+ ) -> torch.Tensor:
637
+ """Pack sound latents into a 1D sequence for transformer input.
638
+
639
+ Args:
640
+ tokens_sound: List of sound latent tensors, each [C, T]
641
+ token_shapes_sound: List of (T, 1, 1) tuples per sample
642
+
643
+ Returns:
644
+ Packed tensor of shape [total_sound_tokens, C]
645
+ """
646
+ packed = []
647
+ for sound, shape in zip(tokens_sound, token_shapes_sound):
648
+ T = shape[0]
649
+ # sound: [C, T] → take first T frames → [C, T]
650
+ # Then permute to [T, C] for packing
651
+ sound_tokens = sound[:, :T].permute(1, 0) # [T,C]
652
+ packed.append(sound_tokens)
653
+ return torch.cat(packed, dim=0) # [total_sound_tokens,C]
654
+
655
+ def unpack_sound_latents(
656
+ self,
657
+ packed_sound_preds: torch.Tensor,
658
+ token_shapes_sound: list[tuple[int, int, int]],
659
+ noisy_frame_indexes_sound: list[torch.Tensor],
660
+ ) -> list[torch.Tensor]:
661
+ """Unpack sound predictions back into per-sample sound latents.
662
+
663
+ Args:
664
+ packed_sound_preds: Packed sound predictions of shape (total_noisy_tokens, sound_dim)
665
+ token_shapes_sound: List of (T, 1, 1) tuples per sample
666
+ noisy_frame_indexes_action: List of tensors, each with shape (T_i,), where the values
667
+ are the noisy frame indices. T_i <= max_T.
668
+
669
+ Returns:
670
+ List of per-sample tensors, each [C, T], with predictions placed at noisy positions.
671
+ Clean positions are left as zeros.
672
+ """
673
+ unpacked = []
674
+ start_idx = 0
675
+ for shape, noisy_frame_indexes in zip(token_shapes_sound, noisy_frame_indexes_sound):
676
+ T = shape[0]
677
+ # Initialize output with zeros for clean positions
678
+ output = torch.zeros(
679
+ (self.sound_dim, T),
680
+ device=packed_sound_preds.device,
681
+ dtype=packed_sound_preds.dtype,
682
+ )
683
+
684
+ t_n = len(noisy_frame_indexes)
685
+
686
+ if t_n > 0:
687
+ end_idx = start_idx + t_n
688
+ # packed_sound_preds: [total_noisy_tokens, C] → transpose and fill at noisy positions
689
+ output[:, noisy_frame_indexes] = packed_sound_preds[
690
+ start_idx:end_idx
691
+ ].T # packed_sound_preds[...]: [T_n,C] → .T: [C,T_n]
692
+ start_idx = end_idx
693
+
694
+ unpacked.append(output)
695
+ return unpacked
696
+
697
+ def _encode_text(
698
+ self,
699
+ packed_seq: PackedSequence,
700
+ ) -> tuple[torch.Tensor, torch.dtype]:
701
+ """Embed text tokens and initialize packed_sequence.
702
+
703
+ Args:
704
+ packed_seq: PackedSequence containing text_ids and text_indexes.
705
+
706
+ Returns:
707
+ tuple of (packed_sequence, target_dtype) where packed_sequence has text embeddings filled in.
708
+ """
709
+ packed_text_embedding = self.language_model.model.embed_tokens(packed_seq.text_ids) # [N_text,hidden_size]
710
+ packed_sequence = packed_text_embedding.new_zeros(
711
+ size=(packed_seq.sequence_length, self.hidden_size)
712
+ ) # [N_total,hidden_size]
713
+ packed_sequence[packed_seq.text_indexes] = (
714
+ packed_text_embedding # [N_text,hidden_size] scattered into [N_total,hidden_size]
715
+ )
716
+ return packed_sequence, packed_text_embedding.dtype
717
+
718
+ def _encode_behavior_prefix(
719
+ self,
720
+ packed_seq: PackedSequence,
721
+ packed_sequence: torch.Tensor,
722
+ target_dtype: torch.dtype,
723
+ ) -> None:
724
+ """Fill the learned global-behavior causal slots reserved by the packer."""
725
+ if self.behavior_global_projector is None:
726
+ if packed_seq.behavior_indexes.numel():
727
+ raise ValueError("Packed behavior prefix slots require behavior_stage2.enabled=True")
728
+ return
729
+ if not packed_seq.behavior_indexes.numel():
730
+ # Clean readout / vision-only calls intentionally contain no action
731
+ # sample and therefore reserve no behavior prefix.
732
+ return
733
+ if not hasattr(packed_seq, "behavior_global"):
734
+ raise ValueError("Stage-2 behavior prefix requires behavior_global on PackedSequence")
735
+ global_features = packed_seq.behavior_global
736
+ if global_features.ndim != 2 or global_features.shape[0] != packed_seq.behavior_indexes.numel():
737
+ raise ValueError(
738
+ "behavior_global must contain one vector per reserved behavior prefix, "
739
+ f"got {tuple(global_features.shape)} for {packed_seq.behavior_indexes.numel()} slots"
740
+ )
741
+ prefix = self.behavior_global_projector(global_features).to(target_dtype)
742
+ if self.behavior_pim_encoder is not None and not self.behavior_pim_force_bypass:
743
+ required = (
744
+ "behavior_phase",
745
+ "behavior_effect",
746
+ "behavior_effect_valid",
747
+ "behavior_pim_phase",
748
+ "behavior_pim_effect",
749
+ "behavior_pim_valid",
750
+ )
751
+ if any(not hasattr(packed_seq, name) for name in required):
752
+ raise ValueError("PIM-enabled behavior prefix requires phase, BIT, and PIM tensors")
753
+ assert self.behavior_pim_projector is not None
754
+ assert self.behavior_pim_gate is not None
755
+ brief_interaction_trace = BriefInteractionTrace(
756
+ effects=packed_seq.behavior_effect,
757
+ valid=packed_seq.behavior_effect_valid,
758
+ )
759
+ causal_prompt_context = self.behavior_pim_encoder(
760
+ global_features,
761
+ packed_seq.behavior_phase,
762
+ brief_interaction_trace.effects,
763
+ brief_interaction_trace.valid,
764
+ packed_seq.behavior_pim_phase,
765
+ packed_seq.behavior_pim_effect,
766
+ packed_seq.behavior_pim_valid,
767
+ )
768
+ # Do not synthesize a residual from BOS slots when no persistent
769
+ # evidence exists. The scalar tanh gate is exactly zero at
770
+ # initialization, preserving the released prefix bit-for-bit while
771
+ # letting the scalar gate learn first; encoder/projector gradients
772
+ # become active once that gate leaves zero.
773
+ causal_prompt = self.behavior_pim_projector(causal_prompt_context)
774
+ prefix = inject_causal_prompt(
775
+ prefix, causal_prompt, self.behavior_pim_gate, packed_seq.behavior_pim_valid
776
+ )
777
+ # Keep the legacy field for existing diagnostics and expose the
778
+ # paper term for new integrations.
779
+ packed_seq.behavior_pim_context = causal_prompt_context
780
+ packed_seq.causal_prompt = causal_prompt_context
781
+ packed_sequence[packed_seq.behavior_indexes] = prefix
782
+
783
+ def _encode_online_memory_prefix(
784
+ self,
785
+ packed_seq: PackedSequence,
786
+ packed_sequence: torch.Tensor,
787
+ target_dtype: torch.dtype,
788
+ ) -> None:
789
+ """Write one causal prefix from same-task completed transitions."""
790
+ indexes = packed_seq.online_memory_indexes
791
+ if not indexes.numel():
792
+ return
793
+ if self.behavior_online_projector is None:
794
+ raise ValueError("Online-memory prefix slots require behavior_stage2.online_memory_enabled=True")
795
+ if not hasattr(packed_seq, "behavior_online_context"):
796
+ raise ValueError("Online-memory prefix requires behavior_online_context on PackedSequence")
797
+ context = packed_seq.behavior_online_context
798
+ if context.ndim != 2 or context.shape[0] != indexes.numel():
799
+ raise ValueError(
800
+ "behavior_online_context must contain one vector per online prefix slot, "
801
+ f"got {tuple(context.shape)} for {indexes.numel()} slots"
802
+ )
803
+ packed_sequence[indexes] = self.behavior_online_projector(context).to(target_dtype)
804
+
805
+ def _encode_proprio_prefix(
806
+ self,
807
+ packed_seq: PackedSequence,
808
+ packed_sequence: torch.Tensor,
809
+ target_dtype: torch.dtype,
810
+ ) -> None:
811
+ """Project the robot state into reserved causal prefix slots."""
812
+ if self.proprio_projector is None:
813
+ if packed_seq.proprio_indexes.numel():
814
+ raise ValueError("Packed proprio prefix slots require proprio_condition.enabled=True")
815
+ return
816
+ if not packed_seq.proprio_indexes.numel():
817
+ return
818
+ if not hasattr(packed_seq, "proprio_condition"):
819
+ raise ValueError("Proprio prefix requires proprio_condition on PackedSequence")
820
+ proprio = packed_seq.proprio_condition
821
+ if proprio.ndim != 2 or proprio.shape[-1] != self.proprio_input_dim:
822
+ raise ValueError(
823
+ f"Expected proprio_condition [B,{self.proprio_input_dim}], got {tuple(proprio.shape)}"
824
+ )
825
+ prefix = self.proprio_projector(proprio).reshape(-1, self.hidden_size).to(target_dtype)
826
+ if prefix.shape[0] != packed_seq.proprio_indexes.numel():
827
+ raise ValueError("Proprio batch/prefix count does not match reserved slots")
828
+ packed_sequence[packed_seq.proprio_indexes] = prefix
829
+
830
+ def _embed_packed_timesteps(self, timesteps: torch.Tensor, packed_seq: PackedSequence) -> torch.Tensor:
831
+ """Embed noised-token timesteps, reusing work when packing proves they share one scalar."""
832
+ if packed_seq.uses_single_timestep and timesteps.numel() > 1:
833
+ timestep = timesteps[:1] # [1]
834
+ with torch.autocast("cuda", enabled=True, dtype=torch.float32):
835
+ timestep_embed = self.time_embedder(timestep) # [1,hidden_size]
836
+ # Materialize: expand() aliases storage; in-place ops on a float32 no-op .to() would corrupt all rows.
837
+ return timestep_embed.expand(timesteps.shape[0], -1).contiguous() # [N_noisy_frames,hidden_size]
838
+
839
+ # Timesteps are computed in FP32 for numerical stability.
840
+ with torch.autocast("cuda", enabled=True, dtype=torch.float32):
841
+ return self.time_embedder(timesteps) # [N_noisy_frames,hidden_size]
842
+
843
+ def _encode_vision(
844
+ self,
845
+ packed_seq: PackedSequence,
846
+ packed_sequence: torch.Tensor,
847
+ target_dtype: torch.dtype,
848
+ ) -> List[Tuple[int, int, int]] | None:
849
+ """Project vision tokens and fill into packed_sequence.
850
+
851
+ Args:
852
+ packed_seq: PackedSequence containing vision tokens and metadata.
853
+ packed_sequence: The packed sequence tensor to fill vision embeddings into (modified in-place).
854
+ target_dtype: Target dtype for embeddings (typically from text embedding).
855
+
856
+ Returns:
857
+ Original latent shapes before padding (for unpadding during decode), or None if no vision tokens.
858
+ """
859
+ if packed_seq.vision is None or packed_seq.vision.tokens is None:
860
+ # No vision tokens in this batch
861
+ return None
862
+
863
+ vision = packed_seq.vision
864
+ assert vision.tokens is not None # Type narrowing (checked above but reassignment loses it)
865
+ assert vision.token_shapes is not None
866
+ assert isinstance(vision.sequence_indexes, torch.Tensor)
867
+ assert isinstance(vision.timesteps, torch.Tensor)
868
+ torch._assert(
869
+ vision.timesteps.dtype in (torch.long, torch.float32),
870
+ f"Timestep must be long/float32, got {vision.timesteps.dtype}",
871
+ )
872
+
873
+ assert isinstance(vision.mse_loss_indexes, torch.Tensor)
874
+
875
+ packed_tokens_vision, original_latent_shapes = self.patchify_and_pack_latents(
876
+ vision.tokens, vision.token_shapes
877
+ ) # packed_tokens_vision: [total_vision_patches,patch_latent_dim]
878
+ packed_tokens_vision = self.vae2llm(packed_tokens_vision.to(target_dtype)) # [total_vision_patches,hidden_size]
879
+
880
+ has_noisy_vision = vision.mse_loss_indexes.numel() > 0
881
+
882
+ if has_noisy_vision:
883
+ timesteps_vision = vision.timesteps.to(dtype=torch.float32) * self.timestep_scale # [N_noisy_frames_vision]
884
+
885
+ packed_timestep_embeds_vision = self._embed_packed_timesteps(
886
+ timesteps_vision, packed_seq
887
+ ) # [N_noisy_frames_vision,hidden_size]
888
+ packed_timestep_embeds_vision = packed_timestep_embeds_vision.to(
889
+ target_dtype
890
+ ) # [N_noisy_frames_vision,hidden_size]
891
+
892
+ packed_tokens_vision = _apply_timestep_embeds_to_noisy_tokens(
893
+ packed_tokens=packed_tokens_vision,
894
+ packed_timestep_embeds=packed_timestep_embeds_vision,
895
+ noisy_frame_indexes=vision.noisy_frame_indexes,
896
+ token_shapes=vision.token_shapes,
897
+ ) # [total_vision_patches,hidden_size]
898
+
899
+ packed_sequence[vision.sequence_indexes] = (
900
+ packed_tokens_vision # [total_vision_patches,hidden_size] scattered into [N_total,hidden_size]
901
+ )
902
+ return original_latent_shapes
903
+
904
+ def _decode_vision(
905
+ self,
906
+ packed_seq: PackedSequence,
907
+ last_hidden_state: torch.Tensor,
908
+ output_dict: dict,
909
+ original_latent_shapes: List[Tuple[int, int, int]] | None = None,
910
+ ) -> None:
911
+ """Decode vision tokens from hidden states and update output_dict.
912
+
913
+ Args:
914
+ packed_seq: PackedSequence containing mse_loss_indexes_vision and token_shapes_vision.
915
+ last_hidden_state: Hidden states from the transformer.
916
+ output_dict: Output dictionary to update with mse_preds (modified in-place).
917
+ original_latent_shapes: Original latent shapes before padding (for unpadding).
918
+ """
919
+ vision = packed_seq.vision
920
+ # Check if no vision or no noisy vision tokens
921
+ has_noisy_vision = (
922
+ vision is not None
923
+ and vision.tokens is not None
924
+ and isinstance(vision.mse_loss_indexes, torch.Tensor)
925
+ and vision.mse_loss_indexes.numel() > 0
926
+ )
927
+ if not has_noisy_vision:
928
+ # No noisy vision tokens present. The model is predicting actions
929
+ # given clean vision tokens. We need to execute a dummy forward to maintain
930
+ # computation graph consistency across ranks (FSDP should torch all weights).
931
+ preds_vision = torch.zeros(
932
+ [1, self.patch_latent_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype
933
+ ) # [1,patch_latent_dim]
934
+ preds_vision = self.vae2llm(preds_vision) # [1,hidden_size]
935
+ preds_vision = self.llm2vae(preds_vision) # [1,patch_latent_dim]
936
+ # Return a list of per-sample zero tensors with correct shapes (e.g. (C, T, H, W)),
937
+ # so downstream code (_get_velocity, _compute_flow_matching_loss) that iterates over preds_vision
938
+ # gets properly-shaped tensors. Without this, the dummy tensor (1, patch_latent_dim)
939
+ # would cause a size mismatch when concatenating vision+action velocities.
940
+ # When vision is None (no vision in batch), fall back to [preds_vision] purely for
941
+ # gradient graph consistency — it won't be iterated over.
942
+ if vision is not None and vision.tokens is not None:
943
+ preds_vision_list = [torch.zeros_like(tok) for tok in vision.tokens]
944
+ # Inject dummy forward's computation graph so vae2llm/llm2vae params
945
+ # stay in the autograd graph (zeros_like creates detached tensors).
946
+ preds_vision_list[0] = preds_vision_list[0] + 0.0 * preds_vision.sum()
947
+ else:
948
+ preds_vision_list = [preds_vision]
949
+ output_dict.update(preds_vision=preds_vision_list)
950
+ else:
951
+ assert vision is not None # Type narrowing
952
+ assert isinstance(vision.mse_loss_indexes, torch.Tensor)
953
+ assert vision.noisy_frame_indexes is not None
954
+ preds_vision = self.llm2vae(
955
+ last_hidden_state[vision.mse_loss_indexes]
956
+ ) # [total_noisy_vision_patches,patch_latent_dim]
957
+ preds_vision = self.unpatchify_and_unpack_latents(
958
+ preds_vision,
959
+ token_shapes_vision=vision.token_shapes,
960
+ noisy_frame_indexes_vision=vision.noisy_frame_indexes,
961
+ original_latent_shapes=original_latent_shapes,
962
+ )
963
+ output_dict.update(preds_vision=preds_vision)
964
+
965
+ def _encode_action(
966
+ self,
967
+ packed_seq: PackedSequence,
968
+ packed_sequence: torch.Tensor,
969
+ target_dtype: torch.dtype,
970
+ ) -> None:
971
+ """Encode action tokens and fill into packed_sequence."""
972
+ if packed_seq.action is None or packed_seq.action.tokens is None:
973
+ # No action tokens in this batch
974
+ return
975
+
976
+ action: ModalityData = packed_seq.action
977
+ assert action.token_shapes is not None
978
+ assert isinstance(action.sequence_indexes, torch.Tensor)
979
+ assert isinstance(action.timesteps, torch.Tensor)
980
+ assert isinstance(action.mse_loss_indexes, torch.Tensor)
981
+
982
+ # Pack variable-length action tokens into a 1D sequence (same pattern as pack_sound_latents)
983
+ packed_tokens_action, per_token_domain_id = self.pack_action(
984
+ action.tokens, action.token_shapes, action.domain_id
985
+ )
986
+ packed_tokens_action = self.action2llm(packed_tokens_action, per_token_domain_id)
987
+
988
+ packed_tokens_action = packed_tokens_action + self.action_modality_embed.view(
989
+ 1, -1
990
+ ) # [B_action*T_action,hidden_size]
991
+
992
+ has_noisy_actions = has_noisy_tokens(action)
993
+ if has_noisy_actions:
994
+ timesteps_action = action.timesteps * self.timestep_scale # [N_noisy_frames_action]
995
+ packed_timestep_embeds_action = self._embed_packed_timesteps(
996
+ timesteps_action, packed_seq
997
+ ) # [N_noisy_frames_action,hidden_size]
998
+ packed_timestep_embeds_action = packed_timestep_embeds_action.to(
999
+ target_dtype
1000
+ ) # [N_noisy_frames_action,hidden_size]
1001
+
1002
+ packed_tokens_action = _apply_timestep_embeds_to_noisy_tokens(
1003
+ packed_tokens=packed_tokens_action,
1004
+ packed_timestep_embeds=packed_timestep_embeds_action,
1005
+ noisy_frame_indexes=action.noisy_frame_indexes,
1006
+ token_shapes=action.token_shapes,
1007
+ ) # [B_action*T_action,hidden_size]
1008
+
1009
+ if self.behavior_pbd is not None and self.behavior_adapter is not None:
1010
+ if not all(hasattr(packed_seq, name) for name in ("behavior_global", "behavior_phase", "behavior_effect", "behavior_effect_valid")):
1011
+ raise ValueError("Zeva requires task context, phase and BIT features")
1012
+ action_lengths = {int(tokens.shape[0]) for tokens in action.tokens}
1013
+ if len(action_lengths) != 1:
1014
+ raise ValueError("Zeva policy injection requires a fixed action horizon within each batch")
1015
+ prior_mean, prior_std = self.behavior_pbd(
1016
+ packed_seq.behavior_global,
1017
+ packed_seq.behavior_phase,
1018
+ packed_seq.behavior_effect,
1019
+ packed_seq.behavior_effect_valid,
1020
+ )
1021
+ residual = self.behavior_adapter(
1022
+ prior_mean,
1023
+ action_length=action_lengths.pop(),
1024
+ leading_condition_steps=self.behavior_prior_leading_condition_steps,
1025
+ )
1026
+ if self.training and self.behavior_prior_dropout_rate:
1027
+ keep = torch.rand((residual.shape[0], 1, 1), device=residual.device) >= self.behavior_prior_dropout_rate
1028
+ residual = residual * keep.to(residual.dtype)
1029
+ elif not self.training:
1030
+ residual = residual * self.behavior_prior_inference_guidance_scale
1031
+ packed_tokens_action = packed_tokens_action + residual.flatten(0, 1).to(packed_tokens_action.dtype)
1032
+ packed_seq.behavior_prior_mean = prior_mean
1033
+ packed_seq.behavior_prior_std = prior_std
1034
+
1035
+ packed_sequence[action.sequence_indexes] = (
1036
+ packed_tokens_action # [B_action*T_action,hidden_size] scattered into [N_total,hidden_size]
1037
+ )
1038
+
1039
+ def _decode_action(
1040
+ self,
1041
+ packed_seq: PackedSequence,
1042
+ last_hidden_state: torch.Tensor,
1043
+ output_dict: dict,
1044
+ ) -> None:
1045
+ """Decode action tokens from hidden states and update output_dict."""
1046
+ action = packed_seq.action
1047
+ # Shared predicate with OmniMoTModel's has_noisy_actions gating: actions
1048
+ # are decodable targets only when the packer marked action tokens noisy.
1049
+ has_noisy_action = has_noisy_tokens(action)
1050
+ if not has_noisy_action:
1051
+ # dummy forward to maintain computation graph consistency across ranks
1052
+ preds_action = torch.zeros(
1053
+ [1, self.action_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype
1054
+ ) # [1,action_dim]
1055
+ dummy_domain_id = torch.zeros([1], device=last_hidden_state.device, dtype=torch.long) # [1]
1056
+ preds_action = self.action2llm(preds_action, dummy_domain_id) + self.action_modality_embed.view(
1057
+ 1, -1
1058
+ ) # [1,hidden_size]
1059
+ preds_action = self.llm2action(preds_action, dummy_domain_id) # [1,action_dim]
1060
+ # Return a list of per-sample zero tensors with correct shapes (e.g. (T, action_dim)),
1061
+ # so downstream code (_get_velocity, _compute_flow_matching_loss) that iterates over preds_action
1062
+ # gets properly-shaped tensors. Without this, the dummy tensor (1, action_dim)
1063
+ # would cause a size mismatch when concatenating vision+action velocities.
1064
+ if action is not None and action.tokens is not None:
1065
+ preds_action_list = [torch.zeros_like(tok) for tok in action.tokens]
1066
+ # Inject dummy forward's computation graph so DomainAwareLinear params
1067
+ # stay in the autograd graph (zeros_like creates detached tensors).
1068
+ preds_action_list[0] = preds_action_list[0] + 0.0 * preds_action.sum()
1069
+ # When action is None (no action in batch), fall back to [preds_action] purely for
1070
+ # gradient graph consistency — it won't be iterated over.
1071
+ else:
1072
+ preds_action_list = [preds_action]
1073
+ output_dict.update(preds_action=preds_action_list)
1074
+ else:
1075
+ assert action is not None # Type narrowing
1076
+ assert isinstance(action.mse_loss_indexes, torch.Tensor)
1077
+ assert action.condition_mask is not None
1078
+ assert len(action.domain_id) > 0
1079
+
1080
+ action_hidden_states = last_hidden_state[action.mse_loss_indexes] # [total_noisy_action_tokens,hidden_size]
1081
+
1082
+ # Build per-token domain IDs for the noisy tokens (same expansion logic as pack_action)
1083
+ domain_ids: list[torch.Tensor] = []
1084
+ for nfi, d_id in zip(action.noisy_frame_indexes, action.domain_id):
1085
+ domain_ids.append(d_id.expand(len(nfi)))
1086
+ per_token_domain_id = torch.cat(domain_ids, dim=0)
1087
+
1088
+ preds_action = self.llm2action(
1089
+ action_hidden_states, per_token_domain_id
1090
+ ) # [total_noisy_action_tokens,action_dim]
1091
+ # The frozen base output is always computed first. R3's
1092
+ # zero-initialized residual expert is always on at inference: the
1093
+ # training-only event metadata must never become a phase/task
1094
+ # router. The legacy explicit gate remains available only when
1095
+ # ``always_on`` is false, preserving the old experiment's exact
1096
+ # bypass semantics without confusing it with R3.
1097
+ residual_cfg = self.config.residual_expert_config or {}
1098
+ residual_always_on = bool(residual_cfg.get("always_on", False))
1099
+ if self.action_residual_head is not None and (residual_always_on or action.residual_gate is not None):
1100
+ residual_delta = self.action_residual_head(action_hidden_states)
1101
+ arm_limit = float(residual_cfg.get("arm_limit", 0.05))
1102
+ gripper_limit = float(residual_cfg.get("gripper_limit", 0.5))
1103
+ gripper_index = int(residual_cfg.get("gripper_index", 7 if self.action_dim == 8 else 9))
1104
+ if arm_limit < 0 or gripper_limit < 0:
1105
+ raise ValueError("Residual arm/gripper limits must be non-negative")
1106
+ offset = 0
1107
+ bounded_parts: list[torch.Tensor] = []
1108
+ for sample_index, nfi in enumerate(action.noisy_frame_indexes):
1109
+ count = int(nfi.numel())
1110
+ delta_i = residual_delta[offset : offset + count]
1111
+ if residual_always_on:
1112
+ gate_i = torch.ones((count, 1), device=delta_i.device, dtype=delta_i.dtype)
1113
+ else:
1114
+ gate_i = action.residual_gate[sample_index].to(
1115
+ device=delta_i.device, dtype=delta_i.dtype
1116
+ )
1117
+ gate_i = gate_i.index_select(0, nfi.to(device=gate_i.device)).reshape(-1, 1)
1118
+ raw_dim = action.raw_action_dim[sample_index] if action.raw_action_dim is not None else None
1119
+ delta_i = bounded_action_residual(
1120
+ delta_i,
1121
+ gate_i,
1122
+ arm_limit=arm_limit,
1123
+ gripper_limit=gripper_limit,
1124
+ gripper_index=gripper_index,
1125
+ raw_action_dim=raw_dim,
1126
+ )
1127
+ bounded_parts.append(delta_i)
1128
+ offset += count
1129
+ if bounded_parts:
1130
+ preds_action = preds_action + torch.cat(bounded_parts, dim=0)
1131
+ preds_action = self.unpack_action(preds_action, action.token_shapes, action.noisy_frame_indexes)
1132
+ output_dict.update(preds_action=preds_action)
1133
+
1134
+ def _encode_sound(
1135
+ self,
1136
+ packed_seq: PackedSequence,
1137
+ packed_sequence: torch.Tensor,
1138
+ target_dtype: torch.dtype,
1139
+ ) -> None:
1140
+ """Encode sound tokens and fill into packed_sequence.
1141
+
1142
+ Args:
1143
+ packed_seq: PackedSequence containing sound tokens and metadata.
1144
+ packed_sequence: The packed sequence tensor to fill sound embeddings into (modified in-place).
1145
+ target_dtype: Target dtype for embeddings (typically from text embedding).
1146
+ """
1147
+ if packed_seq.sound is None or packed_seq.sound.tokens is None:
1148
+ # No sound tokens in this batch
1149
+ return
1150
+
1151
+ sound = packed_seq.sound
1152
+ assert sound.token_shapes is not None
1153
+ assert isinstance(sound.sequence_indexes, torch.Tensor)
1154
+ assert isinstance(sound.timesteps, torch.Tensor)
1155
+ assert isinstance(sound.mse_loss_indexes, torch.Tensor)
1156
+
1157
+ # Pack sound latents: list of [C, T] tensors → [total_tokens, C]
1158
+ packed_tokens_sound = self.pack_sound_latents(
1159
+ sound.tokens, sound.token_shapes
1160
+ ) # [total_sound_tokens,sound_dim]
1161
+ packed_tokens_sound = packed_tokens_sound.to(target_dtype) # [total_sound_tokens,sound_dim]
1162
+
1163
+ # Project sound tokens + modality embedding. Position info comes from
1164
+ # mRoPE position IDs in the attention layers.
1165
+ packed_tokens_sound = (
1166
+ self.sound2llm(packed_tokens_sound) + self.sound_modality_embed
1167
+ ) # [total_sound_tokens,hidden_size]
1168
+
1169
+ has_noisy_sound = sound.mse_loss_indexes.numel() > 0
1170
+ if has_noisy_sound:
1171
+ timesteps_sound = sound.timesteps * self.timestep_scale # [N_noisy_frames_sound]
1172
+ packed_timestep_embeds_sound = self._embed_packed_timesteps(
1173
+ timesteps_sound, packed_seq
1174
+ ) # [N_noisy_frames_sound,hidden_size]
1175
+ packed_timestep_embeds_sound = packed_timestep_embeds_sound.to(
1176
+ target_dtype
1177
+ ) # [N_noisy_frames_sound,hidden_size]
1178
+
1179
+ packed_tokens_sound = _apply_timestep_embeds_to_noisy_tokens(
1180
+ packed_tokens=packed_tokens_sound,
1181
+ packed_timestep_embeds=packed_timestep_embeds_sound,
1182
+ noisy_frame_indexes=sound.noisy_frame_indexes,
1183
+ token_shapes=sound.token_shapes,
1184
+ ) # [total_sound_tokens,hidden_size]
1185
+
1186
+ packed_sequence[sound.sequence_indexes] = (
1187
+ packed_tokens_sound # [total_sound_tokens,hidden_size] scattered into [N_total,hidden_size]
1188
+ )
1189
+
1190
+ def _decode_sound(
1191
+ self,
1192
+ packed_seq: PackedSequence,
1193
+ last_hidden_state: torch.Tensor,
1194
+ output_dict: dict,
1195
+ ) -> None:
1196
+ """Decode sound tokens from hidden states and update output_dict.
1197
+
1198
+ Args:
1199
+ packed_seq: PackedSequence containing sound modality data.
1200
+ last_hidden_state: Hidden states from the transformer.
1201
+ output_dict: Output dictionary to update with preds_sound (modified in-place).
1202
+ """
1203
+ sound = packed_seq.sound
1204
+ # Check if no sound or no noisy sound tokens
1205
+ has_noisy_sound = (
1206
+ sound is not None
1207
+ and sound.tokens is not None
1208
+ and isinstance(sound.mse_loss_indexes, torch.Tensor)
1209
+ and sound.mse_loss_indexes.numel() > 0
1210
+ )
1211
+ if not has_noisy_sound:
1212
+ # dummy forward to maintain computation graph consistency across ranks
1213
+ preds_sound = torch.zeros(
1214
+ [1, self.sound_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype
1215
+ ) # [1,sound_dim]
1216
+ preds_sound = self.sound2llm(preds_sound) + self.sound_modality_embed # [1,hidden_size]
1217
+ preds_sound = self.llm2sound(preds_sound) # [1,sound_dim]
1218
+ if sound is not None and sound.tokens is not None:
1219
+ preds_sound_list = [torch.zeros_like(tok) for tok in sound.tokens]
1220
+ preds_sound_list[0] = preds_sound_list[0] + 0.0 * preds_sound.sum()
1221
+ else:
1222
+ preds_sound_list = [preds_sound]
1223
+ output_dict.update(preds_sound=preds_sound_list)
1224
+ else:
1225
+ assert sound is not None # Type narrowing
1226
+ assert isinstance(sound.mse_loss_indexes, torch.Tensor)
1227
+ assert sound.condition_mask is not None
1228
+ preds_sound = self.llm2sound(
1229
+ last_hidden_state[sound.mse_loss_indexes]
1230
+ ) # [total_noisy_sound_tokens,sound_dim]
1231
+ preds_sound = self.unpack_sound_latents(
1232
+ preds_sound, sound.token_shapes, sound.noisy_frame_indexes
1233
+ ) # list of [C,T] per sample
1234
+ output_dict.update(preds_sound=preds_sound)
1235
+
1236
+ def forward(
1237
+ self,
1238
+ packed_seq: PackedSequence,
1239
+ memory: MemoryState | None = None,
1240
+ video_temporal_causal: bool | None = None,
1241
+ ) -> dict:
1242
+ """
1243
+ Forward pass for Cosmos3VFMNetwork.
1244
+
1245
+ Args:
1246
+ packed_seq: PackedSequence containing all packed tensors and metadata.
1247
+ See PackedSequence dataclass for field details.
1248
+ memory: Optional MemoryState for persistent KV-cache memory
1249
+ (AR inference or rolling-KV-cache training). Built by
1250
+ ``OmniMoTModel.build_memory_state()``.
1251
+ video_temporal_causal: Per-call attention-mode override; ``None``
1252
+ (default) uses the config-selected ``self.video_temporal_causal``.
1253
+
1254
+ Returns:
1255
+ dict with keys:
1256
+ - "preds_vision": list[Tensor[C,T,H,W]], one per sample.
1257
+ - "preds_action": Velocity predictions for action tokens (if action_gen).
1258
+ - "preds_sound": Velocity predictions for sound tokens (if sound_gen).
1259
+ - "last_hidden_state": Last hidden state from the transformer.
1260
+ - "lbl_metadata_*": Load balancing metadata.
1261
+ - "ce_preds": Cross-entropy predictions (if predict_text_tokens is True).
1262
+ """
1263
+ # Note: During inference with @torch.no_grad(), model may be in training mode
1264
+ # This is intentional for proper batch norm / dropout behavior
1265
+ # assert self.training, "Cosmos3VFMNetwork only supports training mode"
1266
+
1267
+ packed_sequence, target_dtype = self._encode_text(packed_seq) # packed_sequence: [N_total,hidden_size]
1268
+ self._encode_behavior_prefix(packed_seq, packed_sequence, target_dtype)
1269
+ self._encode_online_memory_prefix(packed_seq, packed_sequence, target_dtype)
1270
+ self._encode_proprio_prefix(packed_seq, packed_sequence, target_dtype)
1271
+
1272
+ # encode vision tokens
1273
+ original_latent_shapes: List[Tuple[int, int, int]] | None = None
1274
+ if self.config.vision_gen:
1275
+ original_latent_shapes = self._encode_vision(packed_seq, packed_sequence, target_dtype)
1276
+
1277
+ # encode action tokens
1278
+ if self.config.action_gen:
1279
+ self._encode_action(packed_seq, packed_sequence, target_dtype)
1280
+
1281
+ # encode sound tokens
1282
+ if self.config.sound_gen:
1283
+ self._encode_sound(packed_seq, packed_sequence, target_dtype)
1284
+
1285
+ assert packed_seq.attn_modes is not None
1286
+ assert packed_seq.split_lens is not None
1287
+ use_video_temporal_causal = (
1288
+ self.video_temporal_causal if video_temporal_causal is None else video_temporal_causal
1289
+ )
1290
+
1291
+ # Get all generation sequence indexes for MoE routing
1292
+ # IMPORTANT: Include ALL latent tokens (video + action + sound), not just generation targets.
1293
+ # Condition tokens still need to be routed to diffusion experts; they are excluded from
1294
+ # LOSS computation, not from routing.
1295
+ all_gen_indexes = []
1296
+ if packed_seq.vision is not None:
1297
+ assert packed_seq.vision.token_shapes is not None
1298
+ assert isinstance(packed_seq.vision.sequence_indexes, torch.Tensor)
1299
+ all_gen_indexes.append(packed_seq.vision.sequence_indexes)
1300
+ if packed_seq.action is not None and isinstance(packed_seq.action.sequence_indexes, torch.Tensor):
1301
+ all_gen_indexes.append(packed_seq.action.sequence_indexes)
1302
+ if packed_seq.sound is not None and isinstance(packed_seq.sound.sequence_indexes, torch.Tensor):
1303
+ all_gen_indexes.append(packed_seq.sound.sequence_indexes)
1304
+ vision_sequence_indexes = torch.cat(all_gen_indexes, dim=0) if all_gen_indexes else None # [N_gen_tokens]
1305
+
1306
+ # When temporal causal is enabled the buffer is [action_t0, vision_t0, action_t1, vision_t1, ...].
1307
+ # After torch.cat([vision_indexes, action_indexes]) the interleaved order is lost; sorting restores it.
1308
+ if self.video_temporal_causal:
1309
+ assert packed_seq.sound is None, "Sound generation is not supported with video_temporal_causal=True."
1310
+ if vision_sequence_indexes is not None:
1311
+ vision_sequence_indexes = vision_sequence_indexes.sort().values # [N_gen_tokens]
1312
+
1313
+ vision_token_shapes = packed_seq.vision.token_shapes if packed_seq.vision else None
1314
+
1315
+ # The packer is the single source of truth for the supertoken layout.
1316
+ # ``num_action_tokens_per_supertoken`` is stamped onto ``packed_seq`` by
1317
+ # ``pack_supertokens_temporal_causal`` (= tcf when actions are packed
1318
+ # inline, 0 otherwise) and read unchanged by the attention builder, the
1319
+ # NATTEN metadata generator, and the rolling KV-cache state — keeping
1320
+ # all downstream supertoken geometry automatically in sync with the pack.
1321
+ num_action_tokens_per_supertoken = packed_seq.num_action_tokens_per_supertoken
1322
+
1323
+ replicated_attention_io_cp = (
1324
+ self.attention_io_layout == "replicated"
1325
+ and self.parallel_dims is not None
1326
+ and self.parallel_dims.cp_enabled
1327
+ )
1328
+ # ``sequence_sharded`` attention I/O shards the token sequence, so
1329
+ # packing must pad sequence lengths to the CP size and the input/output
1330
+ # sequence helpers need the CP mesh. ``replicated`` attention I/O keeps
1331
+ # current-frame sequences replicated and uses the CP mesh later inside
1332
+ # attention to slice local heads, so the effective sequence-sharding
1333
+ # world size is 1 here.
1334
+ sequence_shard_parallel_dims = None if replicated_attention_io_cp else self.parallel_dims
1335
+ sequence_shard_world_size = (
1336
+ 1 if replicated_attention_io_cp else (self.parallel_dims.cp_size if self.parallel_dims else 1)
1337
+ )
1338
+ prepared_sequence_pack_metadata = packed_seq.get_sequence_pack_metadata()
1339
+
1340
+ packed_understanding_indexes = torch.cat(
1341
+ (
1342
+ packed_seq.behavior_indexes,
1343
+ packed_seq.online_memory_indexes,
1344
+ packed_seq.proprio_indexes,
1345
+ packed_seq.text_indexes,
1346
+ )
1347
+ ).sort().values
1348
+ input_pack, attention_meta, natten_metadata_list = build_packed_sequence(
1349
+ self.config.joint_attn_implementation,
1350
+ packed_sequence=packed_sequence,
1351
+ attn_modes=packed_seq.attn_modes,
1352
+ split_lens=packed_seq.split_lens,
1353
+ sample_lens=packed_seq.sample_lens,
1354
+ packed_und_token_indexes=packed_understanding_indexes,
1355
+ packed_gen_token_indexes=vision_sequence_indexes,
1356
+ num_heads=self.num_heads,
1357
+ is_image_batch=packed_seq.is_image_batch,
1358
+ head_dim=self.head_dim,
1359
+ num_layers=self.num_hidden_layers,
1360
+ token_shapes=packed_seq.vision.token_shapes,
1361
+ natten_parameter_list=self.natten_parameter_list,
1362
+ cp_world_size=sequence_shard_world_size,
1363
+ video_temporal_causal=use_video_temporal_causal,
1364
+ skip_natten_metadata=memory is not None and not memory.requires_natten_metadata(),
1365
+ vision_token_shapes=vision_token_shapes,
1366
+ action_token_shapes=packed_seq.action.token_shapes if packed_seq.action else None,
1367
+ num_action_tokens_per_supertoken=num_action_tokens_per_supertoken,
1368
+ null_action_supertokens=packed_seq.null_action_supertokens,
1369
+ pad_for_cuda_graphs=self.pad_for_cuda_graphs,
1370
+ prepared_metadata=prepared_sequence_pack_metadata,
1371
+ )
1372
+
1373
+ # ── Multi-control transfer: annotate SplitInfo with per-item ranges ──────
1374
+ # Activated only when packed_seq carries control_weights, i.e. the caller
1375
+ # has set up a multi-control batch via build_transfer_batch.
1376
+ #
1377
+ # multi_control_two_way_attention runs N independent maskless SDPA passes,
1378
+ # one per control. For each pass i, KV = [text | ctrl_i | noisy].
1379
+ # The final noisy output is the weighted sum of the N pass outputs:
1380
+ # noisy_out = w_1 * noisy_out_1 + ... + w_N * noisy_out_N
1381
+ # All SDPA calls are maskless → Flash Attention always active.
1382
+ # N=1, w=1.0 → identical to two_way_attention.
1383
+ #
1384
+ # CP compatibility: control_stream_token_ranges are gen-relative global
1385
+ # offsets computed here, before CP sharding. Ulysses CP restores the full
1386
+ # sequence on every rank (via all-to-all) before calling dispatch_attention,
1387
+ # so the global ranges are valid indices inside multi_control_two_way_attention.
1388
+ if (
1389
+ isinstance(attention_meta, SplitInfo)
1390
+ and packed_seq.control_weights is not None
1391
+ and packed_seq.vision_item_split_lens
1392
+ ):
1393
+ # For multi-control, each sample must have N controls + 1 noisy item
1394
+ # (items 0..N-2 are controls, item N-1 is the noisy target).
1395
+ # Only batch_size=1 is supported; assert to catch misuse early.
1396
+ assert len(packed_seq.vision_item_split_lens) == 1, (
1397
+ f"Multi-control transfer requires batch_size=1, got {len(packed_seq.vision_item_split_lens)} samples."
1398
+ )
1399
+ item_lens = packed_seq.vision_item_split_lens[0] # [L_ctrl0, L_ctrl1, ..., L_noisy]
1400
+ weights = packed_seq.control_weights[0] # [w_ctrl0, w_ctrl1, ...]
1401
+ assert len(item_lens) > 1, (
1402
+ f"Multi-control requires at least 1 control + 1 noisy item; got vision_item_split_lens={item_lens}."
1403
+ )
1404
+ assert len(weights) == len(item_lens) - 1, (
1405
+ f"control_weights length ({len(weights)}) must equal number of control items ({len(item_lens) - 1})."
1406
+ )
1407
+ ctrl_ranges: list[tuple[int, int]] = []
1408
+ cursor = 0
1409
+ for lens in item_lens[:-1]: # all but last = control streams
1410
+ ctrl_ranges.append((cursor, cursor + lens))
1411
+ cursor += lens
1412
+ noisy_range = (cursor, cursor + item_lens[-1])
1413
+ n_gen = int(vision_sequence_indexes.shape[0]) if vision_sequence_indexes is not None else 0
1414
+ assert noisy_range[1] == n_gen, (
1415
+ f"vision_item_split_lens sums to {noisy_range[1]} gen tokens but packed tensor has "
1416
+ f"{n_gen}; packing inconsistency detected."
1417
+ )
1418
+ attention_meta.control_stream_token_ranges = ctrl_ranges
1419
+ attention_meta.noisy_token_range = noisy_range
1420
+ attention_meta.control_weights = weights
1421
+
1422
+ input_pack, packed_position_ids = get_context_parallel_sharded_sequence(
1423
+ attn_implementation=self.config.joint_attn_implementation,
1424
+ input_pack=input_pack,
1425
+ position_ids=packed_seq.position_ids,
1426
+ parallel_dims=sequence_shard_parallel_dims,
1427
+ )
1428
+
1429
+ packed_outputs, lbl_metadata = self.language_model(
1430
+ input_pack,
1431
+ attention_mask=attention_meta,
1432
+ position_ids=packed_position_ids,
1433
+ natten_metadata_list=natten_metadata_list,
1434
+ memory=memory,
1435
+ )
1436
+ last_hidden_state = get_context_parallel_last_hidden_state(
1437
+ packed_outputs=packed_outputs,
1438
+ parallel_dims=sequence_shard_parallel_dims,
1439
+ ) # [N_total,hidden_size]
1440
+ output_dict = dict()
1441
+
1442
+ # decode vision tokens
1443
+ if self.config.vision_gen:
1444
+ self._decode_vision(packed_seq, last_hidden_state, output_dict, original_latent_shapes)
1445
+
1446
+ # decode action tokens
1447
+ if self.config.action_gen:
1448
+ self._decode_action(packed_seq, last_hidden_state, output_dict)
1449
+ if self.behavior_pbd is not None and hasattr(packed_seq, "behavior_prior_mean"):
1450
+ output_dict["behavior_prior_mean"] = packed_seq.behavior_prior_mean
1451
+ output_dict["behavior_prior_std"] = packed_seq.behavior_prior_std
1452
+
1453
+ # decode sound tokens
1454
+ if self.config.sound_gen:
1455
+ self._decode_sound(packed_seq, last_hidden_state, output_dict)
1456
+
1457
+ output_dict.update(last_hidden_state=last_hidden_state)
1458
+ for lbl_metadata_key, lbl_metadata_value in lbl_metadata.items():
1459
+ output_dict.update({f"lbl_metadata_{lbl_metadata_key}": lbl_metadata_value})
1460
+ if self.predict_text_tokens:
1461
+ packed_ce_preds = self.language_model.lm_head(
1462
+ last_hidden_state[packed_seq.ce_loss_indexes]
1463
+ ) # [N_ce_tokens,vocab_size]
1464
+ output_dict["ce_preds"] = packed_ce_preds
1465
+
1466
+ return output_dict
1467
+
1468
+
1469
+ def _apply_timestep_embeds_to_noisy_tokens(
1470
+ packed_tokens: torch.Tensor,
1471
+ packed_timestep_embeds: torch.Tensor,
1472
+ noisy_frame_indexes: List[torch.Tensor],
1473
+ token_shapes: list[tuple[int, ...]],
1474
+ ) -> torch.Tensor:
1475
+ """Apply timestep embeddings to noisy tokens.
1476
+ Tn is the number of noisy frames for a given sample.
1477
+ Tc is the number of clean frames for a given sample.
1478
+ T is the total number of frames for a given sample.
1479
+ T = Tn + Tc
1480
+
1481
+ Args:
1482
+ packed_tokens: The packed tokens to apply timestep embeddings to.
1483
+ packed_timestep_embeds: The packed timestep embeddings to apply.
1484
+ noisy_frame_indexes: The frame indices to apply timestep embeddings to
1485
+ (list of tensors, each with shape (Tn,)).
1486
+ token_shapes: The token shapes for each sample. Each entry is a tuple
1487
+ shaped like ``(T, ...)`` where trailing dimensions represent the spatial grid.
1488
+
1489
+ Returns:
1490
+ The packed tokens with timestep embeddings applied to the noisy tokens.
1491
+ """
1492
+
1493
+ # Handle variable token shapes by processing each sample's noisy_frame_indexes individually.
1494
+ # The noisy indices are first expanded to cover the entire spatial grid of each frame.
1495
+ #
1496
+ # For video frames, the spatial grid is (H, W).
1497
+ # For action frames, the spatial grid is ().
1498
+ # For sound frames, the spatial grid is (1, 1).
1499
+ #
1500
+ # The noisy indices are then flattened into a single tensor overall. When flattening,
1501
+ # we must ensure that the noisy indices from each sample are unique by adding the
1502
+ # cumulative sum of the token shapes of previous samples to the noisy indices for
1503
+ # a given sample.
1504
+ start_noisy_index = 0
1505
+ flattened_noisy_frame_indexes = []
1506
+
1507
+ for noisy_indexes_i, token_shape_i in zip(noisy_frame_indexes, token_shapes):
1508
+ assert noisy_indexes_i.numel() <= token_shape_i[0]
1509
+ spatial_numel_i = math.prod(token_shape_i[1:])
1510
+ spatial_indexes_i = torch.arange(spatial_numel_i, device=packed_tokens.device) # [spatial_numel_i]
1511
+ noisy_indexes_i = (
1512
+ (noisy_indexes_i * spatial_numel_i).unsqueeze(-1).expand(-1, spatial_numel_i)
1513
+ ) # [Tn_i,spatial_numel_i]
1514
+ noisy_indexes_i = noisy_indexes_i.clone() + spatial_indexes_i + start_noisy_index # [Tn_i,spatial_numel_i]
1515
+ flattened_noisy_frame_indexes.append(noisy_indexes_i.flatten()) # [Tn_i*spatial_numel_i]
1516
+ start_noisy_index += math.prod(token_shape_i)
1517
+
1518
+ flattened_noisy_frame_indexes = torch.cat(flattened_noisy_frame_indexes, dim=0) # [total_noisy_patches]
1519
+
1520
+ assert packed_tokens.dim() == 2
1521
+ assert packed_timestep_embeds.dim() == 2
1522
+ assert packed_timestep_embeds.shape[1] == packed_tokens.shape[1]
1523
+ assert packed_timestep_embeds.shape[0] <= packed_tokens.shape[0]
1524
+ assert flattened_noisy_frame_indexes.dim() == 1
1525
+ assert flattened_noisy_frame_indexes.shape[0] == packed_timestep_embeds.shape[0]
1526
+
1527
+ flattened_noisy_frame_indexes = flattened_noisy_frame_indexes.unsqueeze(-1).expand(
1528
+ -1,
1529
+ packed_tokens.shape[1],
1530
+ ) # [total_noisy_patches,hidden_size]
1531
+
1532
+ return packed_tokens.scatter_add(
1533
+ dim=0,
1534
+ index=flattened_noisy_frame_indexes,
1535
+ src=packed_timestep_embeds,
1536
+ ) # [total_tokens,hidden_size]
releases/bridge-native-v3/code/cosmos_framework/model/generator/omni_mot_model.py ADDED
The diff for this file is too large to render. See raw diff
 
releases/bridge-native-v3/code/cosmos_framework/model/generator/utils/data_and_condition.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """
5
+ Unified data and condition interface where we save the tokenized states and/or
6
+ noised latent states for diffusion/flow-matching training.
7
+ Used for the VFM generation model.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+
12
+ import torch
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class GenerationDataClean:
17
+ """
18
+ Container for tokenized states and conditioning info (clean states)
19
+ for the multi-modal (vision, sound, action) MoT training.
20
+ Used for the VFM generation model.
21
+ """
22
+
23
+ batch_size: int
24
+ # Vision (list of per-sample tensors)
25
+ is_image_batch: bool
26
+ raw_state_vision: list[torch.Tensor] | None = None # raw state in pixel space
27
+ x0_tokens_vision: list[torch.Tensor] | None = None # tokenized latent state
28
+ fps_vision: torch.Tensor | None = None
29
+ temporal_positions_vision: list[torch.Tensor] | None = None # one [T] tensor per vision latent item
30
+
31
+ # Image editing: number of vision items per sample.
32
+ # When set, x0_tokens_vision is a flat list of individually-encoded image latents
33
+ # (e.g. [src1, tgt1, src2, tgt2, ...]) and this field records how many items belong
34
+ # to each sample (e.g. [2, 2, ...]). None for standard T2I/T2V (one item per sample).
35
+ num_vision_items_per_sample: list[int] | None = None
36
+
37
+ # Audio (Sound)
38
+ raw_state_sound: torch.Tensor | None = None
39
+ x0_tokens_sound: torch.Tensor | None = None
40
+ fps_sound: torch.Tensor | None = None
41
+
42
+ # Action (dense list of per-sample tensors, only action-having samples)
43
+ raw_state_action: list[torch.Tensor] | None = None
44
+ x0_tokens_action: list[torch.Tensor] | None = None
45
+ fps_action: torch.Tensor | None = None
46
+ action_domain_id: list[torch.Tensor] | None = None # per-sample domain IDs, None when no action samples
47
+ raw_action_dim: list[torch.Tensor] | None = None # raw action dimension, used adding masks to loss calculation
48
+ # Dense per-action-sample gate for the residual expert. Values are in
49
+ # [0,1] and aligned with x0_tokens_action; absent means exact base bypass.
50
+ action_residual_gate: list[torch.Tensor] | None = None
51
+
52
+ # Optional per-sample low-dimensional state used as causal prefix tokens.
53
+ proprio_condition: list[torch.Tensor] | None = None
54
+
55
+ # Multi-control transfer: per-sample list of per-control weights.
56
+ # Shape: [num_samples], each element is a list of floats (one per control stream).
57
+ # None for non-transfer or single-control samples.
58
+ control_weights: list[list[float]] | None = None
59
+
60
+
61
+ @dataclass(slots=True)
62
+ class GenerationDataNoised:
63
+ """Container for states after noise addition, along with other
64
+ helper attributes for the flow-matching (gt velocity and noise)
65
+ for the multi-modal (vision, sound, action) MoT training.
66
+ Used for the VFM generation model.
67
+ """
68
+
69
+ batch_size: int
70
+ # Vision
71
+ epsilon_vision: torch.Tensor # unit gaussian noise tensor
72
+ xt_tokens_vision: torch.Tensor # tokens added with noise level t per flow-matching formulation
73
+ vt_target_vision: torch.Tensor # gt rectified flow field
74
+ sigmas_vision: torch.Tensor | None = None # SNR to add to the vision tokens
75
+
76
+ # Audio (Sound)
77
+ epsilon_sound: torch.Tensor | None = None
78
+ xt_tokens_sound: torch.Tensor | None = None
79
+ vt_target_sound: torch.Tensor | None = None
80
+ sigmas_sound: torch.Tensor | None = None
81
+
82
+ # Action
83
+ epsilon_action: torch.Tensor | None = None
84
+ xt_tokens_action: torch.Tensor | None = None
85
+ vt_target_action: torch.Tensor | None = None
86
+ sigmas_action: torch.Tensor | None = None
87
+ raw_action_dim: list[torch.Tensor] | None = None # raw action dimension, used adding masks to loss calculation
88
+
89
+
90
+ def unwrap_and_densify(raw: list | torch.Tensor | None, to_kwargs: dict) -> list[torch.Tensor] | None:
91
+ """Unwrap nested single-element lists and filter ``None`` entries.
92
+
93
+ The joint dataloader can produce data as nested single-element lists
94
+ (e.g. ``[[t1], [None], [t2]]``). This helper flattens the nesting,
95
+ drops ``None`` entries, and moves the remaining tensors to the target
96
+ device/dtype.
97
+
98
+ Args:
99
+ raw: The raw value from ``data_batch``. May be ``None``, a bare
100
+ tensor, or a (possibly nested) list of tensors / ``None`` s.
101
+ Each tensor in the list has shape ``(...)``.
102
+ to_kwargs: Keyword arguments forwarded to ``torch.Tensor.to``
103
+ (e.g. ``{"device": "cuda"}`` or ``{"device": "cuda", "dtype": torch.bfloat16}``).
104
+
105
+ Returns:
106
+ A dense list of device tensors each with shape ``(...)``, or ``None``
107
+ if the input is ``None`` or every entry is ``None``.
108
+
109
+ Examples:
110
+ >>> unwrap_and_densify([[t1], [None], [t2]], {"device": "cuda"})
111
+ [t1.cuda(), t2.cuda()]
112
+ >>> unwrap_and_densify(None, {"device": "cuda"})
113
+ None
114
+ """
115
+ if raw is None:
116
+ return None
117
+ if not isinstance(raw, list):
118
+ return [raw.to(**to_kwargs)] # list of 1 tensor: [(...)]
119
+ # Unwrap single-element inner lists: [[t], [None]] -> [t, None]
120
+ if len(raw) > 0 and isinstance(raw[0], list):
121
+ raw = [item[0] if isinstance(item, list) else item for item in raw]
122
+ # Filter None entries and move to device
123
+ dense = [x.to(**to_kwargs) for x in raw if x is not None] # list of B tensors: [(...), ...]
124
+ return dense if dense else None
125
+
126
+
127
+ def _expand_per_sample_to_per_vision_item(
128
+ tensor: torch.Tensor, # [B,...]
129
+ num_vision_items_per_sample: list[int] | None,
130
+ ) -> torch.Tensor: # [N_vision_items,...]
131
+ """Expand a per-sample tensor to a per-vision-item tensor.
132
+
133
+ For image editing, each sample may contribute multiple vision items
134
+ (e.g. source + target). This helper repeats each sample's value for
135
+ all of its vision items so that downstream indexing by vision-item
136
+ position works correctly.
137
+
138
+ Args:
139
+ tensor: Per-sample tensor of shape ``(N, ...)``.
140
+ num_vision_items_per_sample: Number of vision items per sample,
141
+ e.g. ``[2, 2, ...]``. If ``None``, the tensor is returned as-is
142
+ (standard single-item-per-sample case).
143
+
144
+ Returns:
145
+ Tensor of shape ``(sum(num_vision_items_per_sample), ...)``, or the
146
+ original tensor when ``num_vision_items_per_sample`` is ``None``.
147
+ """
148
+ if num_vision_items_per_sample is None:
149
+ return tensor # [B,...]
150
+ expanded = []
151
+ for sample_idx, num_items in enumerate(num_vision_items_per_sample):
152
+ for _ in range(
153
+ num_items
154
+ ): # torch.stack(tensor[idx].repeat(num_vision_items_per_sample[idx]) for idx in range(len(num_vision_items_per_sample)))
155
+ expanded.append(tensor[sample_idx]) # [...]
156
+ return torch.stack(expanded) # [N_vision_items,...]
157
+
158
+
159
+ def build_dense_sound_schedule(
160
+ sequence_plans: list,
161
+ x0_tokens_sound: list[torch.Tensor] | None,
162
+ timesteps: torch.Tensor, # [B,...]
163
+ sigmas: torch.Tensor, # [B,...]
164
+ ) -> tuple[torch.Tensor | None, torch.Tensor | None]:
165
+ """Reindex per-sample schedules to match the dense sound tensor list.
166
+
167
+ Sound tensors are dense over samples with ``has_sound=True``, while input
168
+ timesteps/sigmas are indexed by original batch position. This helper maps
169
+ dense sound entry ``i`` back to its source sample's schedule row.
170
+ """
171
+ sound_sample_indices = [i for i, plan in enumerate(sequence_plans) if getattr(plan, "has_sound", False)]
172
+ num_sound_tensors = 0 if x0_tokens_sound is None else len(x0_tokens_sound)
173
+ assert len(sound_sample_indices) == num_sound_tensors, (
174
+ "Sound tensor count must match sequence plans with has_sound=True. "
175
+ f"Got {num_sound_tensors} sound tensor(s) for {len(sound_sample_indices)} sound plan(s)."
176
+ )
177
+
178
+ if not sound_sample_indices:
179
+ return None, None
180
+
181
+ idx_sound = torch.tensor(sound_sample_indices, dtype=torch.long, device=timesteps.device) # [n_sound]
182
+ return timesteps[idx_sound], sigmas[idx_sound] # [n_sound,...], [n_sound,...]
releases/bridge-native-v3/code/cosmos_framework/model/zeva/__init__.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Z-Trans CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Zeva causal-interaction modules required by the released inference path."""
5
+
6
+ from .brief_interaction_trace import BriefInteractionTrace
7
+ from .causal_prompt import CausalPromptConfig, CausalPromptEncoder, inject_causal_prompt
8
+ from .causal_transition_encoder import CausalTransitionEncoder, CausalTransitionEncoderConfig
9
+ from .checkpoint_io import normalize_cte_state_dict
10
+ from .cte_losses import CTELossConfig, causal_transition_encoder_loss
11
+ from .persistent_interaction_memory import (
12
+ PIMMemoryEntry,
13
+ PersistentInteractionMemory,
14
+ PersistentInteractionMemoryConfig,
15
+ )
16
+ from .phase_conditioned_retrieval import PhaseConditionedPIMRetrieval
17
+ from .policy_injection import (
18
+ CausalPromptPolicyAdapter,
19
+ PolicyInjectionConfig,
20
+ PolicyInjectionPrior,
21
+ gaussian_prior_nll,
22
+ )
23
+ from .static_task_context_retrieval import (
24
+ StaticTaskContextRetrievalConfig,
25
+ StaticTaskContextRetrievalHead,
26
+ bidirectional_supervised_contrastive_loss,
27
+ retrieve_static_task_context,
28
+ )
29
+
30
+ __all__ = [
31
+ "CausalTransitionEncoder",
32
+ "CausalTransitionEncoderConfig",
33
+ "CTELossConfig",
34
+ "causal_transition_encoder_loss",
35
+ "BriefInteractionTrace",
36
+ "PolicyInjectionConfig",
37
+ "PolicyInjectionPrior",
38
+ "CausalPromptPolicyAdapter",
39
+ "gaussian_prior_nll",
40
+ "StaticTaskContextRetrievalConfig",
41
+ "StaticTaskContextRetrievalHead",
42
+ "bidirectional_supervised_contrastive_loss",
43
+ "retrieve_static_task_context",
44
+ "PIMMemoryEntry",
45
+ "PersistentInteractionMemory",
46
+ "PersistentInteractionMemoryConfig",
47
+ "PhaseConditionedPIMRetrieval",
48
+ "CausalPromptConfig",
49
+ "CausalPromptEncoder",
50
+ "inject_causal_prompt",
51
+ "normalize_cte_state_dict",
52
+ ]
releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_artifacts.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validation and manifests for fixed-seed PIM attempt artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Iterable
9
+
10
+
11
+ ATTEMPT_RECORD_VERSION = "zeva_pim_attempt_v1"
12
+ CHUNK_RECORD_VERSION = "zeva_pim_chunk_v1"
13
+
14
+
15
+ def sha256_file(path: Path) -> str:
16
+ digest = hashlib.sha256()
17
+ with path.open("rb") as handle:
18
+ for block in iter(lambda: handle.read(1024 * 1024), b""):
19
+ digest.update(block)
20
+ return digest.hexdigest()
21
+
22
+
23
+ def validate_attempt_record(record: dict[str, Any]) -> None:
24
+ required = {
25
+ "schema_version",
26
+ "session_id",
27
+ "task",
28
+ "attempt_id",
29
+ "environment_seed",
30
+ "success",
31
+ "total_steps",
32
+ "chunk_records",
33
+ "artifact_npz",
34
+ "artifact_sha256",
35
+ }
36
+ missing = required.difference(record)
37
+ if missing:
38
+ raise ValueError(f"attempt record is missing fields: {sorted(missing)}")
39
+ if record["schema_version"] != ATTEMPT_RECORD_VERSION:
40
+ raise ValueError(f"unsupported attempt schema: {record['schema_version']!r}")
41
+ if not isinstance(record["session_id"], str) or not record["session_id"]:
42
+ raise ValueError("session_id must be a non-empty string")
43
+ if not isinstance(record["task"], str) or not record["task"]:
44
+ raise ValueError("task must be a non-empty string")
45
+ if int(record["attempt_id"]) < 0 or int(record["environment_seed"]) < 0:
46
+ raise ValueError("attempt_id and environment_seed must be non-negative")
47
+ if not isinstance(record["success"], bool):
48
+ raise ValueError("success must be boolean")
49
+ if int(record["total_steps"]) < 0:
50
+ raise ValueError("total_steps must be non-negative")
51
+ chunks = record["chunk_records"]
52
+ if not isinstance(chunks, list):
53
+ raise ValueError("chunk_records must be a list")
54
+ for index, chunk in enumerate(chunks):
55
+ if chunk.get("schema_version") != CHUNK_RECORD_VERSION:
56
+ raise ValueError(f"unsupported chunk schema at index {index}")
57
+ if int(chunk.get("replan_index", -1)) != index:
58
+ raise ValueError("chunk replan indices must be contiguous from zero")
59
+ start, end = int(chunk.get("start_step", -1)), int(chunk.get("end_step", -1))
60
+ if start < 0 or end < start or int(chunk.get("executed_count", -1)) != end - start:
61
+ raise ValueError(f"invalid chunk interval at index {index}")
62
+ artifact = str(record["artifact_npz"])
63
+ checksum = str(record["artifact_sha256"])
64
+ if not artifact.endswith(".npz") or len(checksum) != 64:
65
+ raise ValueError("invalid attempt artifact reference")
66
+
67
+
68
+ def write_manifest(path: Path, records: Iterable[dict[str, Any]]) -> None:
69
+ rows = list(records)
70
+ for row in rows:
71
+ validate_attempt_record(row)
72
+ payload = {
73
+ "schema_version": ATTEMPT_RECORD_VERSION,
74
+ "attempts": len(rows),
75
+ "records": rows,
76
+ }
77
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_artifacts_test.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from cosmos_framework.model.zeva.attempt_artifacts import (
4
+ ATTEMPT_RECORD_VERSION,
5
+ CHUNK_RECORD_VERSION,
6
+ validate_attempt_record,
7
+ )
8
+
9
+
10
+ def test_attempt_artifact_schema() -> None:
11
+ record = {
12
+ "schema_version": ATTEMPT_RECORD_VERSION,
13
+ "session_id": "kettle:197",
14
+ "task": "TurnOnElectricKettle",
15
+ "attempt_id": 0,
16
+ "environment_seed": 197,
17
+ "success": False,
18
+ "total_steps": 16,
19
+ "chunk_records": [
20
+ {
21
+ "schema_version": CHUNK_RECORD_VERSION,
22
+ "replan_index": 0,
23
+ "start_step": 0,
24
+ "end_step": 16,
25
+ "executed_count": 16,
26
+ }
27
+ ],
28
+ "artifact_npz": "attempt_000_seed_197.npz",
29
+ "artifact_sha256": "0" * 64,
30
+ }
31
+ validate_attempt_record(record)
releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_protocol.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Frozen repeated-attempt protocol for RoboCasa Atomic-5 evaluation.
2
+
3
+ This module intentionally has no simulator or model dependencies so clients,
4
+ servers, tests, and result auditors can share one exact protocol definition.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ PROTOCOL_VERSION = "zeva_robocasa_atomic5_v1"
13
+ PREDICTED_ACTION_HORIZON = 32
14
+ EXECUTED_ACTION_HORIZON = 16
15
+ EFFECT_ACTION_HORIZON = 16
16
+ CTE_TRANSITION_ACTION_HORIZON = 4
17
+ EFFECT_CTE_TRANSITIONS = 4
18
+ MAX_CONTROLS_PER_ATTEMPT = 300
19
+ REQUIRED_WARMUP_REQUESTS = 1
20
+
21
+
22
+ def inference_seed(base_seed: int, attempt_id: int, replan_index: int) -> int:
23
+ """Return a reproducible per-request seed shared by paired conditions."""
24
+ if base_seed < 0 or attempt_id < 0 or replan_index < 0:
25
+ raise ValueError("base_seed, attempt_id, and replan_index must be non-negative")
26
+ # A RoboCasa attempt has at most ceil(300/16)=19 replans. Reserving 1,000
27
+ # values per attempt makes the mapping readable and collision-free.
28
+ if replan_index >= 1_000:
29
+ raise ValueError("replan_index must be < 1000")
30
+ value = int(base_seed) + 1_000 * int(attempt_id) + int(replan_index)
31
+ if value >= 2**32:
32
+ raise ValueError("derived inference seed exceeds uint32")
33
+ return value
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class AttemptSessionKey:
38
+ """Identity of memory that may survive an environment reset."""
39
+
40
+ session_id: str
41
+ task_cluster: str
42
+ environment_seed: int
43
+
44
+ def __post_init__(self) -> None:
45
+ if not self.session_id:
46
+ raise ValueError("session_id must be non-empty")
47
+ if not self.task_cluster:
48
+ raise ValueError("task_cluster must be non-empty")
49
+ if self.environment_seed < 0:
50
+ raise ValueError("environment_seed must be non-negative")
51
+
52
+ def assert_compatible(self, other: "AttemptSessionKey") -> None:
53
+ if self != other:
54
+ raise ValueError(
55
+ "attempt-session mismatch: memory may persist only for the exact "
56
+ f"(session_id, task, seed); existing={self}, requested={other}"
57
+ )
58
+
59
+
60
+ def default_session_id(task_cluster: str, environment_seed: int) -> str:
61
+ if not task_cluster:
62
+ raise ValueError("task_cluster must be non-empty")
63
+ if environment_seed < 0:
64
+ raise ValueError("environment_seed must be non-negative")
65
+ return f"{PROTOCOL_VERSION}:{task_cluster}:seed={int(environment_seed)}"
66
+
67
+
68
+ def contract_manifest() -> dict[str, int | str]:
69
+ return {
70
+ "version": PROTOCOL_VERSION,
71
+ "predicted_action_horizon": PREDICTED_ACTION_HORIZON,
72
+ "executed_action_horizon": EXECUTED_ACTION_HORIZON,
73
+ "effect_action_horizon": EFFECT_ACTION_HORIZON,
74
+ "cte_transition_action_horizon": CTE_TRANSITION_ACTION_HORIZON,
75
+ "effect_cte_transitions": EFFECT_CTE_TRANSITIONS,
76
+ "max_controls_per_attempt": MAX_CONTROLS_PER_ATTEMPT,
77
+ "discarded_warmup_requests": REQUIRED_WARMUP_REQUESTS,
78
+ }
releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_protocol_test.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from cosmos_framework.model.zeva.attempt_protocol import (
6
+ EFFECT_ACTION_HORIZON,
7
+ EFFECT_CTE_TRANSITIONS,
8
+ EXECUTED_ACTION_HORIZON,
9
+ PREDICTED_ACTION_HORIZON,
10
+ REQUIRED_WARMUP_REQUESTS,
11
+ CTE_TRANSITION_ACTION_HORIZON,
12
+ AttemptSessionKey,
13
+ default_session_id,
14
+ inference_seed,
15
+ )
16
+
17
+
18
+ def test_zeva_temporal_contract_is_exact() -> None:
19
+ assert PREDICTED_ACTION_HORIZON == 32
20
+ assert EXECUTED_ACTION_HORIZON == 16
21
+ assert EFFECT_ACTION_HORIZON == 16
22
+ assert CTE_TRANSITION_ACTION_HORIZON == 4
23
+ assert EFFECT_CTE_TRANSITIONS == 4
24
+ assert CTE_TRANSITION_ACTION_HORIZON * EFFECT_CTE_TRANSITIONS == EFFECT_ACTION_HORIZON
25
+ assert REQUIRED_WARMUP_REQUESTS == 1
26
+
27
+
28
+ def test_attempt_memory_survives_only_exact_task_seed_session() -> None:
29
+ session = AttemptSessionKey(default_session_id("OpenStandMixerHead", 195), "OpenStandMixerHead", 195)
30
+ session.assert_compatible(
31
+ AttemptSessionKey(default_session_id("OpenStandMixerHead", 195), "OpenStandMixerHead", 195)
32
+ )
33
+ with pytest.raises(ValueError, match="attempt-session mismatch"):
34
+ session.assert_compatible(
35
+ AttemptSessionKey(default_session_id("OpenStandMixerHead", 195), "OpenStandMixerHead", 196)
36
+ )
37
+ with pytest.raises(ValueError, match="attempt-session mismatch"):
38
+ session.assert_compatible(AttemptSessionKey(session.session_id, "TurnOnMicrowave", 195))
39
+
40
+
41
+ def test_paired_inference_seed_schedule_is_deterministic() -> None:
42
+ seeds_a = [inference_seed(20260824, attempt, replan) for attempt in range(3) for replan in range(19)]
43
+ seeds_b = [inference_seed(20260824, attempt, replan) for attempt in range(3) for replan in range(19)]
44
+ assert seeds_a == seeds_b
45
+ assert len(seeds_a) == len(set(seeds_a))
releases/bridge-native-v3/code/cosmos_framework/model/zeva/brief_interaction_trace.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Z-Trans CORPORATION.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Brief Interaction Trace (BIT) for the current attempt."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from torch import Tensor
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class BriefInteractionTrace:
15
+ """Ordered causal effects completed during the current attempt."""
16
+
17
+ effects: Tensor
18
+ valid: Tensor
19
+
20
+ def __post_init__(self) -> None:
21
+ if self.effects.ndim != 3:
22
+ raise ValueError("BIT effects must be [B,L,D]")
23
+ if self.valid.shape != self.effects.shape[:2]:
24
+ raise ValueError("BIT valid mask must be [B,L]")
releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_prompt.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Z-Trans CORPORATION.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Causal Prompt construction and policy injection (paper Eq. 8)."""
5
+
6
+ from .persistent_interaction_memory import (
7
+ CausalPromptConfig,
8
+ CausalPromptEncoder,
9
+ inject_causal_prompt,
10
+ )
11
+
12
+ __all__ = ["CausalPromptConfig", "CausalPromptEncoder", "inject_causal_prompt"]
releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_transition_encoder.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Z-Trans CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Causal Transition Encoder (CTE) for robot trajectories.
5
+
6
+ This is intentionally independent from the Cosmos action denoiser. It learns a
7
+ per-time-step causal interaction state from the same multi-view observation and
8
+ 8-D joint-position command format consumed by Cosmos Action.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from copy import deepcopy
14
+ from dataclasses import asdict, dataclass
15
+
16
+ import torch
17
+ from torch import Tensor, nn
18
+ from torch.nn import functional as F
19
+ from torch.utils.checkpoint import checkpoint
20
+
21
+
22
+ @dataclass
23
+ class CausalTransitionEncoderConfig:
24
+ action_dim: int = 8
25
+ hidden_dim: int = 256
26
+ retrieval_dim: int = 128
27
+ phase_dim: int = 128
28
+ effect_dim: int = 128
29
+ transition_steps: int = 4
30
+ # Phase stays at the VAE's 4x cadence. Effect uses four such transitions
31
+ # (16 raw controls) so it is supervised by an observable visual outcome.
32
+ effect_window_transitions: int = 4
33
+ effect_target_grid: tuple[int, int] = (4, 6)
34
+ num_layers: int = 4
35
+ num_heads: int = 8
36
+ image_channels: int = 3
37
+ ema_decay: float = 0.996
38
+ # GRU is the stable/default RoboCasa lineage. Mamba remains opt-in so that
39
+ # installing mamba_ssm cannot silently change the checkpoint architecture.
40
+ use_mamba: bool = False
41
+ vision_chunk_size: int = 256
42
+ vision_gradient_checkpointing: bool = True
43
+ vision_checkpoint_threshold: int = 1024
44
+
45
+ def to_dict(self) -> dict[str, object]:
46
+ return asdict(self)
47
+
48
+
49
+ class _VisionStem(nn.Module):
50
+ """Small trainable visual stem; inputs are DROID's composite multi-view frames."""
51
+
52
+ def __init__(
53
+ self,
54
+ channels: int,
55
+ hidden_dim: int,
56
+ chunk_size: int,
57
+ gradient_checkpointing: bool,
58
+ checkpoint_threshold: int,
59
+ ) -> None:
60
+ super().__init__()
61
+ width = max(hidden_dim // 4, 32)
62
+ self.net = nn.Sequential(
63
+ nn.Conv2d(channels, width, kernel_size=7, stride=4, padding=3),
64
+ nn.GELU(),
65
+ nn.Conv2d(width, width * 2, kernel_size=3, stride=2, padding=1),
66
+ nn.GELU(),
67
+ nn.Conv2d(width * 2, hidden_dim, kernel_size=3, stride=2, padding=1),
68
+ nn.GELU(),
69
+ nn.AdaptiveAvgPool2d(1),
70
+ )
71
+ self.proj = nn.Linear(hidden_dim, hidden_dim)
72
+ self.chunk_size = chunk_size
73
+ self.gradient_checkpointing = gradient_checkpointing
74
+ self.checkpoint_threshold = checkpoint_threshold
75
+
76
+ def _encode_flat(self, flat: Tensor) -> Tensor:
77
+ return self.proj(self.net(flat).flatten(1))
78
+
79
+ def forward(self, frames: Tensor) -> Tensor:
80
+ # [B,T,C,H,W] -> [B,T,D]
81
+ batch, steps = frames.shape[:2]
82
+ flat = frames.flatten(0, 1)
83
+ # A complete DROID segment can exceed 2,000 frames. Checkpoint each
84
+ # visual micro-batch so the CTE remains full-trajectory causal while
85
+ # its convolutional activations do not scale with segment length.
86
+ use_checkpoint = (
87
+ self.training
88
+ and self.gradient_checkpointing
89
+ and torch.is_grad_enabled()
90
+ and flat.shape[0] > self.checkpoint_threshold
91
+ )
92
+ if not use_checkpoint:
93
+ return self._encode_flat(flat).unflatten(0, (batch, steps))
94
+ chunks = []
95
+ for frame_chunk in flat.split(self.chunk_size, dim=0):
96
+ if use_checkpoint:
97
+ chunks.append(checkpoint(self._encode_flat, frame_chunk, use_reentrant=False))
98
+ else:
99
+ chunks.append(self._encode_flat(frame_chunk))
100
+ return torch.cat(chunks, dim=0).unflatten(0, (batch, steps))
101
+
102
+
103
+ class _FrozenVAEDeltaTarget(nn.Module):
104
+ """Fixed spatial projection of cached Cosmos-VAE latents.
105
+
106
+ Unlike the EMA vision stem, this target cannot drift to make an auxiliary
107
+ loss easy. Retaining a 4x6 pooled grid makes camera/object displacement
108
+ visible instead of reducing every frame to one spatial mean.
109
+ """
110
+
111
+ def __init__(self, channels: int, hidden_dim: int, grid: tuple[int, int]) -> None:
112
+ super().__init__()
113
+ self.grid = grid
114
+ input_dim = channels * grid[0] * grid[1]
115
+ generator = torch.Generator().manual_seed(20260815)
116
+ projection = torch.randn(hidden_dim, input_dim, generator=generator) / input_dim**0.5
117
+ self.register_buffer("projection", projection, persistent=True)
118
+
119
+ @torch.no_grad()
120
+ def forward(self, frames: Tensor) -> Tensor:
121
+ batch, steps = frames.shape[:2]
122
+ flat = frames.flatten(0, 1).float()
123
+ pooled = F.adaptive_avg_pool2d(flat, self.grid).flatten(1)
124
+ pooled = F.layer_norm(pooled, (pooled.shape[-1],))
125
+ return F.linear(pooled, self.projection).unflatten(0, (batch, steps))
126
+
127
+
128
+ class _CausalMixer(nn.Module):
129
+ """Causal temporal mixer with a portable GRU fallback when mamba_ssm is absent."""
130
+
131
+ def __init__(self, hidden_dim: int, use_mamba: bool) -> None:
132
+ super().__init__()
133
+ self.norm = nn.LayerNorm(hidden_dim)
134
+ self.kind = "gru"
135
+ if use_mamba:
136
+ try:
137
+ from mamba_ssm import Mamba # type: ignore[import-not-found]
138
+
139
+ self.mixer: nn.Module = Mamba(d_model=hidden_dim, d_state=16, d_conv=4, expand=2)
140
+ self.kind = "mamba"
141
+ except ImportError:
142
+ self.mixer = nn.GRU(hidden_dim, hidden_dim, batch_first=True)
143
+ else:
144
+ self.mixer = nn.GRU(hidden_dim, hidden_dim, batch_first=True)
145
+
146
+ def forward(self, x: Tensor) -> Tensor:
147
+ y = self.norm(x)
148
+ if self.kind == "mamba":
149
+ y = self.mixer(y)
150
+ else:
151
+ y, _ = self.mixer(y)
152
+ return x + y
153
+
154
+
155
+ class _CausalInteractionBlock(nn.Module):
156
+ """Causal visual/action/interaction streams with same-step cross attention."""
157
+
158
+ def __init__(self, cfg: CausalTransitionEncoderConfig) -> None:
159
+ super().__init__()
160
+ self.visual = _CausalMixer(cfg.hidden_dim, cfg.use_mamba)
161
+ self.action = _CausalMixer(cfg.hidden_dim, cfg.use_mamba)
162
+ self.interaction_state = _CausalMixer(cfg.hidden_dim, cfg.use_mamba)
163
+ self.cross = nn.MultiheadAttention(cfg.hidden_dim, cfg.num_heads, batch_first=True)
164
+ self.cross_norm = nn.LayerNorm(cfg.hidden_dim)
165
+ self.ffn = nn.Sequential(
166
+ nn.LayerNorm(cfg.hidden_dim),
167
+ nn.Linear(cfg.hidden_dim, 4 * cfg.hidden_dim),
168
+ nn.GELU(),
169
+ nn.Linear(4 * cfg.hidden_dim, cfg.hidden_dim),
170
+ )
171
+
172
+ def forward(self, visual: Tensor, action: Tensor, interaction_state: Tensor, valid: Tensor) -> tuple[Tensor, Tensor, Tensor]:
173
+ visual, action, interaction_state = (
174
+ self.visual(visual),
175
+ self.action(action),
176
+ self.interaction_state(interaction_state),
177
+ )
178
+ # The three tokens for each timestep attend only within that timestep.
179
+ tokens = torch.stack((visual, action, interaction_state), dim=2)
180
+ batch, steps, streams, dim = tokens.shape
181
+ tokens = tokens.reshape(batch * steps, streams, dim)
182
+ attended, _ = self.cross(tokens, tokens, tokens, need_weights=False)
183
+ tokens = tokens + attended
184
+ tokens = tokens + self.ffn(tokens)
185
+ tokens = tokens.reshape(batch, steps, streams, dim)
186
+ visual, action, interaction_state = tokens.unbind(dim=2)
187
+ mask = valid.unsqueeze(-1).to(visual.dtype)
188
+ return visual * mask, action * mask, interaction_state * mask
189
+
190
+
191
+ class CausalTransitionEncoder(nn.Module):
192
+ """Causal transition encoder with phase and causal-effect projections."""
193
+
194
+ def __init__(self, cfg: CausalTransitionEncoderConfig | None = None) -> None:
195
+ super().__init__()
196
+ self.cfg = cfg or CausalTransitionEncoderConfig()
197
+ self.visual_encoder = _VisionStem(
198
+ self.cfg.image_channels,
199
+ self.cfg.hidden_dim,
200
+ self.cfg.vision_chunk_size,
201
+ self.cfg.vision_gradient_checkpointing,
202
+ self.cfg.vision_checkpoint_threshold,
203
+ )
204
+ self.target_visual_encoder = deepcopy(self.visual_encoder).requires_grad_(False)
205
+ # The interaction/action stream is intentionally right shifted. At state
206
+ # v_b it sees only the completed transition u_(b-1), never u_b. The
207
+ # latter is supplied exclusively to the auxiliary effect heads.
208
+ transition_dim = self.cfg.transition_steps * self.cfg.action_dim
209
+ self.action_encoder = nn.Sequential(nn.LayerNorm(transition_dim), nn.Linear(transition_dim, self.cfg.hidden_dim))
210
+ self.bos_action = nn.Parameter(torch.zeros(1, 1, self.cfg.hidden_dim))
211
+ nn.init.normal_(self.bos_action, std=0.02)
212
+ self.interaction_state_token = nn.Parameter(torch.zeros(1, 1, self.cfg.hidden_dim))
213
+ nn.init.normal_(self.interaction_state_token, std=0.02)
214
+ self.blocks = nn.ModuleList([_CausalInteractionBlock(self.cfg) for _ in range(self.cfg.num_layers)])
215
+ self.final_norm = nn.LayerNorm(self.cfg.hidden_dim)
216
+ self.retrieval_head = nn.Linear(self.cfg.hidden_dim, self.cfg.retrieval_dim)
217
+ self.phase_head = nn.Linear(self.cfg.hidden_dim, self.cfg.phase_dim)
218
+ self.action_head = nn.Sequential(nn.LayerNorm(self.cfg.hidden_dim), nn.Linear(self.cfg.hidden_dim, transition_dim))
219
+ self.visual_head = nn.Sequential(nn.LayerNorm(self.cfg.hidden_dim), nn.Linear(self.cfg.hidden_dim, self.cfg.hidden_dim))
220
+ effect_window_dim = self.cfg.effect_window_transitions * transition_dim
221
+ self.effect_action_encoder = nn.Sequential(
222
+ nn.LayerNorm(effect_window_dim), nn.Linear(effect_window_dim, self.cfg.hidden_dim)
223
+ )
224
+ self.frozen_effect_target = _FrozenVAEDeltaTarget(
225
+ self.cfg.image_channels, self.cfg.hidden_dim, self.cfg.effect_target_grid
226
+ )
227
+ self.effect_pre_head = nn.Sequential(
228
+ nn.LayerNorm(2 * self.cfg.hidden_dim), nn.Linear(2 * self.cfg.hidden_dim, self.cfg.effect_dim)
229
+ )
230
+ # Observed effect is visual only. Passing executed actions here lets
231
+ # post simply echo the action-conditioned pre code and caused v2's
232
+ # rank-one collapse.
233
+ self.effect_post_head = nn.Sequential(
234
+ nn.LayerNorm(2 * self.cfg.hidden_dim), nn.Linear(2 * self.cfg.hidden_dim, self.cfg.effect_dim)
235
+ )
236
+ self.effect_action_head = nn.Sequential(
237
+ nn.LayerNorm(self.cfg.effect_dim), nn.Linear(self.cfg.effect_dim, 4 * self.cfg.action_dim)
238
+ )
239
+ self.effect_outcome_head = nn.Sequential(
240
+ nn.LayerNorm(self.cfg.effect_dim), nn.Linear(self.cfg.effect_dim, self.cfg.hidden_dim)
241
+ )
242
+
243
+ @torch.no_grad()
244
+ def update_ema_target(self) -> None:
245
+ """Call once after every optimizer step, before building the next target."""
246
+ for target, online in zip(self.target_visual_encoder.parameters(), self.visual_encoder.parameters(), strict=True):
247
+ target.lerp_(online, 1.0 - self.cfg.ema_decay)
248
+
249
+ @torch.no_grad()
250
+ def encode_target_vision(self, frames: Tensor) -> Tensor:
251
+ return self.target_visual_encoder(frames)
252
+
253
+ def forward(
254
+ self,
255
+ frames: Tensor,
256
+ transition_actions: Tensor,
257
+ valid_mask: Tensor | None = None,
258
+ transition_valid: Tensor | None = None,
259
+ ) -> dict[str, Tensor]:
260
+ """Encode padded full trajectories.
261
+
262
+ Args:
263
+ frames: DROID composite observations ``[B,T,3,H,W]`` in [0, 1].
264
+ transition_actions: completed/raw four-action transitions
265
+ ``[B,T-1,4,A]``. At state ``b`` the main action stream only
266
+ receives transition ``b-1``; transition ``b`` is used solely
267
+ to supervise the effect heads.
268
+ valid_mask: ``[B,T]`` true for real timesteps, false for padding.
269
+ """
270
+ if frames.ndim != 5 or transition_actions.ndim != 4:
271
+ raise ValueError("Expected frames [B,T,C,H,W] and transition_actions [B,T-1,4,A].")
272
+ if (
273
+ transition_actions.shape[0] != frames.shape[0]
274
+ or transition_actions.shape[1] != frames.shape[1] - 1
275
+ or transition_actions.shape[2:] != (self.cfg.transition_steps, self.cfg.action_dim)
276
+ ):
277
+ raise ValueError("Frame/transition time axes or action shape do not match CTE config.")
278
+ if valid_mask is None:
279
+ valid_mask = torch.ones(frames.shape[:2], dtype=torch.bool, device=frames.device)
280
+ if transition_valid is None:
281
+ transition_valid = torch.ones(transition_actions.shape[:-1], dtype=torch.bool, device=frames.device)
282
+ if transition_valid.shape != transition_actions.shape[:-1]:
283
+ raise ValueError("transition_valid must have shape [B,T-1,4].")
284
+ visual = self.visual_encoder(frames)
285
+ transition_flat = transition_actions.flatten(-2)
286
+ transition_embed = self.action_encoder(transition_flat)
287
+ action = self.bos_action.expand(frames.shape[0], frames.shape[1], -1).clone()
288
+ action[:, 1:] = transition_embed
289
+ action[:, 1:] *= transition_valid.all(dim=-1, keepdim=True).to(action.dtype)
290
+ interaction_state = self.interaction_state_token.expand_as(visual)
291
+ for block in self.blocks:
292
+ visual, action, interaction_state = block(visual, action, interaction_state, valid_mask)
293
+ z = self.final_norm(interaction_state) * valid_mask.unsqueeze(-1)
294
+ with torch.no_grad():
295
+ target_visual = self.target_visual_encoder(frames)
296
+ transition_complete = valid_mask[:, :-1] & valid_mask[:, 1:] & transition_valid.all(dim=-1)
297
+ effect_windows = (frames.shape[1] - 1) // self.cfg.effect_window_transitions
298
+ effect_steps = self.cfg.effect_window_transitions
299
+ if effect_windows:
300
+ effect_start = torch.arange(effect_windows, device=frames.device) * effect_steps
301
+ effect_end = effect_start + effect_steps
302
+ effect_actions = transition_actions[:, : effect_windows * effect_steps].reshape(
303
+ frames.shape[0], effect_windows, effect_steps, self.cfg.transition_steps, self.cfg.action_dim
304
+ )
305
+ effect_action_valid = transition_valid[:, : effect_windows * effect_steps].reshape(
306
+ frames.shape[0], effect_windows, effect_steps, self.cfg.transition_steps
307
+ )
308
+ effect_complete = (
309
+ valid_mask[:, effect_start]
310
+ & valid_mask[:, effect_end]
311
+ & effect_action_valid.all(dim=(-1, -2))
312
+ )
313
+ effect_embed = self.effect_action_encoder(effect_actions.flatten(-3))
314
+ frozen_visual = self.frozen_effect_target(frames)
315
+ effect_delta = frozen_visual[:, effect_end] - frozen_visual[:, effect_start]
316
+ effect_pre_raw = self.effect_pre_head(torch.cat((z[:, effect_start], effect_embed), dim=-1))
317
+ effect_post_raw = self.effect_post_head(torch.cat((frozen_visual[:, effect_start], effect_delta), dim=-1))
318
+ else:
319
+ effect_actions = transition_actions.new_zeros((frames.shape[0], 0, effect_steps, self.cfg.transition_steps, self.cfg.action_dim))
320
+ effect_complete = torch.zeros((frames.shape[0], 0), dtype=torch.bool, device=frames.device)
321
+ effect_delta = z[:, :0]
322
+ effect_pre_raw = z[:, :0]
323
+ effect_post_raw = z[:, :0]
324
+ # At reset there is a single observed frame and no completed
325
+ # transition/effect window. LayerNorm cannot be applied to the empty
326
+ # ``[..., 0, hidden_dim]`` fallback above because the effect heads
327
+ # expect ``effect_dim``. Preserve the causal no-effect state with
328
+ # correctly shaped empty outputs; callers then provide only BOS/pad
329
+ # effect tokens to the policy.
330
+ if effect_pre_raw.shape[1] == 0:
331
+ effect_pre = effect_pre_raw.new_zeros(
332
+ (frames.shape[0], 0, self.cfg.effect_dim)
333
+ )
334
+ effect_post = effect_pre_raw.new_zeros(
335
+ (frames.shape[0], 0, self.cfg.effect_dim)
336
+ )
337
+ effect_outcome_pre = effect_pre_raw.new_zeros(
338
+ (frames.shape[0], 0, self.cfg.hidden_dim)
339
+ )
340
+ effect_outcome_post = effect_pre_raw.new_zeros(
341
+ (frames.shape[0], 0, self.cfg.hidden_dim)
342
+ )
343
+ effect_action = effect_pre_raw.new_zeros(
344
+ (frames.shape[0], 0, 4 * self.cfg.action_dim)
345
+ )
346
+ else:
347
+ effect_pre = F.normalize(effect_pre_raw, dim=-1)
348
+ effect_post = F.normalize(effect_post_raw, dim=-1)
349
+ effect_outcome_pre = self.effect_outcome_head(effect_pre_raw)
350
+ effect_outcome_post = self.effect_outcome_head(effect_post_raw)
351
+ effect_action = self.effect_action_head(effect_pre_raw)
352
+ return {
353
+ "causal_interaction_state": z,
354
+ "retrieval": F.normalize(self.retrieval_head(z), dim=-1),
355
+ "phase": F.normalize(self.phase_head(z), dim=-1),
356
+ "next_action": self.action_head(z[:, :-1]).view(
357
+ frames.shape[0], frames.shape[1] - 1, self.cfg.transition_steps, self.cfg.action_dim
358
+ ),
359
+ "next_vision": self.visual_head(z),
360
+ # Frozen target-visual representation used as the PIM retrieval key.
361
+ "visual_key": F.normalize(target_visual[..., : self.cfg.phase_dim], dim=-1),
362
+ "effect_pre": effect_pre,
363
+ "effect_post": effect_post,
364
+ "effect_pre_raw": effect_pre_raw,
365
+ "effect_post_raw": effect_post_raw,
366
+ "effect_outcome_pre": effect_outcome_pre,
367
+ "effect_outcome_post": effect_outcome_post,
368
+ "effect_action": effect_action,
369
+ "effect_actions": effect_actions,
370
+ "effect_delta_target": effect_delta,
371
+ "effect_complete": effect_complete,
372
+ "target_visual": target_visual,
373
+ "transition_complete": transition_complete,
374
+ }
releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_transition_encoder_test.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Z-Trans CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ import torch
5
+
6
+ from cosmos_framework.model.zeva import (
7
+ CausalTransitionEncoder,
8
+ CausalTransitionEncoderConfig,
9
+ causal_transition_encoder_loss,
10
+ )
11
+
12
+
13
+ def test_cte_forward_loss_and_ema() -> None:
14
+ model = CausalTransitionEncoder(CausalTransitionEncoderConfig(hidden_dim=32, retrieval_dim=16, phase_dim=16, num_layers=1, num_heads=4, use_mamba=False))
15
+ frames = torch.rand(3, 5, 3, 48, 48)
16
+ actions = torch.rand(3, 4, 4, 8)
17
+ valid = torch.tensor([[1, 1, 1, 1, 1], [1, 1, 1, 1, 0], [1, 1, 1, 0, 0]], dtype=torch.bool)
18
+ output = model(frames, actions, valid)
19
+ assert output["causal_interaction_state"].shape == (3, 5, 32)
20
+ assert output["retrieval"].shape == (3, 5, 16)
21
+ losses = causal_transition_encoder_loss(output, actions, valid, torch.tensor([0, 0, 1]))
22
+ assert torch.isfinite(losses["total"])
23
+ losses["total"].backward()
24
+ before = next(model.target_visual_encoder.parameters()).clone()
25
+ with torch.no_grad():
26
+ next(model.visual_encoder.parameters()).add_(0.01)
27
+ model.update_ema_target()
28
+ assert not torch.equal(before, next(model.target_visual_encoder.parameters()))
releases/bridge-native-v3/code/cosmos_framework/model/zeva/checkpoint_io.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Z-Trans CORPORATION.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Checkpoint loading utilities for Zeva modules."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from typing import Any
10
+
11
+
12
+ def normalize_cte_state_dict(state_dict: Mapping[str, Any]) -> dict[str, Any]:
13
+ """Normalize serialized CTE parameter names without changing tensors."""
14
+
15
+ normalized: dict[str, Any] = {}
16
+ for key, value in state_dict.items():
17
+ new_key = key.replace("behavior_token", "interaction_state_token")
18
+ new_key = new_key.replace(".behavior.", ".interaction_state.")
19
+ if new_key in normalized:
20
+ raise ValueError(f"Duplicate CTE checkpoint key after normalization: {new_key}")
21
+ normalized[new_key] = value
22
+ return normalized
releases/bridge-native-v3/code/cosmos_framework/model/zeva/checkpoint_io_test.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+ from cosmos_framework.model.zeva import (
6
+ CausalTransitionEncoder,
7
+ CausalTransitionEncoderConfig,
8
+ normalize_cte_state_dict,
9
+ )
10
+
11
+
12
+ def test_cte_key_normalization_preserves_every_tensor() -> None:
13
+ config = CausalTransitionEncoderConfig(
14
+ hidden_dim=16,
15
+ retrieval_dim=8,
16
+ phase_dim=8,
17
+ effect_dim=8,
18
+ num_layers=1,
19
+ num_heads=4,
20
+ use_mamba=False,
21
+ )
22
+ source = CausalTransitionEncoder(config)
23
+ serialized = {}
24
+ for key, value in source.state_dict().items():
25
+ serialized_key = key.replace("interaction_state_token", "behavior_token")
26
+ serialized_key = serialized_key.replace(".interaction_state.", ".behavior.")
27
+ serialized[serialized_key] = value.clone()
28
+
29
+ restored = CausalTransitionEncoder(config)
30
+ restored.load_state_dict(normalize_cte_state_dict(serialized), strict=True)
31
+ for key, value in source.state_dict().items():
32
+ assert torch.equal(value, restored.state_dict()[key]), key
releases/bridge-native-v3/code/cosmos_framework/model/zeva/cte_losses.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Z-Trans CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ """Learning objectives for the Causal Transition Encoder (CTE)."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ import torch
11
+ from torch import Tensor
12
+ from torch.nn import functional as F
13
+
14
+
15
+ @dataclass
16
+ class CTELossConfig:
17
+ action_weight: float = 1.0
18
+ vision_weight: float = 1.0
19
+ task_weight: float = 0.2
20
+ phase_weight: float = 0.1
21
+ # Effect-v3: observed outcomes are contrastive and explicitly protected
22
+ # against rank collapse. Action reconstruction remains a weak auxiliary.
23
+ effect_weight: float = 0.25
24
+ effect_contrastive_weight: float = 1.0
25
+ effect_action_weight: float = 0.05
26
+ effect_align_weight: float = 0.1
27
+ effect_variance_weight: float = 1.0
28
+ effect_covariance_weight: float = 0.04
29
+ effect_temperature: float = 0.07
30
+ temperature: float = 0.1
31
+
32
+
33
+ def _masked_mean(x: Tensor, mask: Tensor) -> Tensor:
34
+ weight = mask.unsqueeze(-1).to(x.dtype)
35
+ return (x * weight).sum(dim=1) / weight.sum(dim=1).clamp_min(1.0)
36
+
37
+
38
+ def _task_identity_clustering_loss(
39
+ retrieval: Tensor, valid: Tensor, semantic_ids: Tensor, temperature: float
40
+ ) -> Tensor:
41
+ """Multi-positive supervised contrastive loss; samples without a positive are ignored."""
42
+ z = F.normalize(_masked_mean(retrieval, valid), dim=-1)
43
+ logits = z @ z.T / temperature
44
+ logits.fill_diagonal_(-torch.inf)
45
+ positives = semantic_ids[:, None].eq(semantic_ids[None, :])
46
+ positives.fill_diagonal_(False)
47
+ usable = positives.any(dim=1)
48
+ if not bool(usable.any()):
49
+ return z.new_zeros(())
50
+ log_prob = logits - torch.logsumexp(logits, dim=1, keepdim=True)
51
+ return -(log_prob.masked_fill(~positives, 0.0).sum(dim=1) / positives.sum(dim=1).clamp_min(1))[usable].mean()
52
+
53
+
54
+ def _phase_progression_loss(phase: Tensor, valid: Tensor, margin: float = 0.01) -> Tensor:
55
+ """Encourage consecutive phase tokens to advance along one trajectory direction."""
56
+ losses: list[Tensor] = []
57
+ for sequence, mask in zip(phase, valid, strict=True):
58
+ z = sequence[mask]
59
+ if z.shape[0] < 3:
60
+ continue
61
+ direction = F.normalize(z[-1] - z[0], dim=-1)
62
+ increments = (z[1:] - z[:-1]) @ direction
63
+ losses.append(F.relu(margin - increments).mean())
64
+ return torch.stack(losses).mean() if losses else phase.new_zeros(())
65
+
66
+
67
+ def summarize_effect_window(actions: Tensor) -> Tensor:
68
+ """Summarize an executed low-rate effect window ``[B,W,K,4,A]``.
69
+
70
+ Keeping first, net displacement, mean and standard deviation makes this a
71
+ light action regularizer, not a loss that can be solved by copying the
72
+ current four-control input into the effect code.
73
+ """
74
+ if actions.ndim != 5:
75
+ raise ValueError("Effect actions must have shape [B,W,K,4,A].")
76
+ sequence = actions.flatten(-3, -2)
77
+ return torch.cat(
78
+ (
79
+ sequence[..., 0, :],
80
+ sequence[..., -1, :] - sequence[..., 0, :],
81
+ sequence.mean(dim=-2),
82
+ sequence.std(dim=-2, correction=0),
83
+ ),
84
+ dim=-1,
85
+ )
86
+
87
+
88
+ def _effect_nce(query: Tensor, target: Tensor, mask: Tensor, temperature: float) -> Tensor:
89
+ query, target = query[mask], target[mask]
90
+ if query.shape[0] < 2:
91
+ return query.new_zeros(())
92
+ logits = F.normalize(query, dim=-1) @ F.normalize(target, dim=-1).T / temperature
93
+ return F.cross_entropy(logits, torch.arange(logits.shape[0], device=logits.device))
94
+
95
+
96
+ def _vicreg(code: Tensor, mask: Tensor) -> tuple[Tensor, Tensor]:
97
+ code = code[mask]
98
+ if code.shape[0] < 2:
99
+ zero = code.new_zeros(())
100
+ return zero, zero
101
+ centered = code - code.mean(dim=0, keepdim=True)
102
+ std = (centered.var(dim=0, correction=0) + 1e-4).sqrt()
103
+ variance = F.relu(1.0 - std).mean()
104
+ covariance = centered.T @ centered / max(code.shape[0] - 1, 1)
105
+ covariance.fill_diagonal_(0)
106
+ covariance_loss = covariance.square().sum() / code.shape[-1]
107
+ return variance, covariance_loss
108
+
109
+
110
+ def causal_transition_encoder_loss(
111
+ outputs: dict[str, Tensor],
112
+ transition_actions: Tensor,
113
+ valid_mask: Tensor,
114
+ semantic_ids: Tensor,
115
+ cfg: CTELossConfig | None = None,
116
+ ) -> dict[str, Tensor]:
117
+ """CTE objective: causal effect, task identity, and phase progression."""
118
+ cfg = cfg or CTELossConfig()
119
+ transition_complete = outputs["transition_complete"]
120
+ if transition_complete.shape != transition_actions.shape[:2]:
121
+ raise ValueError("Effect outputs and transition-action cache are not temporally aligned.")
122
+ action_error = F.smooth_l1_loss(
123
+ outputs["next_action"], transition_actions, reduction="none"
124
+ ).mean(dim=(-1, -2))
125
+ action_loss = (action_error * transition_complete).sum() / transition_complete.sum().clamp_min(1)
126
+ target_vision = outputs["target_visual"]
127
+ vision_error = F.mse_loss(
128
+ outputs["next_vision"][:, :-1], target_vision[:, 1:], reduction="none"
129
+ ).mean(dim=-1)
130
+ vision_loss = (vision_error * transition_complete).sum() / transition_complete.sum().clamp_min(1)
131
+ task_loss = _task_identity_clustering_loss(outputs["retrieval"], valid_mask, semantic_ids, cfg.temperature)
132
+ phase_loss = _phase_progression_loss(outputs["phase"], valid_mask)
133
+ effect_mask = outputs["effect_complete"]
134
+ effect_target = outputs["effect_delta_target"]
135
+ effect_nce_pre = _effect_nce(outputs["effect_outcome_pre"], effect_target, effect_mask, cfg.effect_temperature)
136
+ effect_nce_post = _effect_nce(outputs["effect_outcome_post"], effect_target, effect_mask, cfg.effect_temperature)
137
+ effect_contrastive_loss = 0.5 * (effect_nce_pre + effect_nce_post)
138
+ # Diagnostic only: absolute deltas have a strong zero baseline, so this
139
+ # MSE is intentionally not optimized directly.
140
+ effect_visual_error = F.mse_loss(outputs["effect_outcome_pre"], effect_target, reduction="none").mean(dim=-1)
141
+ effect_visual_loss = (effect_visual_error * effect_mask).sum() / effect_mask.sum().clamp_min(1)
142
+ action_target = summarize_effect_window(outputs["effect_actions"])
143
+ effect_action_error = F.smooth_l1_loss(
144
+ outputs["effect_action"], action_target, reduction="none"
145
+ ).mean(dim=-1)
146
+ effect_action_loss = (effect_action_error * effect_mask).sum() / effect_mask.sum().clamp_min(1)
147
+ effect_align_error = 1.0 - (outputs["effect_pre"] * outputs["effect_post"]).sum(dim=-1)
148
+ effect_align_loss = (effect_align_error * effect_mask).sum() / effect_mask.sum().clamp_min(1)
149
+ effect_variance_loss, effect_covariance_loss = _vicreg(outputs["effect_post_raw"], effect_mask)
150
+ effect_loss = (
151
+ cfg.effect_contrastive_weight * effect_contrastive_loss
152
+ + cfg.effect_action_weight * effect_action_loss
153
+ + cfg.effect_align_weight * effect_align_loss
154
+ + cfg.effect_variance_weight * effect_variance_loss
155
+ + cfg.effect_covariance_weight * effect_covariance_loss
156
+ )
157
+ total = (
158
+ cfg.action_weight * action_loss
159
+ + cfg.vision_weight * vision_loss
160
+ + cfg.task_weight * task_loss
161
+ + cfg.phase_weight * phase_loss
162
+ + cfg.effect_weight * effect_loss
163
+ )
164
+ return {
165
+ "total": total,
166
+ "action": action_loss,
167
+ "vision": vision_loss,
168
+ "loss_task": task_loss,
169
+ "loss_phase": phase_loss,
170
+ "loss_effect": effect_loss,
171
+ "effect_contrastive": effect_contrastive_loss,
172
+ "effect_visual": effect_visual_loss,
173
+ "effect_action": effect_action_loss,
174
+ "effect_align": effect_align_loss,
175
+ "effect_variance": effect_variance_loss,
176
+ "effect_covariance": effect_covariance_loss,
177
+ }
releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Experimental Zeva components that are not part of the paper architecture."""
releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/robocasa_transition_memory.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RoboCasa Atomic-5 experimental transition-memory contract.
2
+
3
+ This module keeps the RoboCasa contract separate from the ARX defaults in
4
+ the generic transition-memory defaults. In particular, RoboCasa uses the
5
+ frozen Atomic-5 CTE,
6
+ 7-dimensional arm actions and the left-agent/wrist 20-Hz temporal contract.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .transition_memory import TransitionMemorySchema
12
+
13
+
14
+ ROBOCASA_ATOMIC5_TASKS = (
15
+ "OpenStandMixerHead",
16
+ "TurnOnElectricKettle",
17
+ "CloseToasterOvenDoor",
18
+ "TurnOnMicrowave",
19
+ "CoffeeSetupMug",
20
+ )
21
+
22
+
23
+ def make_robocasa_atomic5_schema(
24
+ *,
25
+ cte_hash: str = "",
26
+ vae_temporal_hash: str = "",
27
+ capacity: int = 64,
28
+ top_k: int = 4,
29
+ ) -> TransitionMemorySchema:
30
+ """Return the Atomic-5 transition-memory schema.
31
+
32
+ ``cte_hash`` and ``vae_temporal_hash`` are part of the schema hash. A
33
+ runtime/cache produced with a different CTE or temporal contract is
34
+ therefore rejected instead of silently mixing representations.
35
+ """
36
+
37
+ return TransitionMemorySchema(
38
+ version="robocasa_atomic5_transition_memory_v1",
39
+ task_contract="robocasa365_atomic5_5task_left_wrist_arm7_20hz_cosmos32_exec16",
40
+ phase_dim=128,
41
+ visual_key_dim=128,
42
+ effect_dim=128,
43
+ action_dim=7,
44
+ action_horizon=16,
45
+ capacity=int(capacity),
46
+ top_k=int(top_k),
47
+ cte_hash=str(cte_hash),
48
+ vae_temporal_hash=str(vae_temporal_hash),
49
+ )
releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Experimental transition memory retained for non-paper ablations.
2
+
3
+ The memory stores only completed 16-control transitions. It is deliberately
4
+ independent of the Cosmos model so that real-robot code can run it in shadow
5
+ mode before enabling the extra conditioning prefix.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import deque
11
+ from dataclasses import dataclass, field
12
+ import hashlib
13
+ import json
14
+ from typing import Iterable
15
+
16
+ import torch
17
+ from torch import nn
18
+ import torch.nn.functional as F
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TransitionMemorySchema:
23
+ """Dimensions and temporal contract used by one memory session."""
24
+
25
+ version: str = "arx_transition_memory_v1"
26
+ task_contract: str = "arx_task7_model_a_5task"
27
+ phase_dim: int = 128
28
+ visual_key_dim: int = 128
29
+ effect_dim: int = 128
30
+ action_dim: int = 14
31
+ action_horizon: int = 16
32
+ capacity: int = 64
33
+ top_k: int = 4
34
+ cte_hash: str = ""
35
+ vae_temporal_hash: str = ""
36
+
37
+ @property
38
+ def hash(self) -> str:
39
+ payload = json.dumps(self.__dict__, sort_keys=True, separators=(",", ":"))
40
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
41
+
42
+
43
+ @dataclass
44
+ class TransitionRecord:
45
+ """One causally completed replan transition."""
46
+
47
+ task_cluster: str
48
+ phase: torch.Tensor
49
+ visual_key: torch.Tensor
50
+ effect_post: torch.Tensor
51
+ executed_action: torch.Tensor
52
+ next_visual_key: torch.Tensor
53
+ next_phase: torch.Tensor
54
+ latent_index: int
55
+ schema_hash: str
56
+ valid: bool = True
57
+ metadata: dict[str, object] = field(default_factory=dict)
58
+
59
+ def cpu(self) -> "TransitionRecord":
60
+ """Detach tensors so the session never retains a training graph."""
61
+ return TransitionRecord(
62
+ task_cluster=self.task_cluster,
63
+ phase=self.phase.detach().float().cpu().contiguous(),
64
+ visual_key=self.visual_key.detach().float().cpu().contiguous(),
65
+ effect_post=self.effect_post.detach().float().cpu().contiguous(),
66
+ executed_action=self.executed_action.detach().float().cpu().contiguous(),
67
+ next_visual_key=self.next_visual_key.detach().float().cpu().contiguous(),
68
+ next_phase=self.next_phase.detach().float().cpu().contiguous(),
69
+ latent_index=int(self.latent_index),
70
+ schema_hash=self.schema_hash,
71
+ valid=bool(self.valid),
72
+ metadata=dict(self.metadata),
73
+ )
74
+
75
+
76
+ class TransitionMemory:
77
+ """Bounded FIFO memory scoped to one task session."""
78
+
79
+ def __init__(self, schema: TransitionMemorySchema | None = None) -> None:
80
+ self.schema = schema or TransitionMemorySchema()
81
+ self._task_cluster: str | None = None
82
+ self._entries: deque[TransitionRecord] = deque(maxlen=self.schema.capacity)
83
+
84
+ @property
85
+ def task_cluster(self) -> str | None:
86
+ return self._task_cluster
87
+
88
+ def __len__(self) -> int:
89
+ return len(self._entries)
90
+
91
+ @property
92
+ def entries(self) -> tuple[TransitionRecord, ...]:
93
+ """Return a stable, read-only snapshot for audit/finalization."""
94
+ return tuple(self._entries)
95
+
96
+ def reset(self, task_cluster: str | None = None) -> None:
97
+ self._entries.clear()
98
+ self._task_cluster = task_cluster
99
+
100
+ def _validate(self, transition: TransitionRecord) -> None:
101
+ if transition.schema_hash != self.schema.hash:
102
+ raise ValueError(
103
+ "transition memory schema mismatch: "
104
+ f"got {transition.schema_hash}, expected {self.schema.hash}"
105
+ )
106
+ if transition.phase.shape != (self.schema.phase_dim,):
107
+ raise ValueError(f"phase must be [{self.schema.phase_dim}], got {tuple(transition.phase.shape)}")
108
+ if transition.visual_key.shape != (self.schema.visual_key_dim,):
109
+ raise ValueError("visual_key has an invalid shape")
110
+ if transition.effect_post.shape != (self.schema.effect_dim,):
111
+ raise ValueError("effect_post has an invalid shape")
112
+ if transition.executed_action.shape != (self.schema.action_horizon, self.schema.action_dim):
113
+ raise ValueError("executed_action must contain exactly one completed 16-step window")
114
+ if transition.next_visual_key.shape != (self.schema.visual_key_dim,):
115
+ raise ValueError("next_visual_key has an invalid shape")
116
+ if transition.next_phase.shape != (self.schema.phase_dim,):
117
+ raise ValueError("next_phase has an invalid shape")
118
+ if not transition.valid:
119
+ raise ValueError("invalid or incomplete transitions must not enter transition memory")
120
+
121
+ def append(self, transition: TransitionRecord) -> None:
122
+ """Append one completed transition, resetting on a task change."""
123
+ self._validate(transition)
124
+ if self._task_cluster is None:
125
+ self._task_cluster = transition.task_cluster
126
+ if transition.task_cluster != self._task_cluster:
127
+ raise ValueError(
128
+ f"task mismatch: session={self._task_cluster!r}, transition={transition.task_cluster!r}; reset first"
129
+ )
130
+ self._entries.append(transition.cpu())
131
+
132
+ def annotate_attempt_outcome(
133
+ self,
134
+ attempt_id: int,
135
+ *,
136
+ outcome: str,
137
+ termination_reason: str,
138
+ total_steps: int,
139
+ final_progress: float,
140
+ ) -> int:
141
+ """Attach a terminal label to every transition from one attempt."""
142
+ if outcome not in {"success", "failure"}:
143
+ raise ValueError(f"unsupported attempt outcome: {outcome!r}")
144
+ if termination_reason not in {"success", "env_done", "max_steps", "client_error", "safety"}:
145
+ raise ValueError(f"unsupported termination reason: {termination_reason!r}")
146
+ if attempt_id < 0 or total_steps < 0:
147
+ raise ValueError("attempt_id and total_steps must be non-negative")
148
+ if not 0.0 <= float(final_progress) <= 1.0:
149
+ raise ValueError("final_progress must be in [0,1]")
150
+ updated = 0
151
+ for entry in self._entries:
152
+ if int(entry.metadata.get("attempt_id", -1)) != attempt_id:
153
+ continue
154
+ entry.metadata.update(
155
+ {
156
+ "terminal_outcome": outcome,
157
+ "termination_reason": termination_reason,
158
+ "total_steps": int(total_steps),
159
+ "final_progress": float(final_progress),
160
+ "progress_source": "success_only",
161
+ }
162
+ )
163
+ updated += 1
164
+ return updated
165
+
166
+ def query(
167
+ self,
168
+ phase: torch.Tensor,
169
+ visual_key: torch.Tensor,
170
+ *,
171
+ top_k: int | None = None,
172
+ ) -> tuple[list[TransitionRecord], torch.Tensor]:
173
+ """Return cosine-ranked history entries and scores."""
174
+ if phase.shape != (self.schema.phase_dim,) or visual_key.shape != (self.schema.visual_key_dim,):
175
+ raise ValueError("query phase/visual_key shape mismatch")
176
+ if not self._entries:
177
+ return [], torch.empty(0, dtype=torch.float32)
178
+ k = min(int(top_k or self.schema.top_k), len(self._entries))
179
+ q = F.normalize(torch.cat((phase.float(), visual_key.float())), dim=0)
180
+ keys = torch.stack([
181
+ F.normalize(torch.cat((entry.phase, entry.visual_key)), dim=0)
182
+ for entry in self._entries
183
+ ])
184
+ scores = keys @ q
185
+ values, indices = torch.topk(scores, k=k, largest=True, sorted=True)
186
+ entries = [list(self._entries)[int(index)] for index in indices]
187
+ return entries, values
188
+
189
+
190
+ class TransitionMemoryEncoder(nn.Module):
191
+ """Encode top-k transitions into one 256-D causal context token."""
192
+
193
+ def __init__(self, schema: TransitionMemorySchema | None = None, hidden_dim: int = 256) -> None:
194
+ super().__init__()
195
+ self.schema = schema or TransitionMemorySchema()
196
+ self.hidden_dim = hidden_dim
197
+ value_dim = (
198
+ self.schema.phase_dim
199
+ + self.schema.visual_key_dim
200
+ + self.schema.effect_dim
201
+ + self.schema.action_horizon * self.schema.action_dim
202
+ + self.schema.visual_key_dim
203
+ + self.schema.phase_dim
204
+ )
205
+ query_dim = self.schema.phase_dim + self.schema.visual_key_dim
206
+ self.query_proj = nn.Sequential(nn.Linear(query_dim, hidden_dim), nn.SiLU(), nn.Linear(hidden_dim, hidden_dim))
207
+ self.key_proj = nn.Sequential(nn.Linear(query_dim, hidden_dim), nn.SiLU(), nn.Linear(hidden_dim, hidden_dim))
208
+ self.value_proj = nn.Sequential(nn.Linear(value_dim, hidden_dim), nn.SiLU(), nn.Linear(hidden_dim, hidden_dim))
209
+ self.output = nn.Sequential(nn.LayerNorm(hidden_dim), nn.Linear(hidden_dim, hidden_dim), nn.SiLU())
210
+ self.bos_online = nn.Parameter(torch.zeros(hidden_dim))
211
+ nn.init.normal_(self.bos_online, std=0.02)
212
+
213
+ def forward(
214
+ self,
215
+ phase: torch.Tensor,
216
+ visual_key: torch.Tensor,
217
+ entries: Iterable[TransitionRecord] | None = None,
218
+ ) -> torch.Tensor:
219
+ """Return ``[B, hidden_dim]`` or ``[hidden_dim]`` for one query."""
220
+ if phase.ndim == 1:
221
+ phase = phase.unsqueeze(0)
222
+ if visual_key.ndim == 1:
223
+ visual_key = visual_key.unsqueeze(0)
224
+ if phase.shape[0] != 1 or visual_key.shape[0] != 1:
225
+ raise ValueError("TransitionMemoryEncoder currently accepts one query at a time")
226
+ device = phase.device
227
+ query = torch.cat((phase.float(), visual_key.float()), dim=-1)
228
+ q = self.query_proj(query)
229
+ items = list(entries or [])[: self.schema.top_k]
230
+ if not items:
231
+ return self.output(q + self.bos_online.to(q)).to(phase.dtype)
232
+ keys = torch.stack([torch.cat((item.phase, item.visual_key)) for item in items]).to(device=device)
233
+ values = torch.stack([
234
+ torch.cat((
235
+ item.phase,
236
+ item.visual_key,
237
+ item.effect_post,
238
+ item.executed_action.reshape(-1),
239
+ item.next_visual_key,
240
+ item.next_phase,
241
+ ))
242
+ for item in items
243
+ ]).to(device=device)
244
+ attention = torch.softmax(F.normalize(self.key_proj(keys), dim=-1) @ F.normalize(q, dim=-1).squeeze(0), dim=0)
245
+ context = (attention.unsqueeze(-1) * self.value_proj(values)).sum(dim=0, keepdim=True)
246
+ return self.output(q + context).to(phase.dtype)
releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_policy_adapter.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Learned action residual projector for the Cosmos transition-memory prefix.
2
+
3
+ This module is intentionally tiny and dependency-light so the same class can
4
+ be copied into the remote Cosmos framework checkout. It is not a selector:
5
+ it receives only the server-produced action chunk, the current phase/visual
6
+ features and the learned transition-memory context, and emits a bounded residual.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import torch
12
+ from torch import Tensor, nn
13
+
14
+
15
+ ACTION_PROJECTOR_FORMAT = "robocasa_atomic5_transition_memory_action_projector_v1"
16
+
17
+
18
+ class TransitionMemoryPolicyAdapter(nn.Module):
19
+ """Predict a bounded 32x7 residual from the learned memory context."""
20
+
21
+ def __init__(
22
+ self,
23
+ *,
24
+ context_dim: int = 256,
25
+ phase_dim: int = 128,
26
+ visual_dim: int = 128,
27
+ action_horizon: int = 32,
28
+ action_dim: int = 7,
29
+ hidden_dim: int = 256,
30
+ residual_scale: float = 0.5,
31
+ effective_horizon: int = 16,
32
+ residual_scale_max: float | None = None,
33
+ ) -> None:
34
+ super().__init__()
35
+ if action_horizon != 32 or action_dim != 7:
36
+ raise ValueError("Atomic-5 action projector requires horizon=32 and action_dim=7")
37
+ if effective_horizon < 1 or effective_horizon > action_horizon:
38
+ raise ValueError("effective_horizon must be in [1, action_horizon]")
39
+ if residual_scale_max is None:
40
+ residual_scale_max = float(residual_scale)
41
+ if not torch.isfinite(torch.tensor(float(residual_scale_max))):
42
+ raise ValueError("residual_scale_max must be finite")
43
+ if float(residual_scale_max) <= 0.0:
44
+ raise ValueError("residual_scale_max must be positive")
45
+ if not torch.isfinite(torch.tensor(float(residual_scale))):
46
+ raise ValueError("residual_scale must be finite")
47
+ if float(residual_scale) < 0.0 or float(residual_scale) > float(residual_scale_max):
48
+ raise ValueError("residual_scale must lie in [0, residual_scale_max]")
49
+ self.context_dim = int(context_dim)
50
+ self.phase_dim = int(phase_dim)
51
+ self.visual_dim = int(visual_dim)
52
+ self.action_horizon = int(action_horizon)
53
+ self.action_dim = int(action_dim)
54
+ self.hidden_dim = int(hidden_dim)
55
+ self.residual_scale = float(residual_scale)
56
+ self.residual_scale_max = float(residual_scale_max)
57
+ self.effective_horizon = int(effective_horizon)
58
+ input_dim = context_dim + phase_dim + visual_dim + action_horizon * action_dim
59
+ self.body = nn.Sequential(
60
+ nn.Linear(input_dim, hidden_dim),
61
+ nn.SiLU(),
62
+ nn.Linear(hidden_dim, hidden_dim),
63
+ nn.SiLU(),
64
+ )
65
+ self.residual_head = nn.Linear(hidden_dim, action_horizon * action_dim)
66
+
67
+ def forward(
68
+ self,
69
+ context: Tensor,
70
+ phase: Tensor,
71
+ visual_key: Tensor,
72
+ baseline_actions: Tensor,
73
+ ) -> tuple[Tensor, Tensor]:
74
+ if context.ndim == 1:
75
+ context = context.unsqueeze(0)
76
+ if phase.ndim == 1:
77
+ phase = phase.unsqueeze(0)
78
+ if visual_key.ndim == 1:
79
+ visual_key = visual_key.unsqueeze(0)
80
+ if baseline_actions.ndim != 3:
81
+ raise ValueError("baseline_actions must be [B,32,7]")
82
+ expected = (self.action_horizon, self.action_dim)
83
+ if tuple(baseline_actions.shape[1:]) != expected:
84
+ raise ValueError(f"baseline_actions must be [B,{expected[0]},{expected[1]}]")
85
+ if context.shape[0] != baseline_actions.shape[0] or phase.shape[0] != baseline_actions.shape[0] or visual_key.shape[0] != baseline_actions.shape[0]:
86
+ raise ValueError("projector batch dimensions do not match")
87
+ # The evaluator executes only the first ``effective_horizon`` controls
88
+ # of a 32-action prediction. Do not let the unexecuted suffix affect
89
+ # the learned correction: it is often stochastic padding/noise.
90
+ baseline_for_features = baseline_actions.float()
91
+ if self.effective_horizon < self.action_horizon:
92
+ mask = torch.zeros_like(baseline_for_features)
93
+ mask[:, : self.effective_horizon] = 1.0
94
+ baseline_for_features = baseline_for_features * mask
95
+ features = torch.cat(
96
+ (context.float(), phase.float(), visual_key.float(), baseline_for_features.reshape(baseline_actions.shape[0], -1)),
97
+ dim=-1,
98
+ )
99
+ residual = torch.tanh(self.residual_head(self.body(features))).reshape(-1, *expected)
100
+ if self.effective_horizon < self.action_horizon:
101
+ mask = torch.zeros_like(residual)
102
+ mask[:, : self.effective_horizon] = 1.0
103
+ residual = residual * mask
104
+ residual = residual * self.residual_scale
105
+ return baseline_actions + residual, residual
106
+
107
+ def set_residual_scale(self, value: float) -> None:
108
+ """Set a calibrated bounded residual scale without changing weights."""
109
+ value = float(value)
110
+ if not torch.isfinite(torch.tensor(value)):
111
+ raise ValueError("residual scale must be finite")
112
+ if value < 0.0 or value > self.residual_scale_max:
113
+ raise ValueError(
114
+ f"residual scale {value} is outside [0, {self.residual_scale_max}]"
115
+ )
116
+ self.residual_scale = value
releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_runtime.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime glue for the experimental task-session transition memory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import torch
8
+
9
+ from .transition_memory import TransitionMemory, TransitionMemoryEncoder, TransitionMemorySchema, TransitionRecord
10
+
11
+
12
+ @dataclass
13
+ class TransitionMemoryReadout:
14
+ context: torch.Tensor
15
+ scores: torch.Tensor
16
+ num_entries: int
17
+
18
+
19
+ class TransitionMemoryController:
20
+ """Owns task reset, causal writes, and context reads for one policy server."""
21
+
22
+ def __init__(
23
+ self,
24
+ schema: TransitionMemorySchema | None = None,
25
+ *,
26
+ encoder: TransitionMemoryEncoder | None = None,
27
+ enabled: bool = False,
28
+ ) -> None:
29
+ self.schema = schema or TransitionMemorySchema()
30
+ self.memory = TransitionMemory(self.schema)
31
+ self.encoder = encoder or TransitionMemoryEncoder(self.schema)
32
+ self.enabled = bool(enabled)
33
+ self._replan_open = False
34
+
35
+ def reset(self, task_cluster: str) -> None:
36
+ self.memory.reset(task_cluster)
37
+ self._replan_open = False
38
+
39
+ def begin_replan(self) -> None:
40
+ if self.memory.task_cluster is None:
41
+ raise RuntimeError("reset(task_cluster) is required before begin_replan")
42
+ if self._replan_open:
43
+ raise RuntimeError("previous replan is still open")
44
+ self._replan_open = True
45
+
46
+ def discard_open_replan(self) -> bool:
47
+ """Discard a terminal partial window without writing it to memory."""
48
+ was_open = self._replan_open
49
+ self._replan_open = False
50
+ return was_open
51
+
52
+ @torch.inference_mode()
53
+ def read(self, phase: torch.Tensor, visual_key: torch.Tensor) -> TransitionMemoryReadout:
54
+ """Read only transitions written before this replan."""
55
+ if not self._replan_open:
56
+ raise RuntimeError("begin_replan() must precede an transition-memory read")
57
+ entries, scores = self.memory.query(phase.detach().cpu(), visual_key.detach().cpu())
58
+ if self.enabled:
59
+ context = self.encoder(phase, visual_key, entries)
60
+ else:
61
+ context = phase.new_zeros((1, self.encoder.hidden_dim))
62
+ return TransitionMemoryReadout(context=context, scores=scores, num_entries=len(entries))
63
+
64
+ def complete_replan(
65
+ self,
66
+ *,
67
+ phase: torch.Tensor,
68
+ visual_key: torch.Tensor,
69
+ effect_post: torch.Tensor,
70
+ executed_action: torch.Tensor,
71
+ next_visual_key: torch.Tensor,
72
+ next_phase: torch.Tensor,
73
+ latent_index: int,
74
+ completed: bool,
75
+ metadata: dict[str, object] | None = None,
76
+ ) -> bool:
77
+ """Commit exactly one completed 16-step transition, or discard it."""
78
+ if not self._replan_open:
79
+ raise RuntimeError("begin_replan() must precede complete_replan()")
80
+ self._replan_open = False
81
+ if not completed:
82
+ return False
83
+ if executed_action.shape != (self.schema.action_horizon, self.schema.action_dim):
84
+ raise ValueError("only a completed 16-step action window may enter transition memory")
85
+ assert self.memory.task_cluster is not None
86
+ self.memory.append(
87
+ TransitionRecord(
88
+ task_cluster=self.memory.task_cluster,
89
+ phase=phase,
90
+ visual_key=visual_key,
91
+ effect_post=effect_post,
92
+ executed_action=executed_action,
93
+ next_visual_key=next_visual_key,
94
+ next_phase=next_phase,
95
+ latent_index=int(latent_index),
96
+ schema_hash=self.schema.hash,
97
+ metadata=dict(metadata or {}),
98
+ )
99
+ )
100
+ return True
releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_test.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ import torch
5
+
6
+ from cosmos_framework.model.zeva.experimental.transition_memory import (
7
+ TransitionMemory,
8
+ TransitionMemoryEncoder,
9
+ TransitionMemorySchema,
10
+ TransitionRecord,
11
+ )
12
+ from cosmos_framework.model.zeva.experimental.transition_memory_runtime import TransitionMemoryController
13
+ from cosmos_framework.model.zeva.experimental.robocasa_transition_memory import make_robocasa_atomic5_schema
14
+
15
+
16
+ def make_transition(schema: TransitionMemorySchema, task: str, i: int, *, valid: bool = True) -> TransitionRecord:
17
+ return TransitionRecord(
18
+ task_cluster=task,
19
+ phase=torch.full((schema.phase_dim,), float(i)),
20
+ visual_key=torch.full((schema.visual_key_dim,), float(i + 1)),
21
+ effect_post=torch.full((schema.effect_dim,), float(i + 2)),
22
+ executed_action=torch.full((schema.action_horizon, schema.action_dim), float(i + 3)),
23
+ next_visual_key=torch.full((schema.visual_key_dim,), float(i + 4)),
24
+ next_phase=torch.full((schema.phase_dim,), float(i + 5)),
25
+ latent_index=4 * i,
26
+ schema_hash=schema.hash,
27
+ valid=valid,
28
+ )
29
+
30
+
31
+ def test_fifo_reset_and_task_isolation() -> None:
32
+ schema = TransitionMemorySchema(capacity=2)
33
+ memory = TransitionMemory(schema)
34
+ memory.append(make_transition(schema, "pick", 0))
35
+ memory.append(make_transition(schema, "pick", 1))
36
+ memory.append(make_transition(schema, "pick", 2))
37
+ assert len(memory) == 2
38
+ with pytest.raises(ValueError, match="task mismatch"):
39
+ memory.append(make_transition(schema, "pour", 3))
40
+ memory.reset("pour")
41
+ memory.append(make_transition(schema, "pour", 3))
42
+ assert len(memory) == 1
43
+ assert memory.task_cluster == "pour"
44
+
45
+
46
+ def test_schema_and_incomplete_transition_fail_fast() -> None:
47
+ schema = TransitionMemorySchema()
48
+ memory = TransitionMemory(schema)
49
+ bad_hash = make_transition(schema, "pick", 0)
50
+ bad_hash.schema_hash = "wrong"
51
+ with pytest.raises(ValueError, match="schema mismatch"):
52
+ memory.append(bad_hash)
53
+ with pytest.raises(ValueError, match="invalid or incomplete"):
54
+ memory.append(make_transition(schema, "pick", 0, valid=False))
55
+
56
+
57
+ def test_query_is_bounded_and_encoder_handles_empty_memory() -> None:
58
+ schema = TransitionMemorySchema(capacity=64, top_k=4)
59
+ memory = TransitionMemory(schema)
60
+ for i in range(10):
61
+ memory.append(make_transition(schema, "pick", i))
62
+ entries, scores = memory.query(torch.zeros(schema.phase_dim), torch.zeros(schema.visual_key_dim))
63
+ assert len(entries) == 4
64
+ assert scores.shape == (4,)
65
+ encoder = TransitionMemoryEncoder(schema)
66
+ context = encoder(torch.zeros(schema.phase_dim), torch.zeros(schema.visual_key_dim), entries)
67
+ empty_context = encoder(torch.zeros(schema.phase_dim), torch.zeros(schema.visual_key_dim), [])
68
+ assert context.shape == (1, 256)
69
+ assert empty_context.shape == (1, 256)
70
+ assert torch.isfinite(context).all()
71
+
72
+
73
+ def test_controller_does_not_commit_incomplete_replan() -> None:
74
+ schema = TransitionMemorySchema()
75
+ controller = TransitionMemoryController(schema, enabled=True)
76
+ controller.reset("pick")
77
+ controller.begin_replan()
78
+ readout = controller.read(torch.zeros(schema.phase_dim), torch.zeros(schema.visual_key_dim))
79
+ assert readout.num_entries == 0
80
+ assert not controller.complete_replan(
81
+ phase=torch.zeros(schema.phase_dim),
82
+ visual_key=torch.zeros(schema.visual_key_dim),
83
+ effect_post=torch.zeros(schema.effect_dim),
84
+ executed_action=torch.zeros(schema.action_horizon, schema.action_dim),
85
+ next_visual_key=torch.zeros(schema.visual_key_dim),
86
+ next_phase=torch.zeros(schema.phase_dim),
87
+ latent_index=0,
88
+ completed=False,
89
+ )
90
+ assert len(controller.memory) == 0
91
+
92
+
93
+ def test_robocasa_atomic5_contract_is_arm7_and_temporally_bounded() -> None:
94
+ schema = make_robocasa_atomic5_schema(cte_hash="cte", vae_temporal_hash="vae")
95
+ assert schema.action_dim == 7
96
+ assert schema.action_horizon == 16
97
+ assert schema.capacity == 64
98
+ assert schema.top_k == 4
99
+ # The provenance fields participate in the hash; a cache from another
100
+ # A different CTE/VAE contract cannot be accepted by the runtime.
101
+ assert schema.hash != make_robocasa_atomic5_schema(cte_hash="other", vae_temporal_hash="vae").hash
102
+
103
+
104
+ def test_robocasa_controller_writes_only_after_completed_window() -> None:
105
+ schema = make_robocasa_atomic5_schema()
106
+ controller = TransitionMemoryController(schema, enabled=True)
107
+ controller.reset("OpenStandMixerHead")
108
+ def zeros(shape: tuple[int, ...]) -> torch.Tensor:
109
+ return torch.zeros(shape)
110
+ controller.begin_replan()
111
+ controller.read(zeros((128,)), zeros((128,)))
112
+ assert not controller.complete_replan(
113
+ phase=zeros((128,)), visual_key=zeros((128,)), effect_post=zeros((128,)),
114
+ executed_action=zeros((16, 7)), next_visual_key=zeros((128,)),
115
+ next_phase=zeros((128,)), latent_index=0, completed=False,
116
+ )
117
+ assert len(controller.memory) == 0
118
+ controller.begin_replan()
119
+ controller.read(zeros((128,)), zeros((128,)))
120
+ assert controller.complete_replan(
121
+ phase=zeros((128,)), visual_key=zeros((128,)), effect_post=zeros((128,)),
122
+ executed_action=zeros((16, 7)), next_visual_key=zeros((128,)),
123
+ next_phase=zeros((128,)), latent_index=0, completed=True,
124
+ )
125
+ assert len(controller.memory) == 1
126
+
127
+
128
+ def test_attempt_outcome_annotation_is_scoped_and_auditable() -> None:
129
+ schema = make_robocasa_atomic5_schema()
130
+ memory = TransitionMemory(schema)
131
+ first = make_transition(schema, "OpenStandMixerHead", 0)
132
+ first.metadata = {"attempt_id": 0, "session_id": "mixer:195", "environment_seed": 195}
133
+ second = make_transition(schema, "OpenStandMixerHead", 1)
134
+ second.metadata = {"attempt_id": 1, "session_id": "mixer:195", "environment_seed": 195}
135
+ memory.append(first)
136
+ memory.append(second)
137
+ updated = memory.annotate_attempt_outcome(
138
+ 0,
139
+ outcome="failure",
140
+ termination_reason="max_steps",
141
+ total_steps=300,
142
+ final_progress=0.0,
143
+ )
144
+ assert updated == 1
145
+ assert memory.entries[0].metadata["terminal_outcome"] == "failure"
146
+ assert "terminal_outcome" not in memory.entries[1].metadata
147
+
148
+
149
+ def test_discard_open_replan_never_commits_partial_window() -> None:
150
+ schema = make_robocasa_atomic5_schema()
151
+ controller = TransitionMemoryController(schema, enabled=False)
152
+ controller.reset("OpenStandMixerHead")
153
+ controller.begin_replan()
154
+ assert controller.discard_open_replan()
155
+ assert len(controller.memory) == 0
156
+ assert not controller.discard_open_replan()
157
+
158
+
159
+ expected = {(95, frame) for frame in range(85)}
160
+ assert (95, 83) in expected
161
+ assert (93, 83) not in expected