Add Bridge native Stage2 training source; GPU validation NOT_RUN
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- releases/bridge-native-v3/README.md +160 -0
- releases/bridge-native-v3/code/ATTRIBUTIONS.md +0 -0
- releases/bridge-native-v3/code/LICENSE +52 -0
- releases/bridge-native-v3/code/NOTICE +54 -0
- releases/bridge-native-v3/code/README.md +142 -0
- releases/bridge-native-v3/code/configs/bridge_stage2_native_contract.json +47 -0
- releases/bridge-native-v3/code/cosmos_framework/__init__.py +3 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/config.py +109 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/defaults/model_config.py +380 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/defaults/vlm.py +17 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva.py +65 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_pim.py +61 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_pim_inference.py +36 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_test.py +14 -0
- releases/bridge-native-v3/code/cosmos_framework/configs/base/experiment/action/posttrain_config/action_policy_robocasa365_atomic5_zeva_transition_memory.py +66 -0
- releases/bridge-native-v3/code/cosmos_framework/data/__init__.py +2 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/__init__.py +2 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/action/__init__.py +2 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/__init__.py +15 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/robocasa365_lerobot_dataset.py +334 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/action/datasets/widowx_bridge_v3_dataset.py +163 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/action/domain_utils.py +83 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/action/normalizer_stats/robocasa365_target_action_stats.json +11 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/action/transforms.py +710 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/joint_dataloader.py +1271 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/modality.py +256 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/packers.py +446 -0
- releases/bridge-native-v3/code/cosmos_framework/data/generator/sequence_packing/sequence.py +1203 -0
- releases/bridge-native-v3/code/cosmos_framework/inference/common/public_model_config.py +361 -0
- releases/bridge-native-v3/code/cosmos_framework/model/generator/mot/cosmos3_vfm_network.py +1536 -0
- releases/bridge-native-v3/code/cosmos_framework/model/generator/omni_mot_model.py +0 -0
- releases/bridge-native-v3/code/cosmos_framework/model/generator/utils/data_and_condition.py +182 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/__init__.py +52 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_artifacts.py +77 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_artifacts_test.py +31 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_protocol.py +78 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/attempt_protocol_test.py +45 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/brief_interaction_trace.py +24 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_prompt.py +12 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_transition_encoder.py +374 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/causal_transition_encoder_test.py +28 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/checkpoint_io.py +22 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/checkpoint_io_test.py +32 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/cte_losses.py +177 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/__init__.py +1 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/robocasa_transition_memory.py +49 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory.py +246 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_policy_adapter.py +116 -0
- releases/bridge-native-v3/code/cosmos_framework/model/zeva/experimental/transition_memory_runtime.py +100 -0
- 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 |
+

|
| 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
|