Add source_code/ training snapshot + depth/ray provenance + reference distributions
Browse files- pibehavior_da3_clean_up_your_desk_40k/source_code/README.md +107 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/da3/da3_for_geostack.py +639 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/launch/launch_b1k_robopro.sh +45 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/model/observation.py +168 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/model/pi_behavior.py +1270 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/model/pi_behavior_config.py +234 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/model/spatial_da3.py +266 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/data_loader.py +676 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/gemma.py +613 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/optimizer.py +176 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/sharding.py +114 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/weight_loaders.py +110 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/training/_da3_contrib_check.py +63 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/training/b1k_2026.py +414 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/training/b1k_da3.py +307 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/training/da3_extractor.py +212 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/training/train.py +489 -0
- pibehavior_da3_clean_up_your_desk_40k/source_code/training/train_2026.py +135 -0
pibehavior_da3_clean_up_your_desk_40k/source_code/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# source_code — training snapshot for `pibehavior_da3_clean_up_your_desk_40k`
|
| 2 |
+
|
| 3 |
+
Exact source used to produce the checkpoint, so eval doesn't have to reconstruct architecture from
|
| 4 |
+
Orbax metadata. **Read the "Answers" section first — it settles the train/eval mismatch questions.**
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## Answers to the questions that matter
|
| 9 |
+
|
| 10 |
+
### 1. Where do `da3_depth` and `da3_ray` come from? (the critical one)
|
| 11 |
+
**Both are DA3 model predictions. Neither comes from the simulator.**
|
| 12 |
+
|
| 13 |
+
- **`da3_depth` = DA3's own predicted METRIC depth.** From the Nested-GIANT `da3_metric` branch
|
| 14 |
+
(`out["depth"]`, `out["is_metric"]`) — see `da3/da3_for_geostack.py`.
|
| 15 |
+
- **`da3_ray` = DA3's ray-head output**, captured by a forward hook on the final aux conv of DA3's
|
| 16 |
+
DualDPT. Direction-only, unit-normalized (verified: per-pixel L2 norm = 1.0000).
|
| 17 |
+
- **The simulator's `depth_linear` is NEVER fed to the model.** It was used *only offline*, to
|
| 18 |
+
calibrate the extrinsics convention (that is how the GL2CV flip + wxyz + camera-in-robot ordering
|
| 19 |
+
were derived). **Do not feed `depth_linear` at eval** — run the same DA3 forward and use its outputs.
|
| 20 |
+
- `use_ray_pose=False`: the extrinsics/intrinsics drive the **camera encoder (`cam_enc`)** that
|
| 21 |
+
conditions the features, *not* the ray head. This is why wrong poses corrupt the **features**
|
| 22 |
+
themselves rather than merely an auxiliary channel.
|
| 23 |
+
|
| 24 |
+
⚠️ **Silent-zeros trap.** `da3_extractor.py` contains:
|
| 25 |
+
```python
|
| 26 |
+
depth = out["depth"]
|
| 27 |
+
if depth is None:
|
| 28 |
+
depth = torch.zeros(...) # silently all-zeros
|
| 29 |
+
```
|
| 30 |
+
If your wrapper fails to surface depth you get **zeros with no error**. We verified this fallback was
|
| 31 |
+
**NOT** active in training (`ALL_ZERO=False`, `frac_zero=0.0000`). Assert on this at eval.
|
| 32 |
+
|
| 33 |
+
### 2. Reference distributions — validate your eval against these
|
| 34 |
+
Measured on **real b1k frames with real `robot2cam_pose`**, batch of 4, `clean_up_your_desk`:
|
| 35 |
+
|
| 36 |
+
```
|
| 37 |
+
da3_features (B,4,3,1536,18,18) uint16 # uint16 = bf16 BITS, not floats
|
| 38 |
+
da3_ray (B,3,3,18,18) float32 L2 norm per pixel = 1.0000
|
| 39 |
+
da3_depth (B,3,1,18,18) float32
|
| 40 |
+
min=0.2110 max=2.0765 mean=0.9492 std=0.3715
|
| 41 |
+
p1=0.309 p25=0.617 p50=0.979 p75=1.244 p99=1.782
|
| 42 |
+
per-view depth mean: head 1.310 (0.81–2.08) | left_wrist 0.723 (0.21–1.48) | right_wrist 0.815 (0.37–1.59)
|
| 43 |
+
```
|
| 44 |
+
**Sanity checks for your eval:**
|
| 45 |
+
- depth must be **non-zero** and roughly **0.2–2.1 m**
|
| 46 |
+
- **head depth > wrist depth** (head views the scene; wrists sit near manipulated objects).
|
| 47 |
+
If this ordering inverts, your **view order is wrong** (must be `head, left_wrist, right_wrist`).
|
| 48 |
+
- ray L2 norm must be **1.0**. If not, you're feeding the wrong tensor.
|
| 49 |
+
- On structureless input DA3 degenerates to near-constant depth (~0.93, std 0.006) — if you see that
|
| 50 |
+
on real frames, images aren't reaching the extractor.
|
| 51 |
+
|
| 52 |
+
### 3. Bank tokens are 320 = 128 + 96 + 96
|
| 53 |
+
`pi_behavior_config.py`: `spatial_num_tokens: int = 320 # DA3 perc bank default: 128 + 96 + 96`.
|
| 54 |
+
(Not 32/24/24.)
|
| 55 |
+
|
| 56 |
+
### 4. Feature wire format
|
| 57 |
+
bf16 bit-packed into uint16 (torch and JAX share no bf16 interchange dtype):
|
| 58 |
+
```python
|
| 59 |
+
# torch: feats.to(torch.bfloat16).view(torch.uint16)
|
| 60 |
+
# jax: jax.lax.bitcast_convert_type(feats, jnp.bfloat16)
|
| 61 |
+
```
|
| 62 |
+
**Do not cast to float32** — the model branches on dtype (see `PiBehavior._compute_banks`).
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## Contents
|
| 67 |
+
|
| 68 |
+
```
|
| 69 |
+
model/ pi_behavior.py policy + _compute_banks + spatial action conditioning
|
| 70 |
+
pi_behavior_config.py B1KDA3Config (spatial_num_tokens=320, init_std, logit_gain...)
|
| 71 |
+
spatial_da3.py bank builder + cross-attention injection
|
| 72 |
+
observation.py Observation dataclass (da3_features / da3_ray / da3_depth fields)
|
| 73 |
+
training/ da3_extractor.py DA3InlineExtractor <-- produces the DA3 tensors
|
| 74 |
+
b1k_da3.py dataset + da3_fields() (extrinsics/intrinsics) + batch_transform
|
| 75 |
+
b1k_2026.py BehaviorV3Dataset (video decode)
|
| 76 |
+
train.py multi-group LR (vlm/core/geom) + train step
|
| 77 |
+
train_2026.py entrypoint / env -> config
|
| 78 |
+
_da3_contrib_check.py bank-ablation sanity check (RUN THIS, see below)
|
| 79 |
+
openpi/ gemma.py spatial injection into last 6 of 18 action-expert layers
|
| 80 |
+
data_loader.py collator + DLPack/async staging
|
| 81 |
+
optimizer.py LR schedules
|
| 82 |
+
sharding.py B1K_HOLDOUT_GPU0 mesh holdout
|
| 83 |
+
weight_loaders.py PiBehaviorWeightLoader (param key merge)
|
| 84 |
+
da3/ da3_for_geostack.py DA3 wrapper: forward_multi_view, ray hook, depth surfacing
|
| 85 |
+
launch/ launch_b1k_robopro.sh EXACT env/hyperparameters that produced this checkpoint
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
**DA3 upstream:** `Depth-Anything-3` @ commit **`4173623`**, sources at `<repo>/src`, loaded via
|
| 89 |
+
`_DA3_SRC` in `da3_extractor.py`. Weights: `depth-anything/DA3NESTED-GIANT-LARGE-1.1`,
|
| 90 |
+
`out_layers=(19,26,33,39)`, `patch_size=14`, `use_bf16=True`, frozen (`no_grad()` + `eval()`).
|
| 91 |
+
|
| 92 |
+
## Verify your integration before trusting results
|
| 93 |
+
Run the **bank ablation** (`training/_da3_contrib_check.py`): compute action loss normally, then with
|
| 94 |
+
the spatial banks zeroed. On this checkpoint the gap is large (at 14k it was 0.301 → 4.300, **+1329%**).
|
| 95 |
+
**A near-zero gap means the spatial branch isn't actually engaged** — and note the model *silently
|
| 96 |
+
falls back to base behavior* when `da3_features` is `None`, so a wiring bug looks like "DA3 doesn't
|
| 97 |
+
help", not like a crash.
|
| 98 |
+
|
| 99 |
+
## Training configuration (as shipped in `launch/`)
|
| 100 |
+
```
|
| 101 |
+
BS=128 FLOW=15 STEPS=50000 warmup=1000 decay=50000
|
| 102 |
+
LR: vlm 2.5e-5->2.5e-6 | core 5e-4->5e-5 | geom 5e-4->1e-4 (DA3_LR_GROUPS=1)
|
| 103 |
+
DA3: spatial_init_std=0.01 attn_logit_gain=1 (init 32.0) spatial_scale=2.0
|
| 104 |
+
da3_hw=(252,252) -> 18x18 grid; VLM images 224x224; views: head, left_wrist, right_wrist
|
| 105 |
+
```
|
| 106 |
+
`spatial_init_std` is deliberately **non-zero**: with zero-init the model learned to *ignore* the
|
| 107 |
+
spatial branch entirely.
|
pibehavior_da3_clean_up_your_desk_40k/source_code/da3/da3_for_geostack.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX: same as DA3-XVLA repo
|
| 2 |
+
"""
|
| 3 |
+
DA3-Large frozen feature extractor for GeoStack-XVLA v2B.
|
| 4 |
+
|
| 5 |
+
This wrapper around `depth_anything_3.api.DepthAnything3`:
|
| 6 |
+
* Loads DA3-Large-1.1 and freezes ALL parameters.
|
| 7 |
+
* Receives ImageNet-normalized RGB at any input dimensions (typically the
|
| 8 |
+
already-square 224×224 that the X-VLA processor produces for Florence).
|
| 9 |
+
* Bilinearly resizes to the configured (da3_input_h, da3_input_w) — typically
|
| 10 |
+
a 4:3-aspect rectangle like 252×336 that recovers the original aspect after
|
| 11 |
+
Florence's stretch. NO PADDING — both dims must be multiples of patch_size.
|
| 12 |
+
* Returns a list of N feature levels at the configured DPT taps (default
|
| 13 |
+
out_layers=[11, 19, 23] for DA3-Large = shallow, mid, deep), each as
|
| 14 |
+
[B, C, h_grid, w_grid] with (h_grid, w_grid) = (input_h/14, input_w/14).
|
| 15 |
+
* Returns the DA3 ray-head output (3D direction unit vector per pixel),
|
| 16 |
+
captured via a forward hook on `head.scratch.output_conv2_aux[-1]` and
|
| 17 |
+
resampled to (h_grid, w_grid).
|
| 18 |
+
|
| 19 |
+
Spatial-bias coordinate alignment:
|
| 20 |
+
* Florence sees a stretched 224×224 → its token-grid (e.g. 7×7) maps linearly
|
| 21 |
+
back to the ORIGINAL image's normalized [0,1]² coords (no aspect correction
|
| 22 |
+
needed; the stretch is bijective per axis).
|
| 23 |
+
* DA3 sees the same content but at the aspect-correct dims (e.g. 252×336)
|
| 24 |
+
→ its token-grid (e.g. 18×24) ALSO maps linearly to the ORIGINAL image's
|
| 25 |
+
normalized [0,1]² coords.
|
| 26 |
+
* Both grids therefore live in the same normalized coord space, and the
|
| 27 |
+
spatial-distance bias `-λ · ||pos_vlm - pos_da3||²` is well-defined.
|
| 28 |
+
"""
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
from typing import Dict, List, Tuple
|
| 32 |
+
|
| 33 |
+
import logging
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
import torch.nn as nn
|
| 37 |
+
import torch.nn.functional as F
|
| 38 |
+
|
| 39 |
+
logger = logging.getLogger(__name__)
|
| 40 |
+
|
| 41 |
+
# DA3-Large fallback (used if hub model has no `backbone.out_layers` attribute).
|
| 42 |
+
_DA3_LARGE_OUT_LAYERS = (11, 15, 19, 23)
|
| 43 |
+
_DA3_LARGE_EMBED_DIM = 1024
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class DA3LargeForGeoStack(nn.Module):
|
| 47 |
+
"""Frozen DA3-Large feature + ray-head extractor for GeoStack.
|
| 48 |
+
|
| 49 |
+
Usage:
|
| 50 |
+
da3 = DA3LargeForGeoStack(
|
| 51 |
+
model_name="depth-anything/DA3-Large-1.1",
|
| 52 |
+
out_layers=(11, 19, 23),
|
| 53 |
+
da3_input_h=252, da3_input_w=336, # aspect-correct 4:3 (~1.05× of 240×320)
|
| 54 |
+
)
|
| 55 |
+
# pixel_values: [B, 3, H_in, W_in] in ImageNet-normalized space
|
| 56 |
+
# (typically H_in=W_in=224 — the X-VLA processor's stretched square).
|
| 57 |
+
out = da3(pixel_values)
|
| 58 |
+
# out["feats"]: list[N] of [B, C, h_grid, w_grid]
|
| 59 |
+
# out["ray"]: [B, 3, h_grid, w_grid] (direction-only, unit-normalized)
|
| 60 |
+
# out["h_grid"]: int
|
| 61 |
+
# out["w_grid"]: int
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
model_name: str = "depth-anything/DA3-Large-1.1",
|
| 67 |
+
out_layers: Tuple[int, ...] = (11, 19, 23),
|
| 68 |
+
da3_input_h: int = 252,
|
| 69 |
+
da3_input_w: int = 336,
|
| 70 |
+
patch_size: int = 14,
|
| 71 |
+
use_bf16: bool = True,
|
| 72 |
+
):
|
| 73 |
+
super().__init__()
|
| 74 |
+
try:
|
| 75 |
+
from depth_anything_3.api import DepthAnything3
|
| 76 |
+
except Exception as exc:
|
| 77 |
+
raise ImportError(
|
| 78 |
+
"depth_anything_3 not installed; install third_party/Depth-Anything-3"
|
| 79 |
+
) from exc
|
| 80 |
+
|
| 81 |
+
self.model_name = str(model_name)
|
| 82 |
+
self.out_layers = tuple(int(i) for i in out_layers)
|
| 83 |
+
self.patch_size = int(patch_size)
|
| 84 |
+
self.use_bf16 = bool(use_bf16)
|
| 85 |
+
self.da3_input_h = int(da3_input_h)
|
| 86 |
+
self.da3_input_w = int(da3_input_w)
|
| 87 |
+
if self.da3_input_h % self.patch_size != 0 or self.da3_input_w % self.patch_size != 0:
|
| 88 |
+
raise ValueError(
|
| 89 |
+
f"DA3 input dims must be multiples of patch_size={self.patch_size}; "
|
| 90 |
+
f"got ({self.da3_input_h}, {self.da3_input_w})"
|
| 91 |
+
)
|
| 92 |
+
self.h_grid = self.da3_input_h // self.patch_size # e.g. 252/14 = 18
|
| 93 |
+
self.w_grid = self.da3_input_w // self.patch_size # e.g. 336/14 = 24
|
| 94 |
+
|
| 95 |
+
# Load DA3 + freeze
|
| 96 |
+
self.model = DepthAnything3.from_pretrained(self.model_name)
|
| 97 |
+
for p in self.model.parameters():
|
| 98 |
+
p.requires_grad_(False)
|
| 99 |
+
self.model.eval()
|
| 100 |
+
|
| 101 |
+
# Channel dim: introspect the loaded backbone rather than assuming
|
| 102 |
+
# DA3-Large. DA3-Large=1024, DA3NESTED-GIANT-LARGE-1.1=1536, etc.
|
| 103 |
+
# Downstream perceiver/projection is sized from self.embed_dim, so this
|
| 104 |
+
# MUST match the real feature width or K/V dims mismatch.
|
| 105 |
+
self.embed_dim = self._infer_embed_dim()
|
| 106 |
+
|
| 107 |
+
# Ray-head hook capture (after DA3's DualDPT output_conv2_aux).
|
| 108 |
+
self._ray_capture: Dict[str, torch.Tensor] = {}
|
| 109 |
+
self._register_ray_hook()
|
| 110 |
+
|
| 111 |
+
# v4.1 OPT-IN: capture depth output (main head) via a second hook on
|
| 112 |
+
# head.scratch.output_conv2. Off by default — only enabled when the
|
| 113 |
+
# depth-distillation aux head is configured.
|
| 114 |
+
self._depth_capture: Dict[str, torch.Tensor] = {}
|
| 115 |
+
self._depth_hook = None
|
| 116 |
+
self._capture_depth = False
|
| 117 |
+
|
| 118 |
+
# ---------- backbone feature-dim introspection (GIANT-safe) ----------
|
| 119 |
+
|
| 120 |
+
def _infer_embed_dim(self) -> int:
|
| 121 |
+
"""Read the DINOv2 backbone hidden width from the loaded DA3 model.
|
| 122 |
+
|
| 123 |
+
Walks common attribute paths to a `.embed_dim`/`.num_features`; falls
|
| 124 |
+
back to inspecting a transformer block's LayerNorm, then to the
|
| 125 |
+
DA3-Large constant. DA3-Large=1024, DA3NESTED-GIANT-LARGE-1.1=1536.
|
| 126 |
+
"""
|
| 127 |
+
obj = self.model
|
| 128 |
+
for path in (("model", "da3", "backbone", "pretrained"),
|
| 129 |
+
("da3", "backbone", "pretrained"),
|
| 130 |
+
("backbone", "pretrained"),
|
| 131 |
+
("backbone",)):
|
| 132 |
+
cur = obj
|
| 133 |
+
ok = True
|
| 134 |
+
for a in path:
|
| 135 |
+
if hasattr(cur, a):
|
| 136 |
+
cur = getattr(cur, a)
|
| 137 |
+
else:
|
| 138 |
+
ok = False
|
| 139 |
+
break
|
| 140 |
+
if ok:
|
| 141 |
+
for a in ("embed_dim", "num_features", "hidden_size", "n_embd"):
|
| 142 |
+
if hasattr(cur, a):
|
| 143 |
+
try:
|
| 144 |
+
return int(getattr(cur, a))
|
| 145 |
+
except Exception:
|
| 146 |
+
pass
|
| 147 |
+
# last-ditch: infer from a block's norm weight length
|
| 148 |
+
for n, p in self.model.named_parameters():
|
| 149 |
+
if "blocks.0." in n and n.endswith("norm1.weight") and p.ndim == 1:
|
| 150 |
+
return int(p.shape[0])
|
| 151 |
+
logger.warning("[da3_for_geostack] could not introspect embed_dim; "
|
| 152 |
+
"falling back to DA3-Large=%d", int(_DA3_LARGE_EMBED_DIM))
|
| 153 |
+
return int(_DA3_LARGE_EMBED_DIM)
|
| 154 |
+
|
| 155 |
+
# ---------- post-load reload of DA3 weights ----------
|
| 156 |
+
|
| 157 |
+
def reload_pretrained_weights(self) -> None:
|
| 158 |
+
"""Reload DA3-Large pretrained weights AFTER ``XVLA.from_pretrained``.
|
| 159 |
+
|
| 160 |
+
HF's ``from_pretrained`` treats every key absent from the loaded
|
| 161 |
+
checkpoint as a "missing" param and overwrites it via ``_init_weights``.
|
| 162 |
+
That zeroes/random-inits the DA3-Large weights we loaded inside
|
| 163 |
+
``__init__``, producing garbage geometry features as soon as alpha > 0.
|
| 164 |
+
|
| 165 |
+
Call this once right after ``XVLA.from_pretrained(...)`` to restore the
|
| 166 |
+
actual DA3 weights. Same pattern as ``DA3InlineEncoder.reload_pretrained_weights``.
|
| 167 |
+
"""
|
| 168 |
+
try:
|
| 169 |
+
from depth_anything_3.api import DepthAnything3
|
| 170 |
+
except Exception as exc:
|
| 171 |
+
raise ImportError("depth_anything_3 not installed") from exc
|
| 172 |
+
device = next(self.model.parameters()).device
|
| 173 |
+
dtype = next(self.model.parameters()).dtype
|
| 174 |
+
fresh = DepthAnything3.from_pretrained(self.model_name)
|
| 175 |
+
self.model.load_state_dict(fresh.state_dict(), strict=True)
|
| 176 |
+
self.model.to(device=device, dtype=dtype)
|
| 177 |
+
for p in self.model.parameters():
|
| 178 |
+
p.requires_grad_(False)
|
| 179 |
+
self.model.eval()
|
| 180 |
+
# Re-attach the ray hook defensively (load_state_dict copies weights
|
| 181 |
+
# into existing modules so the hook target is preserved, but if any
|
| 182 |
+
# downstream change re-creates head modules this re-attach saves us).
|
| 183 |
+
try:
|
| 184 |
+
if hasattr(self, "_ray_hook"):
|
| 185 |
+
self._ray_hook.remove()
|
| 186 |
+
except Exception:
|
| 187 |
+
pass
|
| 188 |
+
self._register_ray_hook()
|
| 189 |
+
|
| 190 |
+
# ---------- head-path resolution (DA3-Large vs NESTED) ----------
|
| 191 |
+
|
| 192 |
+
def _find_head_scratch(self):
|
| 193 |
+
"""Return the DPT ``head.scratch`` module across DA3 variants.
|
| 194 |
+
|
| 195 |
+
DA3-Large: ``self.model.model.head.scratch``.
|
| 196 |
+
DA3NESTED-GIANT-LARGE: the encoder is wrapped one level deeper as
|
| 197 |
+
``self.model.model.da3.head.scratch`` (net children = da3 / da3_metric).
|
| 198 |
+
"""
|
| 199 |
+
for path in (("model", "head", "scratch"),
|
| 200 |
+
("model", "da3", "head", "scratch"),
|
| 201 |
+
("da3", "head", "scratch"),
|
| 202 |
+
("head", "scratch")):
|
| 203 |
+
cur = self.model
|
| 204 |
+
ok = True
|
| 205 |
+
for a in path:
|
| 206 |
+
if hasattr(cur, a):
|
| 207 |
+
cur = getattr(cur, a)
|
| 208 |
+
else:
|
| 209 |
+
ok = False
|
| 210 |
+
break
|
| 211 |
+
if ok:
|
| 212 |
+
return cur
|
| 213 |
+
return None
|
| 214 |
+
|
| 215 |
+
# ---------- ray-head hook ----------
|
| 216 |
+
|
| 217 |
+
def _register_ray_hook(self) -> None:
|
| 218 |
+
"""Capture the ray-head pre-activation output via a forward hook on the
|
| 219 |
+
final aux conv in DA3's DualDPT. Output shape: [B*V, 7, H_ray, W_ray]
|
| 220 |
+
where channels are [dir_x, dir_y, dir_z, ori_x, ori_y, ori_z, conf].
|
| 221 |
+
|
| 222 |
+
DA3's ``head.scratch.output_conv2_aux`` may be an nn.Sequential or a
|
| 223 |
+
single Conv2d — hook whichever leaf module is at the tail.
|
| 224 |
+
"""
|
| 225 |
+
scratch = self._find_head_scratch()
|
| 226 |
+
if scratch is None or not hasattr(scratch, "output_conv2_aux"):
|
| 227 |
+
raise RuntimeError(
|
| 228 |
+
"DA3LargeForGeoStack: could not locate head.scratch.output_conv2_aux "
|
| 229 |
+
"(this DA3 variant may not have a DualDPT head)"
|
| 230 |
+
)
|
| 231 |
+
aux = scratch.output_conv2_aux
|
| 232 |
+
# DA3's DualDPT defines output_conv2_aux as an nn.ModuleList of length
|
| 233 |
+
# aux_pyramid_levels (default 4). The forward call site uses
|
| 234 |
+
# `scratch.output_conv2_aux[-1](last_aux)` (dualdpt.py:255) — i.e., only
|
| 235 |
+
# the LAST module is actually called. Hook it.
|
| 236 |
+
if isinstance(aux, (nn.Sequential, nn.ModuleList)):
|
| 237 |
+
if len(aux) == 0:
|
| 238 |
+
raise RuntimeError("DA3LargeForGeoStack: output_conv2_aux is empty")
|
| 239 |
+
target = aux[-1]
|
| 240 |
+
else:
|
| 241 |
+
target = aux
|
| 242 |
+
|
| 243 |
+
def _hook(module, inputs, output):
|
| 244 |
+
self._ray_capture["ray_raw"] = output
|
| 245 |
+
|
| 246 |
+
self._ray_hook = target.register_forward_hook(_hook)
|
| 247 |
+
|
| 248 |
+
# ---------- depth-head hook (v4.1 opt-in) ----------
|
| 249 |
+
|
| 250 |
+
def enable_depth_capture(self, enabled: bool = True) -> None:
|
| 251 |
+
"""Turn on capture of DA3's main depth output (from output_conv2).
|
| 252 |
+
Idempotent — safe to call multiple times.
|
| 253 |
+
"""
|
| 254 |
+
if enabled and self._depth_hook is None:
|
| 255 |
+
scratch = self._find_head_scratch()
|
| 256 |
+
if scratch is None or not hasattr(scratch, "output_conv2"):
|
| 257 |
+
raise RuntimeError(
|
| 258 |
+
"DA3LargeForGeoStack: could not locate head.scratch.output_conv2 for depth hook"
|
| 259 |
+
)
|
| 260 |
+
target = scratch.output_conv2
|
| 261 |
+
def _hook(module, inputs, output):
|
| 262 |
+
self._depth_capture["depth_raw"] = output
|
| 263 |
+
self._depth_hook = target.register_forward_hook(_hook)
|
| 264 |
+
elif not enabled and self._depth_hook is not None:
|
| 265 |
+
self._depth_hook.remove()
|
| 266 |
+
self._depth_hook = None
|
| 267 |
+
self._capture_depth = bool(enabled)
|
| 268 |
+
|
| 269 |
+
# ---------- forward ----------
|
| 270 |
+
|
| 271 |
+
@torch.no_grad()
|
| 272 |
+
def _da3_forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:
|
| 273 |
+
"""Run DA3 inner forward and return the aux dict.
|
| 274 |
+
|
| 275 |
+
x: [B, V, 3, H, W] in ImageNet-normalized space (already resized to
|
| 276 |
+
(da3_input_h, da3_input_w)).
|
| 277 |
+
"""
|
| 278 |
+
inner = self.model.model
|
| 279 |
+
out = inner(
|
| 280 |
+
x, None, None, # no extrinsics/intrinsics
|
| 281 |
+
list(self.out_layers), # export these feat layers
|
| 282 |
+
False, # infer_gs
|
| 283 |
+
False, # use_ray_pose
|
| 284 |
+
"saddle_balanced", # ref_view_strategy
|
| 285 |
+
)
|
| 286 |
+
if isinstance(out, dict):
|
| 287 |
+
aux = out.get("aux") or {}
|
| 288 |
+
else:
|
| 289 |
+
aux = getattr(out, "aux", None) or {}
|
| 290 |
+
return aux
|
| 291 |
+
|
| 292 |
+
@torch.no_grad()
|
| 293 |
+
def _da3_forward_posed(
|
| 294 |
+
self,
|
| 295 |
+
x: torch.Tensor, # [B, V, 3, H, W]
|
| 296 |
+
extrinsics: torch.Tensor, # [B, V, 4, 4] OpenCV world-to-camera
|
| 297 |
+
intrinsics: torch.Tensor, # [B, V, 3, 3] rescaled to (H, W)
|
| 298 |
+
) -> Dict[str, torch.Tensor]:
|
| 299 |
+
"""Joint posed multi-view DA3 forward: ALL views in one inner() call
|
| 300 |
+
with camera extrinsics + intrinsics, so DA3's camera encoder fuses pose
|
| 301 |
+
tokens into the backbone and returns per-view features that are
|
| 302 |
+
geometrically consistent ACROSS views. Mirrors da3_inline's posed call
|
| 303 |
+
(use_ray_pose=False; poses drive cam_enc, not the ray head)."""
|
| 304 |
+
inner = self.model.model
|
| 305 |
+
out = inner(
|
| 306 |
+
x,
|
| 307 |
+
extrinsics.to(device=x.device, dtype=torch.float32),
|
| 308 |
+
intrinsics.to(device=x.device, dtype=torch.float32),
|
| 309 |
+
list(self.out_layers), # export these feat layers
|
| 310 |
+
False, # infer_gs
|
| 311 |
+
False, # use_ray_pose
|
| 312 |
+
"saddle_balanced", # ref_view_strategy
|
| 313 |
+
)
|
| 314 |
+
# Nested GIANT returns a top-level metric-depth map (da3_metric branch):
|
| 315 |
+
# out["depth"] = [B,V,H,W], out["is_metric"]. Surface it alongside aux so
|
| 316 |
+
# the spatial tokens can carry ray + DEPTH (scale-aware ray).
|
| 317 |
+
if isinstance(out, dict):
|
| 318 |
+
aux = out.get("aux") or {}
|
| 319 |
+
depth = out.get("depth", None)
|
| 320 |
+
else:
|
| 321 |
+
aux = getattr(out, "aux", None) or {}
|
| 322 |
+
depth = getattr(out, "depth", None)
|
| 323 |
+
return aux, depth
|
| 324 |
+
|
| 325 |
+
@staticmethod
|
| 326 |
+
def _rescale_intrinsics(
|
| 327 |
+
K: torch.Tensor, src_hw: Tuple[int, int], dst_hw: Tuple[int, int]
|
| 328 |
+
) -> torch.Tensor:
|
| 329 |
+
"""Rescale pixel intrinsics [..., 3, 3] from (src_H, src_W) to
|
| 330 |
+
(dst_H, dst_W). fx, cx scale with the width ratio; fy, cy with height.
|
| 331 |
+
Matches da3_inline._batched_call so the pose math sees K aligned to the
|
| 332 |
+
actual tensor DA3 receives. Identity when src == dst."""
|
| 333 |
+
sh, sw = int(src_hw[0]), int(src_hw[1])
|
| 334 |
+
dh, dw = int(dst_hw[0]), int(dst_hw[1])
|
| 335 |
+
if (sh, sw) == (dh, dw):
|
| 336 |
+
return K.to(torch.float32)
|
| 337 |
+
sx = float(dw) / float(sw) # width scale
|
| 338 |
+
sy = float(dh) / float(sh) # height scale
|
| 339 |
+
K = K.clone().to(torch.float32)
|
| 340 |
+
K[..., 0, 0] *= sx # fx
|
| 341 |
+
K[..., 1, 1] *= sy # fy
|
| 342 |
+
K[..., 0, 2] *= sx # cx
|
| 343 |
+
K[..., 1, 2] *= sy # cy
|
| 344 |
+
return K
|
| 345 |
+
|
| 346 |
+
def _to_bchw(self, feat: torch.Tensor, B: int) -> torch.Tensor:
|
| 347 |
+
"""Normalize an aux entry to [B*V, C, h, w]. V=1 for GeoStack."""
|
| 348 |
+
if feat.dim() == 6: # [B, V, s, h, w, C]
|
| 349 |
+
feat = feat.mean(dim=2)
|
| 350 |
+
if feat.dim() == 5: # [B, V, h, w, C]
|
| 351 |
+
return feat.permute(0, 1, 4, 2, 3).reshape(-1, feat.shape[4], feat.shape[2], feat.shape[3])
|
| 352 |
+
if feat.dim() == 4: # [B, V, N, C] or [B*V, C, h, w]
|
| 353 |
+
if feat.shape[0] == B: # heuristic: [B, V, N, C]
|
| 354 |
+
B_, V_, N, C_ = feat.shape
|
| 355 |
+
s = max(1, int(round(N ** 0.5)))
|
| 356 |
+
return feat[:, :, : s * s].permute(0, 1, 3, 2).reshape(B_ * V_, C_, s, s)
|
| 357 |
+
return feat # already [B*V, C, h, w]
|
| 358 |
+
raise RuntimeError(f"unexpected DA3 feat tensor rank: {feat.dim()}, shape={tuple(feat.shape)}")
|
| 359 |
+
|
| 360 |
+
def forward(
|
| 361 |
+
self,
|
| 362 |
+
pixel_values: torch.Tensor,
|
| 363 |
+
target_input_hw: Tuple[int, int] | None = None,
|
| 364 |
+
) -> Dict[str, object]:
|
| 365 |
+
"""Run DA3 forward on a batch with aspect-correct resize.
|
| 366 |
+
|
| 367 |
+
Args:
|
| 368 |
+
pixel_values: [B, 3, H_in, W_in] ImageNet-normalized float. Any input
|
| 369 |
+
resolution accepted; bilinearly resized to (h, w) before DA3 forward.
|
| 370 |
+
target_input_hw: optional (h, w) override for DA3 input size. When set,
|
| 371 |
+
this overrides the configured (da3_input_h, da3_input_w) — useful
|
| 372 |
+
for running the same DA3 at different resolutions per view
|
| 373 |
+
(e.g., main camera at 252×336, wrist at 168×224). Both dims must
|
| 374 |
+
be multiples of patch_size. The returned feats and ray use a
|
| 375 |
+
token grid derived from the target dims, NOT the configured ones.
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
dict with keys:
|
| 379 |
+
feats: List[torch.Tensor] one per out_layer, each [B, C, h_grid, w_grid]
|
| 380 |
+
ray: torch.Tensor [B, 3, h_grid, w_grid], unit-normalized direction
|
| 381 |
+
h_grid: int
|
| 382 |
+
w_grid: int
|
| 383 |
+
"""
|
| 384 |
+
if pixel_values.dim() != 4:
|
| 385 |
+
raise ValueError(f"pixel_values must be [B, 3, H, W]; got shape {tuple(pixel_values.shape)}")
|
| 386 |
+
B = pixel_values.shape[0]
|
| 387 |
+
device = pixel_values.device
|
| 388 |
+
x = pixel_values.to(torch.float32)
|
| 389 |
+
|
| 390 |
+
# Resolve input dims + corresponding token grid.
|
| 391 |
+
if target_input_hw is None:
|
| 392 |
+
in_h, in_w = self.da3_input_h, self.da3_input_w
|
| 393 |
+
h_grid, w_grid = self.h_grid, self.w_grid
|
| 394 |
+
else:
|
| 395 |
+
in_h, in_w = int(target_input_hw[0]), int(target_input_hw[1])
|
| 396 |
+
if in_h % self.patch_size != 0 or in_w % self.patch_size != 0:
|
| 397 |
+
raise ValueError(
|
| 398 |
+
f"target_input_hw=({in_h},{in_w}) must be multiples of patch_size={self.patch_size}"
|
| 399 |
+
)
|
| 400 |
+
h_grid, w_grid = in_h // self.patch_size, in_w // self.patch_size
|
| 401 |
+
|
| 402 |
+
# Aspect-correct resize (bilinear). NO PADDING — input dims chosen so
|
| 403 |
+
# both are multiples of patch_size.
|
| 404 |
+
x = F.interpolate(x, size=(in_h, in_w), mode="bilinear", align_corners=False)
|
| 405 |
+
# Expand to V=1 view dim (DA3 expects [B, V, 3, H, W])
|
| 406 |
+
x = x.unsqueeze(1)
|
| 407 |
+
|
| 408 |
+
# Clear stale ray capture (and depth if enabled), run DA3
|
| 409 |
+
self._ray_capture.clear()
|
| 410 |
+
if self._capture_depth:
|
| 411 |
+
self._depth_capture.clear()
|
| 412 |
+
if self.use_bf16:
|
| 413 |
+
with torch.autocast(device_type=device.type, dtype=torch.bfloat16):
|
| 414 |
+
aux = self._da3_forward(x)
|
| 415 |
+
else:
|
| 416 |
+
aux = self._da3_forward(x)
|
| 417 |
+
|
| 418 |
+
feats, ray_at_feat, depth_at_feat = self._finalize(aux, B, h_grid, w_grid)
|
| 419 |
+
return {
|
| 420 |
+
"feats": feats, # list of [B, C, h_grid, w_grid]
|
| 421 |
+
"ray": ray_at_feat, # [B, 3, h_grid, w_grid]
|
| 422 |
+
"depth": depth_at_feat, # [B, 1, h_grid, w_grid] or None
|
| 423 |
+
"h_grid": int(h_grid),
|
| 424 |
+
"w_grid": int(w_grid),
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
def _finalize(self, aux, B: int, h_grid: int, w_grid: int):
|
| 428 |
+
"""Pull per-layer feats + ray + optional depth out of a completed DA3
|
| 429 |
+
forward. Returns tensors whose leading dim is B*V (V baked in by DA3's
|
| 430 |
+
aux layout): feats as list of [B*V, C, h_grid, w_grid], ray as
|
| 431 |
+
[B*V, 3, h_grid, w_grid], depth as [B*V, 1, h_grid, w_grid] or None.
|
| 432 |
+
The single-view ``forward`` (V=1) gets [B, ...]; the joint multi-view
|
| 433 |
+
path unflattens the leading dim back to (B, V).
|
| 434 |
+
"""
|
| 435 |
+
# Extract per-level feats at native (h_grid, w_grid). DA3 may emit them
|
| 436 |
+
# at the backbone resolution already matching (h_grid, w_grid) since
|
| 437 |
+
# in_h/in_w = h_grid*patch × w_grid*patch.
|
| 438 |
+
feats: List[torch.Tensor] = []
|
| 439 |
+
for li in self.out_layers:
|
| 440 |
+
key = f"feat_layer_{li}"
|
| 441 |
+
fi = aux.get(key)
|
| 442 |
+
if fi is None:
|
| 443 |
+
raise RuntimeError(
|
| 444 |
+
f"DA3LargeForGeoStack: aux missing key '{key}'; available={list(aux.keys())[:8]}"
|
| 445 |
+
)
|
| 446 |
+
fi = self._to_bchw(fi, B=B) # [B*V, C, h, w]
|
| 447 |
+
if fi.shape[-1] != w_grid or fi.shape[-2] != h_grid:
|
| 448 |
+
# Defensive resize in case DA3 produced a different grid.
|
| 449 |
+
fi = F.interpolate(
|
| 450 |
+
fi.float(), size=(h_grid, w_grid),
|
| 451 |
+
mode="bilinear", align_corners=False,
|
| 452 |
+
)
|
| 453 |
+
feats.append(fi)
|
| 454 |
+
|
| 455 |
+
# Extract ray-head output (3D direction only, normalized)
|
| 456 |
+
ray_raw = self._ray_capture.get("ray_raw", None)
|
| 457 |
+
if ray_raw is None:
|
| 458 |
+
raise RuntimeError(
|
| 459 |
+
"DA3LargeForGeoStack: ray-head hook did not fire; DA3 forward did not run "
|
| 460 |
+
"the aux/ray branch. Check head.scratch.output_conv2_aux exists."
|
| 461 |
+
)
|
| 462 |
+
if ray_raw.dim() == 4:
|
| 463 |
+
ray_dir = ray_raw[:, :3, :, :]
|
| 464 |
+
elif ray_raw.dim() == 5: # [B, V, C, H, W]
|
| 465 |
+
ray_dir = ray_raw[:, :, :3, :, :].reshape(-1, 3, ray_raw.shape[-2], ray_raw.shape[-1])
|
| 466 |
+
else:
|
| 467 |
+
raise RuntimeError(f"Unexpected ray tensor rank: {ray_raw.dim()}, shape={tuple(ray_raw.shape)}")
|
| 468 |
+
ray_at_feat = F.interpolate(
|
| 469 |
+
ray_dir.float(), size=(h_grid, w_grid),
|
| 470 |
+
mode="bilinear", align_corners=False,
|
| 471 |
+
)
|
| 472 |
+
ray_at_feat = ray_at_feat / (ray_at_feat.norm(dim=1, keepdim=True) + 1e-6)
|
| 473 |
+
self._ray_capture.clear()
|
| 474 |
+
|
| 475 |
+
# Optional depth (v4.1) — extract DA3's main-head depth output at patch grid
|
| 476 |
+
depth_at_feat = None
|
| 477 |
+
if self._capture_depth:
|
| 478 |
+
depth_raw = self._depth_capture.get("depth_raw", None)
|
| 479 |
+
if depth_raw is not None:
|
| 480 |
+
# depth_raw: [B*V, C_out, H, W] or [B, V, C_out, H, W]
|
| 481 |
+
if depth_raw.dim() == 5:
|
| 482 |
+
depth_raw = depth_raw.reshape(-1, depth_raw.shape[-3], depth_raw.shape[-2], depth_raw.shape[-1])
|
| 483 |
+
# First channel = raw depth logits (DPT convention)
|
| 484 |
+
depth_scalar = depth_raw[:, :1].float()
|
| 485 |
+
depth_at_feat = F.interpolate(
|
| 486 |
+
depth_scalar, size=(h_grid, w_grid),
|
| 487 |
+
mode="bilinear", align_corners=False,
|
| 488 |
+
)
|
| 489 |
+
self._depth_capture.clear()
|
| 490 |
+
|
| 491 |
+
return feats, ray_at_feat, depth_at_feat
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
# ---------- multi-view forward (v2C) ----------
|
| 495 |
+
|
| 496 |
+
def forward_multi_view(
|
| 497 |
+
self,
|
| 498 |
+
pixel_values: torch.Tensor, # [B, V, 3, H_in, W_in]
|
| 499 |
+
target_input_hw: Tuple[int, int] | None = None,
|
| 500 |
+
extrinsics: torch.Tensor | None = None, # [B, V, 4, 4] OpenCV w2c
|
| 501 |
+
intrinsics: torch.Tensor | None = None, # [B, V, 3, 3] @ (H_in, W_in)
|
| 502 |
+
) -> Dict[str, object]:
|
| 503 |
+
"""Process multiple views, returning per-view feats/rays.
|
| 504 |
+
|
| 505 |
+
Two modes, same output contract:
|
| 506 |
+
* Unposed (extrinsics/intrinsics=None): flatten [B,V]→[B*V] and run
|
| 507 |
+
each view through DA3 INDEPENDENTLY (legacy v2C wrist-depth path).
|
| 508 |
+
* Posed joint (both provided): feed ALL V views TOGETHER into one DA3
|
| 509 |
+
inner() call with camera extrinsics + intrinsics, so DA3's camera
|
| 510 |
+
encoder makes the per-view features geometrically consistent across
|
| 511 |
+
views. Downstream per-view routing/cross-attention is unchanged —
|
| 512 |
+
only the feature content becomes cross-view-aware.
|
| 513 |
+
|
| 514 |
+
Args:
|
| 515 |
+
pixel_values: [B, V, 3, H_in, W_in] ImageNet-normalized float.
|
| 516 |
+
target_input_hw: optional (h, w) override (e.g., 224×224 for wrists).
|
| 517 |
+
extrinsics: optional [B, V, 4, 4] OpenCV world-to-camera.
|
| 518 |
+
intrinsics: optional [B, V, 3, 3] calibrated to (H_in, W_in); DA3
|
| 519 |
+
resizes views to (in_h, in_w) internally so K is rescaled to match.
|
| 520 |
+
|
| 521 |
+
Returns:
|
| 522 |
+
dict with keys:
|
| 523 |
+
feats: List[torch.Tensor] one per out_layer, each [B, V, C, h_grid, w_grid]
|
| 524 |
+
ray: torch.Tensor [B, V, 3, h_grid, w_grid]
|
| 525 |
+
depth: torch.Tensor | None [B, V, 1, h_grid, w_grid]
|
| 526 |
+
h_grid: int
|
| 527 |
+
w_grid: int
|
| 528 |
+
"""
|
| 529 |
+
if pixel_values.dim() != 5:
|
| 530 |
+
raise ValueError(
|
| 531 |
+
f"forward_multi_view expects [B,V,3,H,W]; got {tuple(pixel_values.shape)}"
|
| 532 |
+
)
|
| 533 |
+
B, V = pixel_values.shape[:2]
|
| 534 |
+
|
| 535 |
+
# ---- Posed joint multi-view path ----
|
| 536 |
+
if extrinsics is not None and intrinsics is not None:
|
| 537 |
+
return self._forward_multi_view_posed(
|
| 538 |
+
pixel_values, extrinsics, intrinsics, target_input_hw
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
# ---- Legacy unposed per-view path ----
|
| 542 |
+
flat = pixel_values.flatten(0, 1) # [B*V, 3, H_in, W_in]
|
| 543 |
+
out_flat = self.forward(flat, target_input_hw=target_input_hw)
|
| 544 |
+
feats_flat = out_flat["feats"] # list of [B*V, C, h, w]
|
| 545 |
+
ray_flat = out_flat["ray"] # [B*V, 3, h, w]
|
| 546 |
+
h_grid = int(out_flat["h_grid"])
|
| 547 |
+
w_grid = int(out_flat["w_grid"])
|
| 548 |
+
feats = [f.unflatten(0, (B, V)) for f in feats_flat] # list of [B, V, C, h, w]
|
| 549 |
+
ray = ray_flat.unflatten(0, (B, V)) # [B, V, 3, h, w]
|
| 550 |
+
result = {
|
| 551 |
+
"feats": feats,
|
| 552 |
+
"ray": ray,
|
| 553 |
+
"h_grid": h_grid,
|
| 554 |
+
"w_grid": w_grid,
|
| 555 |
+
}
|
| 556 |
+
# v4.1: forward multi-view depth if the depth hook is on
|
| 557 |
+
depth_flat = out_flat.get("depth", None)
|
| 558 |
+
if depth_flat is not None:
|
| 559 |
+
result["depth"] = depth_flat.unflatten(0, (B, V)) # [B, V, 1, h, w]
|
| 560 |
+
else:
|
| 561 |
+
result["depth"] = None
|
| 562 |
+
return result
|
| 563 |
+
|
| 564 |
+
def _forward_multi_view_posed(
|
| 565 |
+
self,
|
| 566 |
+
pixel_values: torch.Tensor, # [B, V, 3, H_in, W_in]
|
| 567 |
+
extrinsics: torch.Tensor, # [B, V, 4, 4] OpenCV w2c
|
| 568 |
+
intrinsics: torch.Tensor, # [B, V, 3, 3] @ (H_in, W_in)
|
| 569 |
+
target_input_hw: Tuple[int, int] | None = None,
|
| 570 |
+
) -> Dict[str, object]:
|
| 571 |
+
"""Joint posed multi-view DA3: one inner() call over all V views with
|
| 572 |
+
camera pose conditioning → cross-view-consistent per-view features.
|
| 573 |
+
Same return contract as forward_multi_view (unposed)."""
|
| 574 |
+
B, V, _, H_in, W_in = pixel_values.shape
|
| 575 |
+
device = pixel_values.device
|
| 576 |
+
|
| 577 |
+
# Resolve DA3 input dims + the resulting token grid.
|
| 578 |
+
if target_input_hw is None:
|
| 579 |
+
in_h, in_w = self.da3_input_h, self.da3_input_w
|
| 580 |
+
h_grid, w_grid = self.h_grid, self.w_grid
|
| 581 |
+
else:
|
| 582 |
+
in_h, in_w = int(target_input_hw[0]), int(target_input_hw[1])
|
| 583 |
+
if in_h % self.patch_size != 0 or in_w % self.patch_size != 0:
|
| 584 |
+
raise ValueError(
|
| 585 |
+
f"target_input_hw=({in_h},{in_w}) must be multiples of patch_size={self.patch_size}"
|
| 586 |
+
)
|
| 587 |
+
h_grid, w_grid = in_h // self.patch_size, in_w // self.patch_size
|
| 588 |
+
|
| 589 |
+
# Aspect-correct resize of every view to (in_h, in_w).
|
| 590 |
+
x = pixel_values.to(torch.float32).flatten(0, 1) # [B*V, 3, H_in, W_in]
|
| 591 |
+
if (H_in, W_in) != (in_h, in_w):
|
| 592 |
+
x = F.interpolate(x, size=(in_h, in_w), mode="bilinear", align_corners=False)
|
| 593 |
+
x = x.unflatten(0, (B, V)) # [B, V, 3, in_h, in_w]
|
| 594 |
+
|
| 595 |
+
# Rescale intrinsics from the incoming tensor resolution to (in_h, in_w)
|
| 596 |
+
# so DA3's pose math sees K aligned with the pixels it actually gets.
|
| 597 |
+
K = self._rescale_intrinsics(intrinsics, (H_in, W_in), (in_h, in_w))
|
| 598 |
+
|
| 599 |
+
# Clear stale captures, run ONE joint posed DA3 forward.
|
| 600 |
+
self._ray_capture.clear()
|
| 601 |
+
if self._capture_depth:
|
| 602 |
+
self._depth_capture.clear()
|
| 603 |
+
if self.use_bf16:
|
| 604 |
+
with torch.autocast(device_type=device.type, dtype=torch.bfloat16):
|
| 605 |
+
aux, depth_full = self._da3_forward_posed(x, extrinsics, K)
|
| 606 |
+
else:
|
| 607 |
+
aux, depth_full = self._da3_forward_posed(x, extrinsics, K)
|
| 608 |
+
|
| 609 |
+
# Shared extraction — leading dim is B*V (V baked into DA3's aux layout).
|
| 610 |
+
feats_flat, ray_flat, depth_hook = self._finalize(aux, B, h_grid, w_grid)
|
| 611 |
+
feats = [f.unflatten(0, (B, V)) for f in feats_flat] # list of [B, V, C, h, w]
|
| 612 |
+
ray = ray_flat.unflatten(0, (B, V)) # [B, V, 3, h, w]
|
| 613 |
+
|
| 614 |
+
# Metric depth from the top-level output (preferred over the DPT hook):
|
| 615 |
+
# [B,V,H,W] → resize to the token grid → [B,V,1,h,w].
|
| 616 |
+
if depth_full is not None:
|
| 617 |
+
df = depth_full.float()
|
| 618 |
+
if df.dim() == 4: # [B, V, H, W]
|
| 619 |
+
df = df.flatten(0, 1).unsqueeze(1) # [B*V, 1, H, W]
|
| 620 |
+
elif df.dim() == 5: # [B, V, 1, H, W]
|
| 621 |
+
df = df.flatten(0, 1) # [B*V, 1, H, W]
|
| 622 |
+
df = F.interpolate(df, size=(h_grid, w_grid), mode="bilinear", align_corners=False)
|
| 623 |
+
depth_grid = df.unflatten(0, (B, V)) # [B, V, 1, h, w]
|
| 624 |
+
elif depth_hook is not None:
|
| 625 |
+
depth_grid = depth_hook.unflatten(0, (B, V))
|
| 626 |
+
else:
|
| 627 |
+
depth_grid = None
|
| 628 |
+
|
| 629 |
+
result: Dict[str, object] = {
|
| 630 |
+
"feats": feats,
|
| 631 |
+
"ray": ray,
|
| 632 |
+
"h_grid": int(h_grid),
|
| 633 |
+
"w_grid": int(w_grid),
|
| 634 |
+
"depth": depth_grid, # [B, V, 1, h, w] metric
|
| 635 |
+
}
|
| 636 |
+
return result
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
__all__ = ["DA3LargeForGeoStack"]
|
pibehavior_da3_clean_up_your_desk_40k/source_code/launch/launch_b1k_robopro.sh
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Launch/resume the b1k DA3 robopro-exact run. Arg1 = physical GPU list (e.g. "4,5,6,7").
|
| 3 |
+
# Auto-resumes from the latest checkpoint if one exists; else fresh from behavior_50t.
|
| 4 |
+
set -uo pipefail
|
| 5 |
+
GPUS="${1:-4,5,6,7}"
|
| 6 |
+
cd /work/jack/behavior-1k-solution
|
| 7 |
+
# Auto mode: 8 GPUs => dedicated extraction (hold out first 4 for the frozen DA3 forward, train on last 4);
|
| 8 |
+
# 4 GPUs => inline (extraction shares the training GPUs). Extraction always pins cuda:0-3 (the first 4 given).
|
| 9 |
+
NGPU=$(echo "$GPUS" | tr ',' '\n' | grep -c .)
|
| 10 |
+
if [ "$NGPU" -ge 8 ]; then
|
| 11 |
+
# 8-GPU: 4 train + 4 extract. Training stays 4-way DP to MATCH the checkpoint's mesh — changing the
|
| 12 |
+
# #training-GPUs on resume reshapes the sharding and breaks restore, so we hold it fixed at 4.
|
| 13 |
+
HOLDOUT=4; QDEPTH=6; EXTRACT_DEVS="cuda:0,cuda:1,cuda:2,cuda:3"; BSZ=128; GMODE="8-GPU: 4 train + 4 extract"
|
| 14 |
+
else
|
| 15 |
+
HOLDOUT=0; QDEPTH=2; EXTRACT_DEVS="cuda:0,cuda:1,cuda:2,cuda:3"; BSZ=128; GMODE="${NGPU}-GPU inline"
|
| 16 |
+
fi
|
| 17 |
+
LOG=/work/jack/b1k_da3_robopro_desk_train.log
|
| 18 |
+
CKDIR=outputs/checkpoints/pi_behavior_b1k_fast/b1k_da3_robopro_desk
|
| 19 |
+
# resume if a numeric checkpoint exists, else fresh
|
| 20 |
+
if ls "$CKDIR" 2>/dev/null | grep -qE '^[0-9]+$'; then RES=1; OVW=0; MODE="RESUME from step $(ls "$CKDIR"|grep -E '^[0-9]+$'|sort -n|tail -1)"; else RES=0; OVW=1; MODE="FRESH"; fi
|
| 21 |
+
echo "[$(date +%H:%M)] launching ($MODE, $GMODE) on GPUs $GPUS" >> /work/jack/b1k_babysitter.log
|
| 22 |
+
CUDA_VISIBLE_DEVICES="$GPUS" \
|
| 23 |
+
XLA_PYTHON_CLIENT_MEM_FRACTION=0.80 XLA_PYTHON_CLIENT_ALLOCATOR=platform \
|
| 24 |
+
XLA_FLAGS="--xla_gpu_enable_latency_hiding_scheduler=true --xla_gpu_all_reduce_combine_threshold_bytes=8388608 --xla_gpu_enable_highest_priority_async_stream=true" \
|
| 25 |
+
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
|
| 26 |
+
B1K_HOLDOUT_GPU0=$HOLDOUT \
|
| 27 |
+
B1K_EXTRACT_DEVICES=$EXTRACT_DEVS B1K_DECODE_THREADS=1 \
|
| 28 |
+
B1K_SHARED_DECODE=1 \
|
| 29 |
+
B1K_DLPACK=1 \
|
| 30 |
+
B1K_DA3_FWD_CHUNK=${B1K_DA3_FWD_CHUNK:-32} \
|
| 31 |
+
B1K_EXTRACT_QUEUE=$QDEPTH \
|
| 32 |
+
RAYON_NUM_THREADS=1 OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 \
|
| 33 |
+
NUMEXPR_NUM_THREADS=1 POLARS_MAX_THREADS=1 TOKENIZERS_PARALLELISM=false \
|
| 34 |
+
HF_HOME=/work/jack/da3xvla_workspace/hf_cache HF_HUB_OFFLINE=1 \
|
| 35 |
+
B1K_2026_ROOT=/work/jack/behavior1k/data/behavior_2026_task29_42 \
|
| 36 |
+
USE_DA3_FULL=1 DA3_INIT_STD=0.01 DA3_LOGIT_GAIN=1 DA3_SCALE=2.0 DA3_LR_GROUPS=1 \
|
| 37 |
+
LR_VLM_PEAK=2.5e-5 LR_VLM_END=2.5e-6 LR_CORE_PEAK=5e-4 LR_CORE_END=5e-5 LR_GEOM_PEAK=5e-4 LR_GEOM_END=1e-4 \
|
| 38 |
+
B1K_ACTIVITIES=clean_up_your_desk \
|
| 39 |
+
B1K_INIT_PARAMS=/work/jack/behavior1k/checkpoints/behavior_50t_checkpoint/params \
|
| 40 |
+
EXP=b1k_da3_robopro_desk \
|
| 41 |
+
STEPS=50000 BS=$BSZ FLOW=15 NW=${NW:-32} SHUFFLE=1 RESUME=$RES OVERWRITE=$OVW \
|
| 42 |
+
SAVE_INTERVAL=2000 KEEP_PERIOD=2000 LOG_INTERVAL=50 \
|
| 43 |
+
LR_WARMUP=1000 LR_DECAY_STEPS=50000 \
|
| 44 |
+
nohup .venv/bin/python scripts/train_2026.py >> "$LOG" 2>&1 &
|
| 45 |
+
echo "LAUNCHED pid $! ($MODE) on GPUs $GPUS"
|
pibehavior_da3_clean_up_your_desk_40k/source_code/model/observation.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Observation class and preprocessing with FAST auxiliary fields support.
|
| 2 |
+
|
| 3 |
+
Based on openpi with FAST fields added for PI_BEHAVIOR model.
|
| 4 |
+
|
| 5 |
+
Reference: https://github.com/wensi-ai/openpi/blob/behavior/src/openpi/models/model.py
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from collections.abc import Sequence
|
| 9 |
+
from typing import Generic, TypeVar
|
| 10 |
+
import dataclasses
|
| 11 |
+
|
| 12 |
+
import augmax
|
| 13 |
+
from flax import struct
|
| 14 |
+
import jax
|
| 15 |
+
import jax.numpy as jnp
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from openpi.shared import image_tools
|
| 20 |
+
from openpi.shared import array_typing as at
|
| 21 |
+
|
| 22 |
+
ArrayT = TypeVar("ArrayT", bound=jax.Array | torch.Tensor | np.ndarray)
|
| 23 |
+
|
| 24 |
+
IMAGE_KEYS = (
|
| 25 |
+
"base_0_rgb",
|
| 26 |
+
"left_wrist_0_rgb",
|
| 27 |
+
"right_wrist_0_rgb",
|
| 28 |
+
)
|
| 29 |
+
IMAGE_RESOLUTION = (224, 224)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@at.typecheck
|
| 33 |
+
@struct.dataclass
|
| 34 |
+
class Observation(Generic[ArrayT]):
|
| 35 |
+
"""Observation with FAST auxiliary fields."""
|
| 36 |
+
|
| 37 |
+
images: dict[str, at.Float[ArrayT, "*b h w c"]]
|
| 38 |
+
image_masks: dict[str, at.Bool[ArrayT, "*b"]]
|
| 39 |
+
state: at.Float[ArrayT, "*b s"]
|
| 40 |
+
tokenized_prompt: at.Int[ArrayT, "*b l"] | None = None
|
| 41 |
+
tokenized_prompt_mask: at.Bool[ArrayT, "*b l"] | None = None
|
| 42 |
+
token_ar_mask: at.Int[ArrayT, "*b l"] | None = None
|
| 43 |
+
token_loss_mask: at.Bool[ArrayT, "*b l"] | None = None
|
| 44 |
+
|
| 45 |
+
fast_tokens: at.Int[ArrayT, "*b t"] | None = None
|
| 46 |
+
fast_token_mask: at.Bool[ArrayT, "*b t"] | None = None
|
| 47 |
+
spatial_tokens: at.Float[ArrayT, "*b n d"] | None = None
|
| 48 |
+
spatial_token_mask: at.Bool[ArrayT, "*b n"] | None = None
|
| 49 |
+
# DA3 inline spatial inputs (frozen DA3-GIANT features + geometry; consumed by the trainable
|
| 50 |
+
# bank builder in PiBehavior). da3_features ship as raw bits: uint16=bf16 bits, uint8=fp8 bytes.
|
| 51 |
+
da3_features: at.Num[ArrayT, "*b dl v dc gh gw"] | None = None
|
| 52 |
+
da3_ray: at.Float[ArrayT, "*b v three gh gw"] | None = None
|
| 53 |
+
da3_depth: at.Float[ArrayT, "*b v one gh gw"] | None = None
|
| 54 |
+
camera_extrinsics: at.Float[ArrayT, "*b v four four2"] | None = None
|
| 55 |
+
lang_feat: at.Float[ArrayT, "*b lt ld"] | None = None
|
| 56 |
+
lang_mask: at.Bool[ArrayT, "*b lt"] | None = None
|
| 57 |
+
|
| 58 |
+
@classmethod
|
| 59 |
+
def from_dict(cls, data: at.PyTree[ArrayT]) -> "Observation[ArrayT]":
|
| 60 |
+
"""Convert dict to Observation."""
|
| 61 |
+
if ("tokenized_prompt" in data) != ("tokenized_prompt_mask" in data):
|
| 62 |
+
raise ValueError("tokenized_prompt and tokenized_prompt_mask must be provided together.")
|
| 63 |
+
|
| 64 |
+
# Convert uint8 images to float32 [-1, 1]
|
| 65 |
+
for key in data["image"]:
|
| 66 |
+
if data["image"][key].dtype == np.uint8:
|
| 67 |
+
data["image"][key] = data["image"][key].astype(np.float32) / 255.0 * 2.0 - 1.0
|
| 68 |
+
elif hasattr(data["image"][key], "dtype") and data["image"][key].dtype == torch.uint8:
|
| 69 |
+
data["image"][key] = data["image"][key].to(torch.float32).permute(0, 3, 1, 2) / 255.0 * 2.0 - 1.0
|
| 70 |
+
|
| 71 |
+
return cls(
|
| 72 |
+
images=data["image"],
|
| 73 |
+
image_masks=data["image_mask"],
|
| 74 |
+
state=data["state"],
|
| 75 |
+
tokenized_prompt=data.get("tokenized_prompt"),
|
| 76 |
+
tokenized_prompt_mask=data.get("tokenized_prompt_mask"),
|
| 77 |
+
token_ar_mask=data.get("token_ar_mask"),
|
| 78 |
+
token_loss_mask=data.get("token_loss_mask"),
|
| 79 |
+
fast_tokens=data.get("fast_tokens"),
|
| 80 |
+
fast_token_mask=data.get("fast_token_mask"),
|
| 81 |
+
spatial_tokens=data.get("spatial_tokens"),
|
| 82 |
+
spatial_token_mask=data.get("spatial_token_mask"),
|
| 83 |
+
da3_features=data.get("da3_features"),
|
| 84 |
+
da3_ray=data.get("da3_ray"),
|
| 85 |
+
da3_depth=data.get("da3_depth"),
|
| 86 |
+
camera_extrinsics=data.get("camera_extrinsics"),
|
| 87 |
+
lang_feat=data.get("lang_feat"),
|
| 88 |
+
lang_mask=data.get("lang_mask"),
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
def to_dict(self) -> at.PyTree[ArrayT]:
|
| 92 |
+
"""Convert Observation to dict."""
|
| 93 |
+
result = dataclasses.asdict(self)
|
| 94 |
+
result["image"] = result.pop("images")
|
| 95 |
+
result["image_mask"] = result.pop("image_masks")
|
| 96 |
+
return result
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def preprocess_observation(
|
| 100 |
+
rng: at.KeyArrayLike | None,
|
| 101 |
+
observation: Observation,
|
| 102 |
+
*,
|
| 103 |
+
train: bool = False,
|
| 104 |
+
image_keys: Sequence[str] = IMAGE_KEYS,
|
| 105 |
+
image_resolution: tuple[int, int] = IMAGE_RESOLUTION,
|
| 106 |
+
) -> Observation:
|
| 107 |
+
"""Preprocess observations with image augmentation and FAST fields preservation."""
|
| 108 |
+
if not set(image_keys).issubset(observation.images):
|
| 109 |
+
raise ValueError(f"images dict missing keys: expected {image_keys}, got {list(observation.images)}")
|
| 110 |
+
|
| 111 |
+
batch_shape = observation.state.shape[:-1]
|
| 112 |
+
|
| 113 |
+
out_images = {}
|
| 114 |
+
for key in image_keys:
|
| 115 |
+
image = observation.images[key]
|
| 116 |
+
if image.shape[1:3] != image_resolution:
|
| 117 |
+
image = image_tools.resize_with_pad(image, *image_resolution)
|
| 118 |
+
|
| 119 |
+
if train:
|
| 120 |
+
# Convert from [-1, 1] to [0, 1] for augmax
|
| 121 |
+
image = image / 2.0 + 0.5
|
| 122 |
+
|
| 123 |
+
transforms = []
|
| 124 |
+
if "wrist" not in key:
|
| 125 |
+
height, width = image.shape[1:3]
|
| 126 |
+
transforms += [
|
| 127 |
+
augmax.RandomCrop(int(width * 0.95), int(height * 0.95)),
|
| 128 |
+
augmax.Resize(width, height),
|
| 129 |
+
augmax.Rotate((-5, 5)),
|
| 130 |
+
]
|
| 131 |
+
transforms += [
|
| 132 |
+
augmax.ColorJitter(brightness=0.3, contrast=0.4, saturation=0.5),
|
| 133 |
+
]
|
| 134 |
+
sub_rngs = jax.random.split(rng, image.shape[0])
|
| 135 |
+
image = jax.vmap(augmax.Chain(*transforms))(sub_rngs, image)
|
| 136 |
+
|
| 137 |
+
# Back to [-1, 1]
|
| 138 |
+
image = image * 2.0 - 1.0
|
| 139 |
+
|
| 140 |
+
out_images[key] = image
|
| 141 |
+
|
| 142 |
+
# Obtain masks
|
| 143 |
+
out_masks = {}
|
| 144 |
+
for key in out_images:
|
| 145 |
+
if key not in observation.image_masks:
|
| 146 |
+
out_masks[key] = jnp.ones(batch_shape, dtype=jnp.bool)
|
| 147 |
+
else:
|
| 148 |
+
out_masks[key] = jnp.asarray(observation.image_masks[key])
|
| 149 |
+
|
| 150 |
+
return Observation(
|
| 151 |
+
images=out_images,
|
| 152 |
+
image_masks=out_masks,
|
| 153 |
+
state=observation.state,
|
| 154 |
+
tokenized_prompt=observation.tokenized_prompt,
|
| 155 |
+
tokenized_prompt_mask=observation.tokenized_prompt_mask,
|
| 156 |
+
token_ar_mask=observation.token_ar_mask,
|
| 157 |
+
token_loss_mask=observation.token_loss_mask,
|
| 158 |
+
fast_tokens=getattr(observation, 'fast_tokens', None),
|
| 159 |
+
fast_token_mask=getattr(observation, 'fast_token_mask', None),
|
| 160 |
+
spatial_tokens=getattr(observation, 'spatial_tokens', None),
|
| 161 |
+
da3_features=getattr(observation, 'da3_features', None),
|
| 162 |
+
da3_ray=getattr(observation, 'da3_ray', None),
|
| 163 |
+
da3_depth=getattr(observation, 'da3_depth', None),
|
| 164 |
+
camera_extrinsics=getattr(observation, 'camera_extrinsics', None),
|
| 165 |
+
lang_feat=getattr(observation, 'lang_feat', None),
|
| 166 |
+
lang_mask=getattr(observation, 'lang_mask', None),
|
| 167 |
+
spatial_token_mask=getattr(observation, 'spatial_token_mask', None),
|
| 168 |
+
)
|
pibehavior_da3_clean_up_your_desk_40k/source_code/model/pi_behavior.py
ADDED
|
@@ -0,0 +1,1270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The main model for BEHAVIOR-1K challenge.
|
| 2 |
+
|
| 3 |
+
Based on Pi0.5 implementation from PhysicalIntelligence/openpi
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
import pathlib
|
| 8 |
+
|
| 9 |
+
import einops
|
| 10 |
+
import flax.linen as nn
|
| 11 |
+
import flax.nnx as nnx
|
| 12 |
+
import flax.nnx.bridge as nnx_bridge
|
| 13 |
+
import jax
|
| 14 |
+
import jax.numpy as jnp
|
| 15 |
+
from typing_extensions import override
|
| 16 |
+
|
| 17 |
+
from openpi.models import model as _model
|
| 18 |
+
from openpi.models import gemma as _gemma
|
| 19 |
+
from b1k.models import spatial_da3 as _spatial_da3
|
| 20 |
+
from openpi.models import siglip as _siglip
|
| 21 |
+
from openpi.models.pi0 import make_attn_mask, posemb_sincos
|
| 22 |
+
from openpi.shared import array_typing as at
|
| 23 |
+
|
| 24 |
+
# Import from our custom modules
|
| 25 |
+
from b1k.models import pi_behavior_config
|
| 26 |
+
from b1k.models.observation import Observation, preprocess_observation
|
| 27 |
+
from b1k.models.pi_behavior_config import (
|
| 28 |
+
TASK_NUM_STAGES,
|
| 29 |
+
MAX_NUM_STAGES,
|
| 30 |
+
TOTAL_TASK_STAGE_EMBEDDINGS,
|
| 31 |
+
TASK_STAGE_OFFSETS
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
logger = logging.getLogger("b1k")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class KVCacheTransform(nnx.Module):
|
| 38 |
+
"""Transforms prefix KV cache by mixing across layers.
|
| 39 |
+
|
| 40 |
+
Each destination layer's K and V become learnable linear combinations
|
| 41 |
+
of all source layers' K and V, plus a bias term. This allows the action
|
| 42 |
+
expert to attend to learned combinations of VLM layers rather than being
|
| 43 |
+
forced to attend layer-by-layer.
|
| 44 |
+
|
| 45 |
+
Initialized as identity transform (k_coeffs = I, bias = 0) so the model
|
| 46 |
+
starts with the same behavior as without transformation.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, num_layers: int, head_dim: int, num_kv_heads: int, rngs: nnx.Rngs):
|
| 50 |
+
# K transformation: [dest_layer, src_layer]
|
| 51 |
+
# Initialize as identity so transformation is initially a no-op
|
| 52 |
+
self.k_coeffs = nnx.Param(jnp.eye(num_layers, dtype=jnp.float32))
|
| 53 |
+
|
| 54 |
+
# K bias: [layer, num_kv_heads, head_dim]
|
| 55 |
+
# Initialize as zeros
|
| 56 |
+
self.k_bias = nnx.Param(jnp.zeros((num_layers, num_kv_heads, head_dim), dtype=jnp.float32))
|
| 57 |
+
|
| 58 |
+
# V transformation (independent from K)
|
| 59 |
+
self.v_coeffs = nnx.Param(jnp.eye(num_layers, dtype=jnp.float32))
|
| 60 |
+
self.v_bias = nnx.Param(jnp.zeros((num_layers, num_kv_heads, head_dim), dtype=jnp.float32))
|
| 61 |
+
|
| 62 |
+
def __call__(self, kv_cache: tuple[jnp.ndarray, jnp.ndarray]) -> tuple[jnp.ndarray, jnp.ndarray]:
|
| 63 |
+
"""Transform KV cache by mixing across layers.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
kv_cache: Tuple of (cache_k, cache_v) where each has shape
|
| 67 |
+
[num_layers, batch, seq_len, num_kv_heads, head_dim]
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
Transformed (k_new, v_new) with same shape and dtype as input
|
| 71 |
+
"""
|
| 72 |
+
cache_k, cache_v = kv_cache
|
| 73 |
+
# Shape: [layers, batch, seq_len, num_kv_heads, head_dim]
|
| 74 |
+
|
| 75 |
+
# Preserve original dtype (important for bfloat16 training)
|
| 76 |
+
original_dtype = cache_k.dtype
|
| 77 |
+
|
| 78 |
+
# Transform K: each destination layer is a weighted combination of all source layers
|
| 79 |
+
# k_new[dest] = sum_src(k_coeffs[dest, src] * cache_k[src]) + k_bias[dest]
|
| 80 |
+
# Einsum: [dest, src] @ [src, batch, seq, heads, dim] -> [dest, batch, seq, heads, dim]
|
| 81 |
+
k_new = jnp.einsum('ds,sbtkh->dbtkh', self.k_coeffs.value, cache_k)
|
| 82 |
+
k_new = k_new + self.k_bias.value[:, None, None, :, :] # Add bias
|
| 83 |
+
|
| 84 |
+
# Transform V (same operation, independent parameters)
|
| 85 |
+
v_new = jnp.einsum('ds,sbtkh->dbtkh', self.v_coeffs.value, cache_v)
|
| 86 |
+
v_new = v_new + self.v_bias.value[:, None, None, :, :]
|
| 87 |
+
|
| 88 |
+
# Cast back to original dtype
|
| 89 |
+
k_new = k_new.astype(original_dtype)
|
| 90 |
+
v_new = v_new.astype(original_dtype)
|
| 91 |
+
|
| 92 |
+
return (k_new, v_new)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class SpatialActionCrossAttention(nnx.Module):
|
| 96 |
+
"""Residual cross-attention from action tokens to precomputed DA3 spatial tokens."""
|
| 97 |
+
|
| 98 |
+
def __init__(
|
| 99 |
+
self,
|
| 100 |
+
action_width: int,
|
| 101 |
+
spatial_width: int,
|
| 102 |
+
num_heads: int,
|
| 103 |
+
rngs: nnx.Rngs,
|
| 104 |
+
):
|
| 105 |
+
if action_width % num_heads != 0:
|
| 106 |
+
raise ValueError(f"action_width={action_width} must be divisible by num_heads={num_heads}")
|
| 107 |
+
|
| 108 |
+
self.num_heads = num_heads
|
| 109 |
+
self.head_dim = action_width // num_heads
|
| 110 |
+
self.q_proj = nnx.Linear(action_width, action_width, use_bias=False, rngs=rngs)
|
| 111 |
+
self.k_proj = nnx.Linear(spatial_width, action_width, use_bias=False, rngs=rngs)
|
| 112 |
+
self.v_proj = nnx.Linear(spatial_width, action_width, use_bias=False, rngs=rngs)
|
| 113 |
+
self.out_proj = nnx.Linear(
|
| 114 |
+
action_width,
|
| 115 |
+
action_width,
|
| 116 |
+
kernel_init=nn.initializers.zeros,
|
| 117 |
+
bias_init=nn.initializers.zeros,
|
| 118 |
+
rngs=rngs,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
def __call__(
|
| 122 |
+
self,
|
| 123 |
+
action_tokens: jnp.ndarray,
|
| 124 |
+
spatial_tokens: jnp.ndarray,
|
| 125 |
+
spatial_token_mask: jnp.ndarray | None = None,
|
| 126 |
+
*,
|
| 127 |
+
residual_scale: float = 1.0,
|
| 128 |
+
) -> jnp.ndarray:
|
| 129 |
+
original_dtype = action_tokens.dtype
|
| 130 |
+
q = self.q_proj(action_tokens)
|
| 131 |
+
k = self.k_proj(spatial_tokens.astype(action_tokens.dtype))
|
| 132 |
+
v = self.v_proj(spatial_tokens.astype(action_tokens.dtype))
|
| 133 |
+
|
| 134 |
+
q = einops.rearrange(q, "b t (h d) -> b h t d", h=self.num_heads)
|
| 135 |
+
k = einops.rearrange(k, "b s (h d) -> b h s d", h=self.num_heads)
|
| 136 |
+
v = einops.rearrange(v, "b s (h d) -> b h s d", h=self.num_heads)
|
| 137 |
+
|
| 138 |
+
logits = jnp.einsum("bhtd,bhsd->bhts", q, k, preferred_element_type=jnp.float32)
|
| 139 |
+
logits = logits * (self.head_dim ** -0.5)
|
| 140 |
+
|
| 141 |
+
if spatial_token_mask is not None:
|
| 142 |
+
big_neg = -2.3819763e38
|
| 143 |
+
logits = jnp.where(spatial_token_mask[:, None, None, :], logits, big_neg)
|
| 144 |
+
|
| 145 |
+
probs = jax.nn.softmax(logits, axis=-1).astype(original_dtype)
|
| 146 |
+
context = jnp.einsum("bhts,bhsd->bhtd", probs, v)
|
| 147 |
+
context = einops.rearrange(context, "b h t d -> b t (h d)")
|
| 148 |
+
delta = self.out_proj(context).astype(original_dtype)
|
| 149 |
+
return action_tokens + residual_scale * delta
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class PiBehavior(_model.BaseModel):
|
| 153 |
+
def __init__(self, config: pi_behavior_config.PiBehaviorConfig, rngs: nnx.Rngs):
|
| 154 |
+
super().__init__(config.action_dim, config.action_horizon, config.max_token_len)
|
| 155 |
+
|
| 156 |
+
# Store config for later use
|
| 157 |
+
self.config = config
|
| 158 |
+
|
| 159 |
+
paligemma_config = _gemma.get_config(config.paligemma_variant)
|
| 160 |
+
action_expert_config = _gemma.get_config(config.action_expert_variant)
|
| 161 |
+
|
| 162 |
+
# Initialize Gemma models with AdaRMS (Pi05 style)
|
| 163 |
+
spatial_inject = getattr(config, "da3", None) is not None and config.da3.enabled
|
| 164 |
+
self.da3_cfg = getattr(config, "da3", None)
|
| 165 |
+
llm = nnx_bridge.ToNNX(
|
| 166 |
+
_gemma.Module(
|
| 167 |
+
configs=[paligemma_config, action_expert_config],
|
| 168 |
+
embed_dtype=config.dtype,
|
| 169 |
+
adarms=True,
|
| 170 |
+
spatial_inject=spatial_inject,
|
| 171 |
+
num_spatial_layers=config.da3.num_inject_layers if spatial_inject else 6,
|
| 172 |
+
spatial_scale=config.da3.spatial_scale if spatial_inject else 2.0,
|
| 173 |
+
spatial_init_std=config.da3.spatial_init_std if spatial_inject else 0.0,
|
| 174 |
+
spatial_logit_gain=config.da3.attn_logit_gain if spatial_inject else False,
|
| 175 |
+
spatial_logit_gain_init=config.da3.attn_logit_gain_init if spatial_inject else 1.0,
|
| 176 |
+
)
|
| 177 |
+
)
|
| 178 |
+
llm.lazy_init(rngs=rngs, method="init", use_adarms=[False, True])
|
| 179 |
+
|
| 180 |
+
# Initialize vision model
|
| 181 |
+
img = nnx_bridge.ToNNX(
|
| 182 |
+
_siglip.Module(
|
| 183 |
+
num_classes=paligemma_config.width,
|
| 184 |
+
variant="So400m/14",
|
| 185 |
+
pool_type="none",
|
| 186 |
+
scan=True,
|
| 187 |
+
dtype_mm=config.dtype,
|
| 188 |
+
)
|
| 189 |
+
)
|
| 190 |
+
img.lazy_init(next(iter(config.fake_obs().images.values())), train=False, rngs=rngs)
|
| 191 |
+
|
| 192 |
+
self.PaliGemma = nnx.Dict(llm=llm, img=img)
|
| 193 |
+
|
| 194 |
+
# DA3 spatial-language bank builder (trainable; frozen DA3 runs inline in the data pipeline).
|
| 195 |
+
self.spatial_bank_builder = None
|
| 196 |
+
if spatial_inject:
|
| 197 |
+
d = config.da3
|
| 198 |
+
self.spatial_bank_builder = _spatial_da3.SpatialBankBuilder(
|
| 199 |
+
hidden_dim=d.hidden_dim,
|
| 200 |
+
da3_channels=d.da3_channels,
|
| 201 |
+
num_layers=d.da3_layers,
|
| 202 |
+
grid_hw=d.grid_hw,
|
| 203 |
+
lang_dim=d.lang_dim,
|
| 204 |
+
num_heads=d.num_heads,
|
| 205 |
+
lang_fusion_depth=d.lang_fusion_depth,
|
| 206 |
+
perceiver_query_std=d.perceiver_query_std,
|
| 207 |
+
bank_token_embed=d.bank_token_embed,
|
| 208 |
+
rngs=rngs,
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
# KV cache transformation for cross-layer attention
|
| 212 |
+
# Allows action expert to attend to learned combinations of VLM layers
|
| 213 |
+
if config.use_kv_transform:
|
| 214 |
+
self.kv_transform = KVCacheTransform(
|
| 215 |
+
num_layers=paligemma_config.depth,
|
| 216 |
+
head_dim=paligemma_config.head_dim,
|
| 217 |
+
num_kv_heads=paligemma_config.num_kv_heads,
|
| 218 |
+
rngs=rngs
|
| 219 |
+
)
|
| 220 |
+
else:
|
| 221 |
+
self.kv_transform = None
|
| 222 |
+
|
| 223 |
+
# Task embeddings table - trainable embeddings for each task
|
| 224 |
+
self.task_embeddings = nnx.Embed(
|
| 225 |
+
num_embeddings=config.num_tasks,
|
| 226 |
+
features=config.task_embedding_dim,
|
| 227 |
+
rngs=rngs,
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Stage predictor - predicts stage from VLM output of base task token
|
| 231 |
+
# Outputs MAX_NUM_STAGES logits, but invalid stages are masked per task
|
| 232 |
+
self.stage_pred_from_vlm = nnx.Linear(paligemma_config.width, MAX_NUM_STAGES, rngs=rngs)
|
| 233 |
+
|
| 234 |
+
# Task + subtask fusion layers
|
| 235 |
+
# Combines task embedding + cos/sin encoded subtask state
|
| 236 |
+
self.subtask_encoding_dim = config.task_embedding_dim // 2 # Half of task embedding dim (1024)
|
| 237 |
+
|
| 238 |
+
# Task-specific stage embeddings (one per stage per task)
|
| 239 |
+
# Total embeddings = sum of stages across all tasks (596 for 5-15 stages per task)
|
| 240 |
+
self.task_stage_embeddings = nnx.Embed(
|
| 241 |
+
num_embeddings=TOTAL_TASK_STAGE_EMBEDDINGS,
|
| 242 |
+
features=self.subtask_encoding_dim,
|
| 243 |
+
rngs=rngs,
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# Gated fusion layers
|
| 247 |
+
# Input: task_embedding + sincos + task_stage_emb = task_dim + 2*subtask_dim
|
| 248 |
+
fusion_input_dim = config.task_embedding_dim + 2 * self.subtask_encoding_dim
|
| 249 |
+
|
| 250 |
+
# Gate networks to learn how to combine different signals
|
| 251 |
+
self.gate_sincos = nnx.Linear(fusion_input_dim, self.subtask_encoding_dim, rngs=rngs)
|
| 252 |
+
self.gate_task_stage = nnx.Linear(fusion_input_dim, self.subtask_encoding_dim, rngs=rngs)
|
| 253 |
+
self.gate_task = nnx.Linear(fusion_input_dim, config.task_embedding_dim, rngs=rngs)
|
| 254 |
+
|
| 255 |
+
# Fusion networks to create multiple conditioned vectors
|
| 256 |
+
self.fusion_layer1 = nnx.Linear(fusion_input_dim, config.task_embedding_dim * 2, rngs=rngs)
|
| 257 |
+
self.fusion_layer2 = nnx.Linear(config.task_embedding_dim * 2, config.task_embedding_dim, rngs=rngs)
|
| 258 |
+
|
| 259 |
+
# Additional projection for stage-dominant representation (2 signals now)
|
| 260 |
+
self.stage_projection = nnx.Linear(2 * self.subtask_encoding_dim, config.task_embedding_dim, rngs=rngs)
|
| 261 |
+
|
| 262 |
+
# Pi05 style layers
|
| 263 |
+
self.action_in_proj = nnx.Linear(config.action_dim, action_expert_config.width, rngs=rngs)
|
| 264 |
+
self.time_mlp_in = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
|
| 265 |
+
self.time_mlp_out = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
|
| 266 |
+
self.action_out_proj = nnx.Linear(action_expert_config.width, config.action_dim, rngs=rngs)
|
| 267 |
+
if config.use_spatial_action_cross_attention:
|
| 268 |
+
self.spatial_action_xattn = SpatialActionCrossAttention(
|
| 269 |
+
action_width=action_expert_config.width,
|
| 270 |
+
spatial_width=config.spatial_token_dim,
|
| 271 |
+
num_heads=config.spatial_num_heads,
|
| 272 |
+
rngs=rngs,
|
| 273 |
+
)
|
| 274 |
+
logger.info(
|
| 275 |
+
"DA3 spatial action cross-attention enabled: tokens=%s dim=%s heads=%s scale=%s",
|
| 276 |
+
config.spatial_num_tokens,
|
| 277 |
+
config.spatial_token_dim,
|
| 278 |
+
config.spatial_num_heads,
|
| 279 |
+
config.spatial_residual_scale,
|
| 280 |
+
)
|
| 281 |
+
else:
|
| 282 |
+
self.spatial_action_xattn = None
|
| 283 |
+
|
| 284 |
+
# Correlated noise generation
|
| 285 |
+
# Initialize as NNX Intermediate (excluded from checkpoints, loaded from norm_stats)
|
| 286 |
+
# Full correlation matrix with beta shrinkage for robustness
|
| 287 |
+
flat_dim = config.action_horizon * config.action_dim
|
| 288 |
+
self.action_correlation_cholesky = nnx.Intermediate(
|
| 289 |
+
jnp.eye(flat_dim), # Identity matrix as placeholder
|
| 290 |
+
)
|
| 291 |
+
self.correlation_loaded = False # Track if correlation matrix has been loaded
|
| 292 |
+
self.use_correlated_noise = config.use_correlated_noise
|
| 293 |
+
self.correlation_beta = config.correlation_beta # Shrinkage parameter for regularization
|
| 294 |
+
|
| 295 |
+
# Inpainting cache: stores precomputed matrices for simple correlation-based inpainting
|
| 296 |
+
# Key: num_inpainted_steps (length of inpainted sequence)
|
| 297 |
+
# Value: dict with {O_indices, U_indices, Sigma_UO_SOOinv}
|
| 298 |
+
self.inpainting_cache = {}
|
| 299 |
+
|
| 300 |
+
# FAST auxiliary training components
|
| 301 |
+
if config.use_fast_auxiliary:
|
| 302 |
+
# FAST embedding layer (vocab_size → paligemma_width)
|
| 303 |
+
# Use paligemma width (2048) to match other prefix tokens
|
| 304 |
+
self.fast_token_embedding = nnx.Embed(
|
| 305 |
+
num_embeddings=config.fast_vocab_size,
|
| 306 |
+
features=paligemma_config.width,
|
| 307 |
+
rngs=rngs
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# FAST projection head (paligemma_width → vocab_size)
|
| 311 |
+
self.fast_token_proj = nnx.Linear(
|
| 312 |
+
paligemma_config.width,
|
| 313 |
+
config.fast_vocab_size,
|
| 314 |
+
rngs=rngs
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
logger.info(f"FAST auxiliary enabled, vocab_size={config.fast_vocab_size}")
|
| 318 |
+
|
| 319 |
+
# This attribute gets automatically set by model.train() and model.eval().
|
| 320 |
+
self.deterministic = True
|
| 321 |
+
|
| 322 |
+
def _compute_banks(self, observation):
|
| 323 |
+
"""Build the per-view DA3 spatial banks once per forward (timestep-independent; reused
|
| 324 |
+
across all flow samples / denoise steps)."""
|
| 325 |
+
if self.spatial_bank_builder is None or getattr(observation, "da3_features", None) is None:
|
| 326 |
+
return None
|
| 327 |
+
feats = observation.da3_features
|
| 328 |
+
# Features arrive as raw BITS to minimize host<->device transfer; decode on-device to bf16.
|
| 329 |
+
# uint16 = bf16 bits (inline extractor); uint8 = fp8-e4m3fn bytes (legacy cache).
|
| 330 |
+
if feats.dtype == jnp.uint8:
|
| 331 |
+
feats = jax.lax.bitcast_convert_type(feats, jnp.float8_e4m3fn).astype(jnp.bfloat16)
|
| 332 |
+
elif feats.dtype == jnp.uint16:
|
| 333 |
+
feats = jax.lax.bitcast_convert_type(feats, jnp.bfloat16)
|
| 334 |
+
else:
|
| 335 |
+
feats = feats.astype(jnp.bfloat16)
|
| 336 |
+
return self.spatial_bank_builder(
|
| 337 |
+
feats,
|
| 338 |
+
observation.da3_ray,
|
| 339 |
+
observation.da3_depth,
|
| 340 |
+
observation.camera_extrinsics,
|
| 341 |
+
observation.lang_feat,
|
| 342 |
+
observation.lang_mask,
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
def apply_spatial_action_conditioning(self, observation: Observation, action_tokens: jnp.ndarray) -> jnp.ndarray:
|
| 346 |
+
"""Inject precomputed DA3 spatial tokens into action-token hidden states."""
|
| 347 |
+
if self.spatial_action_xattn is None or observation.spatial_tokens is None:
|
| 348 |
+
return action_tokens
|
| 349 |
+
|
| 350 |
+
return self.spatial_action_xattn(
|
| 351 |
+
action_tokens,
|
| 352 |
+
observation.spatial_tokens,
|
| 353 |
+
observation.spatial_token_mask,
|
| 354 |
+
residual_scale=self.config.spatial_residual_scale,
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
def encode_subtask_state(
|
| 358 |
+
self,
|
| 359 |
+
subtask_state: at.Int[at.Array, " b"],
|
| 360 |
+
task_ids: at.Int[at.Array, " b"]
|
| 361 |
+
) -> at.Float[at.Array, "b {self.subtask_encoding_dim}"]:
|
| 362 |
+
"""Encode subtask state using cos/sin positional encoding, scaled per task.
|
| 363 |
+
|
| 364 |
+
Args:
|
| 365 |
+
subtask_state: Current stage for each sample [B]
|
| 366 |
+
task_ids: Task ID for each sample [B]
|
| 367 |
+
|
| 368 |
+
Returns:
|
| 369 |
+
Positional encodings scaled to [0, 1] range based on task-specific stage count [B, 1024]
|
| 370 |
+
"""
|
| 371 |
+
# Get number of stages for each task in batch using JAX array indexing
|
| 372 |
+
# Convert tuple to JAX array inside function to avoid import-time device allocation
|
| 373 |
+
task_num_stages_array = jnp.array(TASK_NUM_STAGES, dtype=jnp.int32)
|
| 374 |
+
task_num_stages = task_num_stages_array[task_ids] # [B] - JAX array indexing
|
| 375 |
+
|
| 376 |
+
# Normalize: stage 0 → 0.0, last stage → 1.0 (per-task scaling)
|
| 377 |
+
# Add maximum to avoid division by zero for edge cases
|
| 378 |
+
normalized_state = subtask_state.astype(jnp.float32) / jnp.maximum(task_num_stages.astype(jnp.float32) - 1.0, 1.0)
|
| 379 |
+
|
| 380 |
+
# Use cos/sin encoding similar to timestep encoding
|
| 381 |
+
return posemb_sincos(
|
| 382 |
+
normalized_state,
|
| 383 |
+
self.subtask_encoding_dim,
|
| 384 |
+
min_period=1e-3,
|
| 385 |
+
max_period=1.0
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
def load_correlation_matrix(self, norm_stats: dict):
|
| 389 |
+
"""Load full correlation matrix from normalization statistics and apply shrinkage.
|
| 390 |
+
|
| 391 |
+
This should be called after model initialization when norm_stats are available.
|
| 392 |
+
Applies shrinkage regularization: S_reg = beta * S + (1-beta) * I for robustness.
|
| 393 |
+
|
| 394 |
+
Args:
|
| 395 |
+
norm_stats: Dictionary containing normalization statistics (from normalize.load()),
|
| 396 |
+
with 'actions' key containing NormStats with action_correlation_cholesky field.
|
| 397 |
+
|
| 398 |
+
Raises:
|
| 399 |
+
ValueError: If use_correlated_noise=True but correlation matrix is missing.
|
| 400 |
+
TypeError: If norm_stats structure is incorrect.
|
| 401 |
+
"""
|
| 402 |
+
if not self.use_correlated_noise:
|
| 403 |
+
logger.info("Correlated noise disabled in config, skipping correlation matrix loading")
|
| 404 |
+
return
|
| 405 |
+
|
| 406 |
+
# Validate norm_stats is a dict
|
| 407 |
+
if not isinstance(norm_stats, dict):
|
| 408 |
+
raise TypeError(
|
| 409 |
+
f"norm_stats must be a dict, got {type(norm_stats).__name__}. "
|
| 410 |
+
"Ensure norm_stats are loaded using openpi.shared.normalize.load()."
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# Check 'actions' key exists
|
| 414 |
+
if 'actions' not in norm_stats:
|
| 415 |
+
raise ValueError(
|
| 416 |
+
"use_correlated_noise=True but 'actions' key not found in norm_stats. "
|
| 417 |
+
f"Found keys: {list(norm_stats.keys())}. "
|
| 418 |
+
"Run compute_norm_stats.py with --correlation flag to generate correlation matrix."
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
actions_stats = norm_stats['actions']
|
| 422 |
+
|
| 423 |
+
# Extract correlation matrix (support both dict and attribute access for flexibility)
|
| 424 |
+
if isinstance(actions_stats, dict):
|
| 425 |
+
chol_matrix = actions_stats.get('action_correlation_cholesky')
|
| 426 |
+
access_method = "dict"
|
| 427 |
+
elif hasattr(actions_stats, 'action_correlation_cholesky'):
|
| 428 |
+
chol_matrix = actions_stats.action_correlation_cholesky
|
| 429 |
+
access_method = "attribute"
|
| 430 |
+
else:
|
| 431 |
+
raise TypeError(
|
| 432 |
+
f"norm_stats['actions'] has unexpected type {type(actions_stats).__name__} "
|
| 433 |
+
f"and cannot access 'action_correlation_cholesky'. "
|
| 434 |
+
"Ensure norm_stats are loaded using openpi.shared.normalize.load()."
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
# Strict validation: correlation matrix must exist and be non-None
|
| 438 |
+
if chol_matrix is None:
|
| 439 |
+
raise ValueError(
|
| 440 |
+
"use_correlated_noise=True but 'action_correlation_cholesky' is None in norm_stats['actions']. "
|
| 441 |
+
"This means the correlation matrix was not computed during norm_stats generation. "
|
| 442 |
+
"Run compute_norm_stats.py with --correlation flag to generate correlation matrix."
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
logger.info(f"Successfully accessed correlation matrix via {access_method} access")
|
| 446 |
+
|
| 447 |
+
# Validate correlation matrix shape
|
| 448 |
+
expected_dim = self.action_horizon * self.action_dim
|
| 449 |
+
try:
|
| 450 |
+
L = jnp.array(chol_matrix)
|
| 451 |
+
except Exception as e:
|
| 452 |
+
raise ValueError(
|
| 453 |
+
f"Failed to convert action_correlation_cholesky to array: {e}. "
|
| 454 |
+
"The correlation matrix may be corrupted or in an invalid format."
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
if L.ndim != 2 or L.shape[0] != L.shape[1]:
|
| 458 |
+
raise ValueError(
|
| 459 |
+
f"action_correlation_cholesky must be a square 2D matrix, got shape {L.shape}. "
|
| 460 |
+
f"Expected shape: ({expected_dim}, {expected_dim})"
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
if L.shape[0] != expected_dim:
|
| 464 |
+
raise ValueError(
|
| 465 |
+
f"action_correlation_cholesky has wrong dimensions: {L.shape[0]}x{L.shape[0]}. "
|
| 466 |
+
f"Expected {expected_dim}x{expected_dim} (action_horizon={self.action_horizon} * action_dim={self.action_dim}). "
|
| 467 |
+
"This indicates the correlation matrix was computed for a different action space configuration."
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
+
# Reconstruct covariance matrix from Cholesky
|
| 471 |
+
Sigma = L @ L.T
|
| 472 |
+
|
| 473 |
+
# Apply shrinkage regularization: Σ_reg = beta * Σ + (1-beta) * I
|
| 474 |
+
beta = self.correlation_beta
|
| 475 |
+
logger.info(f"Applying shrinkage regularization with beta={beta:.2f}")
|
| 476 |
+
|
| 477 |
+
Sigma_reg = beta * Sigma + (1 - beta) * jnp.eye(Sigma.shape[0])
|
| 478 |
+
|
| 479 |
+
# Compute Cholesky decomposition of regularized covariance
|
| 480 |
+
try:
|
| 481 |
+
L_reg = jnp.linalg.cholesky(Sigma_reg)
|
| 482 |
+
except Exception as e:
|
| 483 |
+
raise RuntimeError(
|
| 484 |
+
f"Cholesky decomposition failed on regularized covariance: {e}. "
|
| 485 |
+
"This indicates the regularized correlation matrix is not positive definite. "
|
| 486 |
+
f"Current beta={beta:.2f}. Try decreasing correlation_beta closer to 0.0 for more shrinkage/regularization."
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
# Update the Intermediate value
|
| 490 |
+
self.action_correlation_cholesky.value = L_reg
|
| 491 |
+
self.correlation_loaded = True
|
| 492 |
+
|
| 493 |
+
logger.info(
|
| 494 |
+
f"✓ Loaded correlation matrix with shape {L_reg.shape} "
|
| 495 |
+
f"(beta={beta:.2f} shrinkage applied)"
|
| 496 |
+
)
|
| 497 |
+
logger.info(
|
| 498 |
+
f" Memory usage: {L_reg.nbytes / 1024 / 1024:.2f} MB"
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
def generate_correlated_noise(
|
| 502 |
+
self,
|
| 503 |
+
rng: at.KeyArrayLike,
|
| 504 |
+
batch_size: int,
|
| 505 |
+
) -> at.Float[at.Array, "b {self.action_horizon} {self.action_dim}"]:
|
| 506 |
+
"""Generate correlated noise matching action covariance structure.
|
| 507 |
+
|
| 508 |
+
Uses full correlation matrix with optional beta shrinkage for robustness.
|
| 509 |
+
|
| 510 |
+
Args:
|
| 511 |
+
rng: Random key for noise generation
|
| 512 |
+
batch_size: Number of noise samples to generate
|
| 513 |
+
|
| 514 |
+
Returns:
|
| 515 |
+
Correlated noise with shape [batch_size, action_horizon, action_dim]
|
| 516 |
+
|
| 517 |
+
Raises:
|
| 518 |
+
RuntimeError: If use_correlated_noise=True but correlation matrix not loaded.
|
| 519 |
+
"""
|
| 520 |
+
if not self.use_correlated_noise:
|
| 521 |
+
# Independent Gaussian noise when correlated noise is disabled
|
| 522 |
+
return jax.random.normal(rng, (batch_size, self.action_horizon, self.action_dim))
|
| 523 |
+
|
| 524 |
+
if not self.correlation_loaded:
|
| 525 |
+
raise RuntimeError(
|
| 526 |
+
"use_correlated_noise=True but correlation matrix is not loaded. "
|
| 527 |
+
"Ensure load_correlation_matrix() was called during model initialization. "
|
| 528 |
+
"Run compute_norm_stats.py with --correlation flag to generate correlation matrix."
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
# Generate standard correlated noise using Cholesky decomposition
|
| 532 |
+
flat_dim = self.action_horizon * self.action_dim
|
| 533 |
+
standard_normal = jax.random.normal(rng, (batch_size, flat_dim))
|
| 534 |
+
correlated_flat = standard_normal @ self.action_correlation_cholesky.value.T
|
| 535 |
+
correlated_noise = correlated_flat.reshape(batch_size, self.action_horizon, self.action_dim)
|
| 536 |
+
return correlated_noise
|
| 537 |
+
|
| 538 |
+
def _precompute_correction_matrix(
|
| 539 |
+
self,
|
| 540 |
+
O_indices: at.Int[at.Array, " nO"],
|
| 541 |
+
U_indices: at.Int[at.Array, " nU"],
|
| 542 |
+
) -> dict:
|
| 543 |
+
"""Precompute matrix for correlation-aware inpainting correction.
|
| 544 |
+
|
| 545 |
+
Computes Σ_{UO}Σ_{OO}^{-1} which propagates corrections from O to U
|
| 546 |
+
while preserving correlation structure.
|
| 547 |
+
|
| 548 |
+
Args:
|
| 549 |
+
O_indices: Flat indices of inpainted dimensions [|O|]
|
| 550 |
+
U_indices: Flat indices of free dimensions [|U|]
|
| 551 |
+
|
| 552 |
+
Returns:
|
| 553 |
+
Dictionary with {O_indices, U_indices, correction_matrix}
|
| 554 |
+
|
| 555 |
+
Raises:
|
| 556 |
+
RuntimeError: If correlation matrix is not loaded
|
| 557 |
+
"""
|
| 558 |
+
if not self.correlation_loaded:
|
| 559 |
+
raise RuntimeError(
|
| 560 |
+
"Cannot precompute correction matrix: correlation matrix not loaded. "
|
| 561 |
+
"Call load_correlation_matrix() first."
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
L = self.action_correlation_cholesky.value
|
| 565 |
+
Sigma = L @ L.T # Full covariance matrix [hd, hd]
|
| 566 |
+
|
| 567 |
+
# Extract submatrices
|
| 568 |
+
Sigma_OO = Sigma[jnp.ix_(O_indices, O_indices)] # [|O|, |O|]
|
| 569 |
+
Sigma_UO = Sigma[jnp.ix_(U_indices, O_indices)] # [|U|, |O|]
|
| 570 |
+
|
| 571 |
+
# Compute correction matrix: Σ_{UO} @ Σ_{OO}^{-1}
|
| 572 |
+
# This propagates corrections from O to U
|
| 573 |
+
eps_OO = 1e-6 * jnp.maximum(jnp.mean(jnp.diag(Sigma_OO)), 1.0)
|
| 574 |
+
Sigma_OO_reg = Sigma_OO + eps_OO * jnp.eye(Sigma_OO.shape[0])
|
| 575 |
+
|
| 576 |
+
# Solve Σ_{OO}_reg @ X = Σ_{UO}.T for X, then transpose
|
| 577 |
+
correction_matrix = jax.scipy.linalg.solve(
|
| 578 |
+
Sigma_OO_reg, Sigma_UO.T, assume_a='pos'
|
| 579 |
+
).T # [|U|, |O|]
|
| 580 |
+
|
| 581 |
+
return {
|
| 582 |
+
'O_indices': O_indices,
|
| 583 |
+
'U_indices': U_indices,
|
| 584 |
+
'correction_matrix': correction_matrix, # Σ_{UO}Σ_{OO}^{-1}
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
def fuse_task_and_subtask(
|
| 588 |
+
self, task_embedding: at.Float[at.Array, "b d"], task_ids: at.Int[at.Array, " b"], subtask_state: at.Int[at.Array, " b"]
|
| 589 |
+
) -> at.Float[at.Array, "b n d"]:
|
| 590 |
+
"""Fuse task embedding with subtask state encoding using multiple representations.
|
| 591 |
+
|
| 592 |
+
Returns multiple vectors that are differently conditioned by the subtask state:
|
| 593 |
+
1. Task-gated representation (task embedding modulated by subtask)
|
| 594 |
+
2. Balanced fusion (task + subtask combined)
|
| 595 |
+
3. Stage-dominant representation (subtask features projected to task space)
|
| 596 |
+
4. Pure stage representation (concatenated learned embeddings)
|
| 597 |
+
|
| 598 |
+
All output representations have dimension 2048 (task_embedding_dim).
|
| 599 |
+
|
| 600 |
+
Args:
|
| 601 |
+
task_embedding: Base task embedding [b, 2048]
|
| 602 |
+
task_ids: Task IDs for task-specific stage embeddings [b]
|
| 603 |
+
subtask_state: Subtask state indices [b]
|
| 604 |
+
|
| 605 |
+
Returns:
|
| 606 |
+
Multiple fused embeddings [b, 4, 2048]
|
| 607 |
+
"""
|
| 608 |
+
# Get subtask representations
|
| 609 |
+
sincos_encoding = self.encode_subtask_state(subtask_state, task_ids) # [b, 1024]
|
| 610 |
+
|
| 611 |
+
# Task-specific stage embedding with corrected indexing
|
| 612 |
+
# Use vectorized lookup: offset + stage for each task
|
| 613 |
+
# Convert tuple to JAX array inside function to avoid import-time device allocation
|
| 614 |
+
task_stage_offsets_array = jnp.array(TASK_STAGE_OFFSETS, dtype=jnp.int32)
|
| 615 |
+
task_stage_offsets = task_stage_offsets_array[task_ids] # [b] - JAX array indexing
|
| 616 |
+
task_stage_idx = task_stage_offsets + subtask_state # [b]
|
| 617 |
+
task_stage_embedding = self.task_stage_embeddings(task_stage_idx) # [b, 1024]
|
| 618 |
+
|
| 619 |
+
# Concatenate inputs for gating: task (2048) + sincos (1024) + task_stage (1024) = 4096
|
| 620 |
+
all_inputs = jnp.concatenate([
|
| 621 |
+
task_embedding, # [b, 2048]
|
| 622 |
+
sincos_encoding, # [b, 1024]
|
| 623 |
+
task_stage_embedding # [b, 1024]
|
| 624 |
+
], axis=-1) # [b, 4096]
|
| 625 |
+
|
| 626 |
+
# Learn gates for each component (sigmoid to get 0-1 scaling)
|
| 627 |
+
gate_sincos = nnx.sigmoid(self.gate_sincos(all_inputs)) # [b, 1024]
|
| 628 |
+
gate_task_stage = nnx.sigmoid(self.gate_task_stage(all_inputs)) # [b, 1024]
|
| 629 |
+
gate_task = nnx.sigmoid(self.gate_task(all_inputs)) # [b, 2048]
|
| 630 |
+
|
| 631 |
+
# 1. Task-gated representation: task embedding modulated by subtask info [b, 2048]
|
| 632 |
+
task_gated = task_embedding * gate_task
|
| 633 |
+
|
| 634 |
+
# 2. Balanced fusion: combine all signals through fusion network [b, 2048]
|
| 635 |
+
x = self.fusion_layer1(all_inputs) # [b, 4096]
|
| 636 |
+
x = nnx.relu(x)
|
| 637 |
+
balanced_fusion = self.fusion_layer2(x) # [b, 2048]
|
| 638 |
+
|
| 639 |
+
# 3. Stage-dominant: weighted combination of stage signals, then project [b, 2048]
|
| 640 |
+
gated_stage_features = jnp.concatenate([
|
| 641 |
+
sincos_encoding * gate_sincos, # [b, 1024]
|
| 642 |
+
task_stage_embedding * gate_task_stage # [b, 1024]
|
| 643 |
+
], axis=-1) # [b, 2048]
|
| 644 |
+
stage_dominant = self.stage_projection(gated_stage_features) # [b, 2048]
|
| 645 |
+
|
| 646 |
+
# 4. Pure stage: concatenate the embeddings (already 2048) [b, 2048]
|
| 647 |
+
pure_stage = jnp.concatenate([sincos_encoding, task_stage_embedding], axis=-1)
|
| 648 |
+
|
| 649 |
+
# Stack all four representations [b, 4, 2048]
|
| 650 |
+
fused_embeddings = jnp.stack([task_gated, balanced_fusion, stage_dominant, pure_stage], axis=1)
|
| 651 |
+
|
| 652 |
+
return fused_embeddings
|
| 653 |
+
|
| 654 |
+
@at.typecheck
|
| 655 |
+
def embed_prefix(
|
| 656 |
+
self,
|
| 657 |
+
obs: Observation
|
| 658 |
+
) -> tuple[
|
| 659 |
+
at.Float[at.Array, "b s emb"],
|
| 660 |
+
at.Bool[at.Array, "b s"],
|
| 661 |
+
at.Bool[at.Array, " s"]
|
| 662 |
+
]:
|
| 663 |
+
"""
|
| 664 |
+
Embed prefix: images + task + state + FAST_tokens (if provided).
|
| 665 |
+
|
| 666 |
+
Args:
|
| 667 |
+
obs: Observation (may include fast_tokens and fast_token_mask)
|
| 668 |
+
|
| 669 |
+
Returns:
|
| 670 |
+
tokens, input_mask, ar_mask
|
| 671 |
+
"""
|
| 672 |
+
input_mask = []
|
| 673 |
+
ar_mask = []
|
| 674 |
+
tokens = []
|
| 675 |
+
|
| 676 |
+
# Embed images
|
| 677 |
+
image_token_list = []
|
| 678 |
+
# Respect freeze_vision_backbone config: if frozen, always use train=False
|
| 679 |
+
# If not frozen, use the model's training state (self.deterministic)
|
| 680 |
+
vision_train_mode = (not self.deterministic) and (not self.config.freeze_vision_backbone)
|
| 681 |
+
|
| 682 |
+
for name in obs.images:
|
| 683 |
+
image_tokens, _ = self.PaliGemma.img(obs.images[name], train=vision_train_mode)
|
| 684 |
+
image_token_list.append(image_tokens) # Store for subtask prediction
|
| 685 |
+
|
| 686 |
+
tokens.append(image_tokens)
|
| 687 |
+
input_mask.append(
|
| 688 |
+
einops.repeat(
|
| 689 |
+
obs.image_masks[name],
|
| 690 |
+
"b -> b s",
|
| 691 |
+
s=image_tokens.shape[1],
|
| 692 |
+
)
|
| 693 |
+
)
|
| 694 |
+
# Image tokens attend to each other
|
| 695 |
+
ar_mask += [False] * image_tokens.shape[1]
|
| 696 |
+
|
| 697 |
+
# Add task embeddings with subtask state fusion
|
| 698 |
+
if obs.tokenized_prompt is not None:
|
| 699 |
+
# obs.tokenized_prompt now contains task_ids (shape: [batch_size, 2])
|
| 700 |
+
task_ids = obs.tokenized_prompt[:, 0] # Extract task_id: [batch_size]
|
| 701 |
+
base_task_embedding = self.task_embeddings(task_ids) # shape: [batch_size, embed_dim]
|
| 702 |
+
|
| 703 |
+
# ALWAYS use the input subtask state - never use predicted state inside model
|
| 704 |
+
if obs.tokenized_prompt.shape[1] > 1: # If we have [task_id, subtask_state]
|
| 705 |
+
subtask_state = obs.tokenized_prompt[:, 1] # Use input subtask state
|
| 706 |
+
else:
|
| 707 |
+
raise ValueError("subtask_state must be provided in tokenized_prompt for PI_BEHAVIOR model")
|
| 708 |
+
|
| 709 |
+
# Fuse task embedding with subtask state - returns [b, 4, d] with multiple representations
|
| 710 |
+
fused_task_embeddings = self.fuse_task_and_subtask(base_task_embedding, task_ids, subtask_state)
|
| 711 |
+
|
| 712 |
+
# Create task token sequence: [base_task, task_gated, balanced_fusion, stage_dominant, pure_stage]
|
| 713 |
+
task_sequence = jnp.concatenate([
|
| 714 |
+
base_task_embedding[:, None, :], # [b, 1, d] - base task token
|
| 715 |
+
fused_task_embeddings # [b, 4, d] - stage-conditioned tokens
|
| 716 |
+
], axis=1) # [b, 5, d]
|
| 717 |
+
|
| 718 |
+
tokens.append(task_sequence)
|
| 719 |
+
# All task tokens are valid
|
| 720 |
+
task_mask = jnp.ones((obs.tokenized_prompt.shape[0], 5), dtype=jnp.bool_)
|
| 721 |
+
input_mask.append(task_mask)
|
| 722 |
+
# Hierarchical attention: base task (False) then stage tokens (True, False, False, False)
|
| 723 |
+
# Base task attends to images bidirectionally
|
| 724 |
+
# Stage tokens attend to images+task but not vice versa
|
| 725 |
+
ar_mask += [False] + [True, False, False, False]
|
| 726 |
+
|
| 727 |
+
# Add state as discrete tokens (Pi05 style)
|
| 728 |
+
# Discretize state into bins
|
| 729 |
+
discretized_state = jnp.digitize(obs.state, bins=jnp.linspace(-1, 1, 256 + 1)[:-1]) - 1
|
| 730 |
+
discretized_state = jnp.clip(discretized_state, 0, 255) # Ensure valid range
|
| 731 |
+
|
| 732 |
+
# Embed each dimension of the discretized state
|
| 733 |
+
state_tokens = []
|
| 734 |
+
for i in range(obs.state.shape[-1]):
|
| 735 |
+
state_dim_tokens = self.PaliGemma.llm(discretized_state[:, i:i+1], method="embed")
|
| 736 |
+
state_tokens.append(state_dim_tokens)
|
| 737 |
+
|
| 738 |
+
if state_tokens:
|
| 739 |
+
state_tokens = jnp.concatenate(state_tokens, axis=1) # shape: [batch_size, state_dim, embed_dim]
|
| 740 |
+
tokens.append(state_tokens)
|
| 741 |
+
input_mask.append(jnp.ones((obs.state.shape[0], obs.state.shape[-1]), dtype=jnp.bool_))
|
| 742 |
+
# State tokens have full bidirectional attention with all prefix tokens
|
| 743 |
+
# (images, task, stages, and other state tokens)
|
| 744 |
+
ar_mask += [False] * state_tokens.shape[1]
|
| 745 |
+
|
| 746 |
+
# FAST tokens (from observation if provided)
|
| 747 |
+
if self.config.use_fast_auxiliary and obs.fast_tokens is not None:
|
| 748 |
+
fast_tokens = obs.fast_tokens # [B, T]
|
| 749 |
+
fast_token_mask = obs.fast_token_mask # [B, T]
|
| 750 |
+
|
| 751 |
+
# Teacher forcing: shift right [BOS, tok0, tok1, ..., tok_{T-1}]
|
| 752 |
+
bos_token = jnp.zeros((fast_tokens.shape[0], 1), dtype=jnp.int32)
|
| 753 |
+
shifted_tokens = jnp.concatenate([bos_token, fast_tokens[:, :-1]], axis=1)
|
| 754 |
+
|
| 755 |
+
# Shift mask too: [True, mask_0, mask_1, ..., mask_{T-1}]
|
| 756 |
+
bos_mask = jnp.ones((fast_tokens.shape[0], 1), dtype=jnp.bool_)
|
| 757 |
+
shifted_mask = jnp.concatenate([bos_mask, fast_token_mask[:, :-1]], axis=1)
|
| 758 |
+
|
| 759 |
+
# Embed using FAST embedding layer (NOT Paligemma!)
|
| 760 |
+
fast_token_emb = self.fast_token_embedding(shifted_tokens) # [B, T, D]
|
| 761 |
+
|
| 762 |
+
tokens.append(fast_token_emb)
|
| 763 |
+
input_mask.append(shifted_mask) # Use the actual token mask
|
| 764 |
+
# Causal for FAST: ALL tokens are causal (pure autoregressive)
|
| 765 |
+
ar_mask += [True] * shifted_tokens.shape[1]
|
| 766 |
+
|
| 767 |
+
tokens = jnp.concatenate(tokens, axis=1)
|
| 768 |
+
input_mask = jnp.concatenate(input_mask, axis=1)
|
| 769 |
+
ar_mask = jnp.array(ar_mask)
|
| 770 |
+
return tokens, input_mask, ar_mask
|
| 771 |
+
|
| 772 |
+
@at.typecheck
|
| 773 |
+
def embed_suffix(
|
| 774 |
+
self, obs: Observation, noisy_actions: _model.Actions, timestep: at.Float[at.Array, " b"]
|
| 775 |
+
) -> tuple[
|
| 776 |
+
at.Float[at.Array, "b s emb"],
|
| 777 |
+
at.Bool[at.Array, "b s"],
|
| 778 |
+
at.Bool[at.Array, " s"],
|
| 779 |
+
at.Float[at.Array, "b emb"],
|
| 780 |
+
]:
|
| 781 |
+
input_mask = []
|
| 782 |
+
ar_mask = []
|
| 783 |
+
tokens = []
|
| 784 |
+
|
| 785 |
+
# Pi05 style: no explicit state token in suffix (it's in prefix as discrete tokens)
|
| 786 |
+
|
| 787 |
+
action_tokens = self.action_in_proj(noisy_actions)
|
| 788 |
+
# Embed timestep using sine-cosine positional encoding
|
| 789 |
+
time_emb = posemb_sincos(timestep, self.action_in_proj.out_features, min_period=4e-3, max_period=4.0)
|
| 790 |
+
|
| 791 |
+
# Pi05 style: time MLP for adaRMS
|
| 792 |
+
time_emb = self.time_mlp_in(time_emb)
|
| 793 |
+
time_emb = nnx.swish(time_emb)
|
| 794 |
+
time_emb = self.time_mlp_out(time_emb)
|
| 795 |
+
time_emb = nnx.swish(time_emb)
|
| 796 |
+
action_expert_tokens = action_tokens
|
| 797 |
+
adarms_cond = time_emb
|
| 798 |
+
|
| 799 |
+
tokens.append(action_expert_tokens)
|
| 800 |
+
input_mask.append(jnp.ones(action_expert_tokens.shape[:2], dtype=jnp.bool_))
|
| 801 |
+
|
| 802 |
+
# image/task/state inputs do not attend to action tokens
|
| 803 |
+
ar_mask += [True] + ([False] * (self.action_horizon - 1))
|
| 804 |
+
|
| 805 |
+
tokens = jnp.concatenate(tokens, axis=1)
|
| 806 |
+
input_mask = jnp.concatenate(input_mask, axis=1)
|
| 807 |
+
ar_mask = jnp.array(ar_mask)
|
| 808 |
+
return tokens, input_mask, ar_mask, adarms_cond
|
| 809 |
+
|
| 810 |
+
@override
|
| 811 |
+
def compute_loss(
|
| 812 |
+
self, rng: at.KeyArrayLike, observation: Observation, actions: _model.Actions, *, train: bool = False
|
| 813 |
+
) -> at.Float[at.Array, "*b ah"]:
|
| 814 |
+
"""Not used - we only use compute_detailed_loss() for training."""
|
| 815 |
+
raise NotImplementedError("Use compute_detailed_loss() instead")
|
| 816 |
+
|
| 817 |
+
@override
|
| 818 |
+
def compute_detailed_loss(
|
| 819 |
+
self, rng: at.KeyArrayLike, observation: Observation, actions: _model.Actions, *, train: bool = False, num_flow_samples: int = 1
|
| 820 |
+
) -> dict[str, at.Float[at.Array, "*b"]]:
|
| 821 |
+
"""
|
| 822 |
+
Compute detailed loss with multiple flow matching samples.
|
| 823 |
+
|
| 824 |
+
Simplified approach using KV cache:
|
| 825 |
+
- Compute prefix KV cache once (with FAST tokens)
|
| 826 |
+
- Remove FAST tokens from cache (action expert doesn't attend to FAST)
|
| 827 |
+
- Process N flow samples independently, each reusing the same cached prefix
|
| 828 |
+
- Each sample has different noise and different time
|
| 829 |
+
- Average losses across samples
|
| 830 |
+
"""
|
| 831 |
+
losses = {}
|
| 832 |
+
|
| 833 |
+
preprocess_rng, rng = jax.random.split(rng)
|
| 834 |
+
observation = preprocess_observation(preprocess_rng, observation, train=train)
|
| 835 |
+
|
| 836 |
+
batch_size = actions.shape[0]
|
| 837 |
+
|
| 838 |
+
# 1. Embed prefix once (includes FAST tokens if provided in observation)
|
| 839 |
+
prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)
|
| 840 |
+
|
| 841 |
+
# 2. Compute prefix KV cache
|
| 842 |
+
prefix_attn_mask = make_attn_mask(prefix_mask, prefix_ar_mask)
|
| 843 |
+
positions_prefix = jnp.cumsum(prefix_mask, axis=1) - 1
|
| 844 |
+
(prefix_out, _), kv_cache_full = self.PaliGemma.llm(
|
| 845 |
+
[prefix_tokens, None],
|
| 846 |
+
mask=prefix_attn_mask,
|
| 847 |
+
positions=positions_prefix
|
| 848 |
+
)
|
| 849 |
+
|
| 850 |
+
# DA3 banks: timestep-independent, computed ONCE and closure-captured by the vmapped
|
| 851 |
+
# flow-sample fn (vmap broadcasts them across the N samples).
|
| 852 |
+
spatial_banks = self._compute_banks(observation)
|
| 853 |
+
|
| 854 |
+
# 3. Predict stage from VLM output of base task token
|
| 855 |
+
# Base task token is the first token after all image tokens
|
| 856 |
+
# Image tokens all have ar_mask=False, task starts with ar_mask=False (base) then True (stage tokens)
|
| 857 |
+
# Structure: [images (all False)] [base_task (False)] [stages (True, False, False, False)]
|
| 858 |
+
# Find first True (first stage token), base task is at that index - 1
|
| 859 |
+
first_stage_token_idx = jnp.argmax(prefix_ar_mask) # Returns index of first True
|
| 860 |
+
base_task_token_idx = first_stage_token_idx - 1
|
| 861 |
+
base_task_output = prefix_out[:, base_task_token_idx, :]
|
| 862 |
+
subtask_logits = self.stage_pred_from_vlm(base_task_output) # [B, MAX_NUM_STAGES]
|
| 863 |
+
|
| 864 |
+
# Mask out invalid stages for each task (vectorized JAX operations)
|
| 865 |
+
task_ids = observation.tokenized_prompt[:, 0] # [B]
|
| 866 |
+
task_num_stages_array = jnp.array(TASK_NUM_STAGES, dtype=jnp.int32)
|
| 867 |
+
task_num_stages = task_num_stages_array[task_ids] # [B] - JAX array indexing
|
| 868 |
+
stage_range = jnp.arange(MAX_NUM_STAGES) # [15]
|
| 869 |
+
valid_mask = stage_range[None, :] < task_num_stages[:, None] # [B, 15]
|
| 870 |
+
subtask_logits = jnp.where(valid_mask, subtask_logits, -jnp.inf) # Mask invalid stages
|
| 871 |
+
|
| 872 |
+
# 4. Extract FAST loss from prefix output (before removing from cache)
|
| 873 |
+
fast_loss_value = 0.0
|
| 874 |
+
fast_len = 0
|
| 875 |
+
fast_targets = observation.fast_tokens
|
| 876 |
+
fast_token_mask = observation.fast_token_mask
|
| 877 |
+
|
| 878 |
+
if self.config.use_fast_auxiliary and fast_targets is not None:
|
| 879 |
+
fast_len = fast_targets.shape[1]
|
| 880 |
+
fast_start_idx = prefix_tokens.shape[1] - fast_len
|
| 881 |
+
fast_outputs = prefix_out[:, fast_start_idx:, :] # [B, T, D]
|
| 882 |
+
|
| 883 |
+
# Project to FAST vocab
|
| 884 |
+
fast_logits = self.fast_token_proj(fast_outputs) # [B, T, vocab_size]
|
| 885 |
+
|
| 886 |
+
# Cross-entropy loss with teacher forcing
|
| 887 |
+
pred_logits = fast_logits # [B, T, vocab]
|
| 888 |
+
target_tokens = fast_targets # [B, T]
|
| 889 |
+
loss_mask = fast_token_mask # [B, T]
|
| 890 |
+
|
| 891 |
+
log_probs = jax.nn.log_softmax(pred_logits, axis=-1)
|
| 892 |
+
target_log_probs = jnp.take_along_axis(
|
| 893 |
+
log_probs,
|
| 894 |
+
target_tokens[:, :, None],
|
| 895 |
+
axis=-1
|
| 896 |
+
).squeeze(-1) # [B, T]
|
| 897 |
+
|
| 898 |
+
fast_token_loss = -target_log_probs # [B, T]
|
| 899 |
+
|
| 900 |
+
# Apply mask and normalize by number of valid tokens
|
| 901 |
+
masked_loss = fast_token_loss * loss_mask # [B, T]
|
| 902 |
+
num_valid_tokens = jnp.maximum(jnp.sum(loss_mask, axis=-1), 1) # [B]
|
| 903 |
+
losses["fast_loss"] = jnp.sum(masked_loss, axis=-1) / num_valid_tokens # [B]
|
| 904 |
+
|
| 905 |
+
# Accuracy (only on valid tokens)
|
| 906 |
+
pred_tokens = jnp.argmax(pred_logits, axis=-1)
|
| 907 |
+
correct = (pred_tokens == target_tokens) * loss_mask
|
| 908 |
+
losses["fast_accuracy"] = jnp.sum(correct, axis=-1) / num_valid_tokens
|
| 909 |
+
|
| 910 |
+
fast_loss_value = self.config.fast_loss_weight * jnp.mean(losses["fast_loss"])
|
| 911 |
+
elif fast_targets is not None:
|
| 912 |
+
# FAST auxiliary is disabled but data contains FAST tokens
|
| 913 |
+
raise ValueError(
|
| 914 |
+
"use_fast_auxiliary=False but observation contains fast_tokens. "
|
| 915 |
+
"Either enable use_fast_auxiliary in config or ensure data doesn't contain fast_tokens."
|
| 916 |
+
)
|
| 917 |
+
|
| 918 |
+
# 5. Remove FAST tokens from KV cache (action expert doesn't attend to FAST)
|
| 919 |
+
# KV cache shape: [layers, batch, seq_len, num_kv_heads, head_dim]
|
| 920 |
+
if fast_len > 0:
|
| 921 |
+
cache_k, cache_v = kv_cache_full
|
| 922 |
+
# Remove last fast_len tokens from sequence dimension
|
| 923 |
+
cache_k = cache_k[:, :, :-fast_len, :, :]
|
| 924 |
+
cache_v = cache_v[:, :, :-fast_len, :, :]
|
| 925 |
+
kv_cache_for_actions = (cache_k, cache_v)
|
| 926 |
+
prefix_len_for_actions = prefix_tokens.shape[1] - fast_len
|
| 927 |
+
# Truncate prefix mask and ar_mask for action expert
|
| 928 |
+
prefix_mask_for_actions = prefix_mask[:, :-fast_len]
|
| 929 |
+
prefix_ar_mask_for_actions = prefix_ar_mask[:-fast_len]
|
| 930 |
+
else:
|
| 931 |
+
kv_cache_for_actions = kv_cache_full
|
| 932 |
+
prefix_len_for_actions = prefix_tokens.shape[1]
|
| 933 |
+
prefix_mask_for_actions = prefix_mask
|
| 934 |
+
prefix_ar_mask_for_actions = prefix_ar_mask
|
| 935 |
+
|
| 936 |
+
# 6. Knowledge insulation: stop gradients from action expert to VLM
|
| 937 |
+
# This must happen BEFORE kv_transform so transform still receives gradients
|
| 938 |
+
if self.config.use_knowledge_insulation:
|
| 939 |
+
kv_cache_for_actions = jax.tree.map(jax.lax.stop_gradient, kv_cache_for_actions)
|
| 940 |
+
|
| 941 |
+
# 7. Transform KV cache (after stop_gradient, so it receives action expert gradients)
|
| 942 |
+
if self.kv_transform is not None:
|
| 943 |
+
kv_cache_for_actions = self.kv_transform(kv_cache_for_actions)
|
| 944 |
+
|
| 945 |
+
# 8. Define single flow sample processing
|
| 946 |
+
def process_one_flow_sample(sample_rng):
|
| 947 |
+
"""Process one flow sample using the original cached prefix."""
|
| 948 |
+
noise_rng, time_rng = jax.random.split(sample_rng)
|
| 949 |
+
|
| 950 |
+
# Generate different noise and time for this sample
|
| 951 |
+
noise = self.generate_correlated_noise(noise_rng, batch_size)
|
| 952 |
+
time = jax.random.beta(time_rng, 1.5, 1, (batch_size,)) * 0.999 + 0.001
|
| 953 |
+
|
| 954 |
+
# Compute noisy actions and target velocity
|
| 955 |
+
time_expanded = time[:, None, None]
|
| 956 |
+
x_t = time_expanded * noise + (1 - time_expanded) * actions
|
| 957 |
+
u_t = noise - actions
|
| 958 |
+
|
| 959 |
+
# Embed suffix for this sample
|
| 960 |
+
suffix_tokens, suffix_mask, suffix_ar_mask, adarms_cond = self.embed_suffix(
|
| 961 |
+
observation, x_t, time
|
| 962 |
+
)
|
| 963 |
+
|
| 964 |
+
# Build attention mask: suffix attends to prefix (without FAST) + itself
|
| 965 |
+
# When using KV cache, mask shape should be [batch, suffix_len, prefix_len + suffix_len]
|
| 966 |
+
suffix_attn_mask = make_attn_mask(suffix_mask, suffix_ar_mask)
|
| 967 |
+
prefix_attn_mask = einops.repeat(
|
| 968 |
+
prefix_mask_for_actions, "b p -> b s p", s=suffix_tokens.shape[1]
|
| 969 |
+
)
|
| 970 |
+
full_attn_mask = jnp.concatenate([prefix_attn_mask, suffix_attn_mask], axis=-1)
|
| 971 |
+
|
| 972 |
+
# Positions for suffix start after cached prefix
|
| 973 |
+
suffix_positions = prefix_len_for_actions + jnp.cumsum(suffix_mask, axis=-1) - 1
|
| 974 |
+
|
| 975 |
+
# Forward pass with cached prefix (discard returned cache - don't modify original!)
|
| 976 |
+
(_, suffix_out), _ = self.PaliGemma.llm(
|
| 977 |
+
[None, suffix_tokens],
|
| 978 |
+
mask=full_attn_mask,
|
| 979 |
+
positions=suffix_positions,
|
| 980 |
+
kv_cache=kv_cache_for_actions, # Original cache, reused for all samples
|
| 981 |
+
adarms_cond=[None, adarms_cond],
|
| 982 |
+
banks=spatial_banks,
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
+
# Compute velocity and loss
|
| 986 |
+
action_hidden = self.apply_spatial_action_conditioning(
|
| 987 |
+
observation,
|
| 988 |
+
suffix_out[:, -self.action_horizon:],
|
| 989 |
+
)
|
| 990 |
+
v_t = self.action_out_proj(action_hidden)
|
| 991 |
+
action_loss = jnp.square(v_t - u_t) # [B, H, D]
|
| 992 |
+
|
| 993 |
+
return action_loss
|
| 994 |
+
|
| 995 |
+
# 9. Vectorize over N flow samples
|
| 996 |
+
# Disable type checking inside vmap (jaxtyping doesn't handle traced values well)
|
| 997 |
+
flow_rngs = jax.random.split(rng, num_flow_samples)
|
| 998 |
+
with at.disable_typechecking():
|
| 999 |
+
all_action_losses = jax.vmap(process_one_flow_sample)(flow_rngs) # [N, B, H, D]
|
| 1000 |
+
|
| 1001 |
+
# 10. Average over flow samples
|
| 1002 |
+
action_loss = jnp.mean(all_action_losses, axis=0) # [B, H, D]
|
| 1003 |
+
|
| 1004 |
+
# 11. Build per-dimension action losses
|
| 1005 |
+
# Base velocity (x,y,z)
|
| 1006 |
+
losses["action_loss_base_vel_x"] = jnp.mean(action_loss[..., 0], axis=-1)
|
| 1007 |
+
losses["action_loss_base_vel_y"] = jnp.mean(action_loss[..., 1], axis=-1)
|
| 1008 |
+
losses["action_loss_base_vel_z"] = jnp.mean(action_loss[..., 2], axis=-1)
|
| 1009 |
+
|
| 1010 |
+
# Trunk joints (4)
|
| 1011 |
+
for i in range(4):
|
| 1012 |
+
losses[f"action_loss_trunk_{i}"] = jnp.mean(action_loss[..., 3+i], axis=-1)
|
| 1013 |
+
|
| 1014 |
+
# Left arm joints (7)
|
| 1015 |
+
for i in range(7):
|
| 1016 |
+
losses[f"action_loss_left_arm_{i}"] = jnp.mean(action_loss[..., 7+i], axis=-1)
|
| 1017 |
+
|
| 1018 |
+
# Left gripper
|
| 1019 |
+
losses["action_loss_left_gripper"] = jnp.mean(action_loss[..., 14], axis=-1)
|
| 1020 |
+
|
| 1021 |
+
# Right arm joints (7)
|
| 1022 |
+
for i in range(7):
|
| 1023 |
+
losses[f"action_loss_right_arm_{i}"] = jnp.mean(action_loss[..., 15+i], axis=-1)
|
| 1024 |
+
|
| 1025 |
+
# Right gripper
|
| 1026 |
+
losses["action_loss_right_gripper"] = jnp.mean(action_loss[..., 22], axis=-1)
|
| 1027 |
+
|
| 1028 |
+
# Total action loss: mean over horizon (H) and action dims (D) -> [B]
|
| 1029 |
+
losses["action_loss"] = jnp.mean(action_loss, axis=(-2, -1))
|
| 1030 |
+
|
| 1031 |
+
# 12. Add subtask loss during training
|
| 1032 |
+
subtask_loss_value = 0.0
|
| 1033 |
+
if train and observation.tokenized_prompt.shape[1] > 1:
|
| 1034 |
+
ground_truth_subtask = observation.tokenized_prompt[:, 1]
|
| 1035 |
+
subtask_loss = -jax.nn.log_softmax(subtask_logits)[
|
| 1036 |
+
jnp.arange(ground_truth_subtask.shape[0]), ground_truth_subtask
|
| 1037 |
+
]
|
| 1038 |
+
losses["subtask_loss"] = jnp.mean(subtask_loss)
|
| 1039 |
+
losses["subtask_accuracy"] = jnp.mean(
|
| 1040 |
+
jnp.argmax(subtask_logits, axis=-1) == ground_truth_subtask
|
| 1041 |
+
)
|
| 1042 |
+
subtask_loss_value = self.config.subtask_loss_weight * jnp.mean(subtask_loss)
|
| 1043 |
+
|
| 1044 |
+
# 13. Total loss
|
| 1045 |
+
losses["total_loss"] = losses["action_loss"] + subtask_loss_value + fast_loss_value
|
| 1046 |
+
|
| 1047 |
+
return losses
|
| 1048 |
+
|
| 1049 |
+
@override
|
| 1050 |
+
def sample_actions(
|
| 1051 |
+
self,
|
| 1052 |
+
rng: at.KeyArrayLike,
|
| 1053 |
+
observation: Observation,
|
| 1054 |
+
*,
|
| 1055 |
+
num_steps: int | at.Int[at.Array, ""] = 20,
|
| 1056 |
+
noise: at.Float[at.Array, "b ah ad"] | None = None,
|
| 1057 |
+
initial_actions: at.Float[at.Array, "b n ad"] | None = None,
|
| 1058 |
+
) -> _model.Actions:
|
| 1059 |
+
observation = preprocess_observation(None, observation, train=False)
|
| 1060 |
+
# Note that we use the convention more common in diffusion literature, where t=1 is noise and t=0 is the target
|
| 1061 |
+
# distribution. yes, this is the opposite of the pi0 paper, and I'm sorry.
|
| 1062 |
+
dt = -1.0 / num_steps
|
| 1063 |
+
batch_size = observation.state.shape[0]
|
| 1064 |
+
|
| 1065 |
+
# Generate or constrain noise based on inpainting requirements
|
| 1066 |
+
if initial_actions is not None:
|
| 1067 |
+
# INPAINTING PATH: Construct constrained noise z that satisfies initial_actions
|
| 1068 |
+
num_initial_actions = initial_actions.shape[1]
|
| 1069 |
+
input_action_dim = initial_actions.shape[2]
|
| 1070 |
+
|
| 1071 |
+
# Pad initial_actions to full model dimensions (32D) and action_horizon (30)
|
| 1072 |
+
if input_action_dim < self.action_dim:
|
| 1073 |
+
action_padding = jnp.zeros((batch_size, num_initial_actions, self.action_dim - input_action_dim))
|
| 1074 |
+
initial_actions_full_dim = jnp.concatenate([initial_actions, action_padding], axis=2)
|
| 1075 |
+
else:
|
| 1076 |
+
initial_actions_full_dim = initial_actions[:, :, :self.action_dim]
|
| 1077 |
+
|
| 1078 |
+
if num_initial_actions < self.action_horizon:
|
| 1079 |
+
seq_padding = jnp.zeros((batch_size, self.action_horizon - num_initial_actions, self.action_dim))
|
| 1080 |
+
initial_actions_padded = jnp.concatenate([initial_actions_full_dim, seq_padding], axis=1)
|
| 1081 |
+
else:
|
| 1082 |
+
initial_actions_padded = initial_actions_full_dim[:, :self.action_horizon]
|
| 1083 |
+
|
| 1084 |
+
# Compute O and U indices for inpainting (JIT-safe: static list comprehensions)
|
| 1085 |
+
flat_dim = self.action_horizon * self.action_dim
|
| 1086 |
+
|
| 1087 |
+
# Build O_indices: first num_initial_actions timesteps, first input_action_dim dimensions
|
| 1088 |
+
O_indices = jnp.array([
|
| 1089 |
+
t * self.action_dim + d
|
| 1090 |
+
for t in range(num_initial_actions)
|
| 1091 |
+
for d in range(input_action_dim)
|
| 1092 |
+
], dtype=jnp.int32)
|
| 1093 |
+
|
| 1094 |
+
# Build U_indices: all other indices (JIT-safe: static list comprehension)
|
| 1095 |
+
# Python set operations happen at trace time (before JIT), so this is safe
|
| 1096 |
+
O_set = {t * self.action_dim + d for t in range(num_initial_actions) for d in range(input_action_dim)}
|
| 1097 |
+
U_indices = jnp.array([
|
| 1098 |
+
i for i in range(flat_dim) if i not in O_set
|
| 1099 |
+
], dtype=jnp.int32)
|
| 1100 |
+
|
| 1101 |
+
# Generate noise
|
| 1102 |
+
rng, noise_rng = jax.random.split(rng)
|
| 1103 |
+
|
| 1104 |
+
if self.correlation_loaded:
|
| 1105 |
+
# CORRELATED NOISE: Sample with correlation matrix
|
| 1106 |
+
noise = self.generate_correlated_noise(noise_rng, batch_size)
|
| 1107 |
+
else:
|
| 1108 |
+
# FALLBACK: Independent noise
|
| 1109 |
+
noise = jax.random.normal(noise_rng, (batch_size, self.action_horizon, self.action_dim))
|
| 1110 |
+
|
| 1111 |
+
# Extract fixed z_O and x0_O for constraint enforcement
|
| 1112 |
+
noise_flat = noise.reshape(batch_size, flat_dim)
|
| 1113 |
+
fixed_z_O = noise_flat[:, O_indices] # [b, |O|] - fixed noise for inpainting
|
| 1114 |
+
x0_O = initial_actions_padded.reshape(batch_size, flat_dim)[:, O_indices] # [b, |O|] - target actions
|
| 1115 |
+
|
| 1116 |
+
# Precompute correction matrix for correlation-aware inpainting
|
| 1117 |
+
inpainting_cache = None
|
| 1118 |
+
if self.correlation_loaded:
|
| 1119 |
+
cache_key = (num_initial_actions, input_action_dim)
|
| 1120 |
+
if cache_key not in self.inpainting_cache:
|
| 1121 |
+
logger.info(f"Computing correction matrix for {num_initial_actions} steps, {input_action_dim} dims...")
|
| 1122 |
+
self.inpainting_cache[cache_key] = self._precompute_correction_matrix(O_indices, U_indices)
|
| 1123 |
+
inpainting_cache = self.inpainting_cache[cache_key]
|
| 1124 |
+
|
| 1125 |
+
else:
|
| 1126 |
+
# NO INPAINTING: Standard noise generation
|
| 1127 |
+
if noise is None:
|
| 1128 |
+
rng, noise_rng = jax.random.split(rng)
|
| 1129 |
+
noise = self.generate_correlated_noise(noise_rng, batch_size)
|
| 1130 |
+
|
| 1131 |
+
fixed_z_O = None
|
| 1132 |
+
x0_O = None
|
| 1133 |
+
O_indices = None
|
| 1134 |
+
inpainting_cache = None
|
| 1135 |
+
|
| 1136 |
+
# Split RNG for step loop
|
| 1137 |
+
rng, step_rng = jax.random.split(rng)
|
| 1138 |
+
|
| 1139 |
+
# Ensure FAST tokens are never used during inference
|
| 1140 |
+
if observation.fast_tokens is not None:
|
| 1141 |
+
raise ValueError(
|
| 1142 |
+
"FAST tokens must not be provided during inference (sample_actions). "
|
| 1143 |
+
"FAST tokens are only used during training for auxiliary loss. "
|
| 1144 |
+
"Set observation.fast_tokens=None before calling sample_actions."
|
| 1145 |
+
)
|
| 1146 |
+
|
| 1147 |
+
# First fill KV cache with a forward pass of the prefix (no FAST tokens during inference)
|
| 1148 |
+
prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)
|
| 1149 |
+
prefix_attn_mask = make_attn_mask(prefix_mask, prefix_ar_mask)
|
| 1150 |
+
positions = jnp.cumsum(prefix_mask, axis=1) - 1
|
| 1151 |
+
(prefix_out, _), kv_cache = self.PaliGemma.llm([prefix_tokens, None], mask=prefix_attn_mask, positions=positions)
|
| 1152 |
+
|
| 1153 |
+
# DA3 banks: computed once, reused across all denoise steps.
|
| 1154 |
+
spatial_banks = self._compute_banks(observation)
|
| 1155 |
+
|
| 1156 |
+
# Predict stage from VLM output of base task token
|
| 1157 |
+
# Find base task token position (same logic as in compute_detailed_loss)
|
| 1158 |
+
first_stage_token_idx = jnp.argmax(prefix_ar_mask) # Returns index of first True
|
| 1159 |
+
base_task_token_idx = first_stage_token_idx - 1
|
| 1160 |
+
base_task_output = prefix_out[:, base_task_token_idx, :]
|
| 1161 |
+
subtask_logits = self.stage_pred_from_vlm(base_task_output) # [B, MAX_NUM_STAGES]
|
| 1162 |
+
|
| 1163 |
+
# Mask out invalid stages for each task (vectorized JAX operations)
|
| 1164 |
+
task_ids = observation.tokenized_prompt[:, 0] # [B]
|
| 1165 |
+
task_num_stages_array = jnp.array(TASK_NUM_STAGES, dtype=jnp.int32)
|
| 1166 |
+
task_num_stages = task_num_stages_array[task_ids] # [B] - JAX array indexing
|
| 1167 |
+
stage_range = jnp.arange(MAX_NUM_STAGES) # [15]
|
| 1168 |
+
valid_mask = stage_range[None, :] < task_num_stages[:, None] # [B, 15]
|
| 1169 |
+
subtask_logits = jnp.where(valid_mask, subtask_logits, -jnp.inf)
|
| 1170 |
+
|
| 1171 |
+
# Transform KV cache for cross-layer attention
|
| 1172 |
+
if self.kv_transform is not None:
|
| 1173 |
+
kv_cache = self.kv_transform(kv_cache)
|
| 1174 |
+
|
| 1175 |
+
def step(carry):
|
| 1176 |
+
x_t, time, step_rng = carry
|
| 1177 |
+
|
| 1178 |
+
# Use config value for time threshold
|
| 1179 |
+
TIME_THRESHOLD_INPAINT = self.config.time_threshold_inpaint
|
| 1180 |
+
|
| 1181 |
+
# Model forward pass
|
| 1182 |
+
suffix_tokens, suffix_mask, suffix_ar_mask, adarms_cond = self.embed_suffix(
|
| 1183 |
+
observation, x_t, jnp.broadcast_to(time, batch_size)
|
| 1184 |
+
)
|
| 1185 |
+
suffix_attn_mask = make_attn_mask(suffix_mask, suffix_ar_mask)
|
| 1186 |
+
prefix_attn_mask = einops.repeat(prefix_mask, "b p -> b s p", s=suffix_tokens.shape[1])
|
| 1187 |
+
full_attn_mask = jnp.concatenate([prefix_attn_mask, suffix_attn_mask], axis=-1)
|
| 1188 |
+
assert full_attn_mask.shape == (
|
| 1189 |
+
batch_size,
|
| 1190 |
+
suffix_tokens.shape[1],
|
| 1191 |
+
prefix_tokens.shape[1] + suffix_tokens.shape[1],
|
| 1192 |
+
)
|
| 1193 |
+
positions = jnp.sum(prefix_mask, axis=-1)[:, None] + jnp.cumsum(suffix_mask, axis=-1) - 1
|
| 1194 |
+
|
| 1195 |
+
(prefix_out, suffix_out), _ = self.PaliGemma.llm(
|
| 1196 |
+
[None, suffix_tokens],
|
| 1197 |
+
mask=full_attn_mask,
|
| 1198 |
+
positions=positions,
|
| 1199 |
+
kv_cache=kv_cache,
|
| 1200 |
+
adarms_cond=[None, adarms_cond],
|
| 1201 |
+
banks=spatial_banks,
|
| 1202 |
+
)
|
| 1203 |
+
assert prefix_out is None
|
| 1204 |
+
action_hidden = self.apply_spatial_action_conditioning(
|
| 1205 |
+
observation,
|
| 1206 |
+
suffix_out[:, -self.action_horizon :],
|
| 1207 |
+
)
|
| 1208 |
+
v_t = self.action_out_proj(action_hidden)
|
| 1209 |
+
|
| 1210 |
+
# Euler step: x_{t+dt} = x_t + dt * v_t
|
| 1211 |
+
x_t_new = x_t + dt * v_t
|
| 1212 |
+
|
| 1213 |
+
# Apply correlation-aware inpainting correction
|
| 1214 |
+
# Only enforce when time > TIME_THRESHOLD_INPAINT (let model be free in final steps)
|
| 1215 |
+
if fixed_z_O is not None:
|
| 1216 |
+
time_new = time + dt
|
| 1217 |
+
|
| 1218 |
+
def apply_correlated_correction(x):
|
| 1219 |
+
x_flat = x.reshape(batch_size, -1)
|
| 1220 |
+
|
| 1221 |
+
# Compute desired state at O: x_t[O] = (1-t)*x0[O] + t*z_O
|
| 1222 |
+
x_desired_O = (1.0 - time_new) * x0_O + time_new * fixed_z_O # [b, |O|]
|
| 1223 |
+
|
| 1224 |
+
# Compute correction at O
|
| 1225 |
+
delta_O = x_desired_O - x_flat[:, O_indices] # [b, |O|]
|
| 1226 |
+
|
| 1227 |
+
# Apply hard constraint at O
|
| 1228 |
+
x_flat = x_flat.at[:, O_indices].set(x_desired_O)
|
| 1229 |
+
|
| 1230 |
+
# If correlation matrix available, propagate correction to U
|
| 1231 |
+
if inpainting_cache is not None:
|
| 1232 |
+
correction_matrix = inpainting_cache['correction_matrix'] # [|U|, |O|]
|
| 1233 |
+
U_indices_cached = inpainting_cache['U_indices']
|
| 1234 |
+
|
| 1235 |
+
# Compute correlated correction: δ_U = Σ_{UO}Σ_{OO}^{-1} @ δ_O
|
| 1236 |
+
delta_U = delta_O @ correction_matrix.T # [b, |U|]
|
| 1237 |
+
|
| 1238 |
+
# Skip if correction too large (indicates instability)
|
| 1239 |
+
max_correction = jnp.max(jnp.abs(delta_U))
|
| 1240 |
+
x_flat = jax.lax.cond(
|
| 1241 |
+
# Prevents exploding corrections in case of noisy out of distribution initial actions
|
| 1242 |
+
max_correction <= 1.0,
|
| 1243 |
+
lambda x: x.at[:, U_indices_cached].add(delta_U),
|
| 1244 |
+
lambda x: x,
|
| 1245 |
+
x_flat
|
| 1246 |
+
)
|
| 1247 |
+
|
| 1248 |
+
# Sanity check: if Σ = I, correction_matrix = 0, so delta_U = 0 that is correct
|
| 1249 |
+
# If the correlation is 1 everywhere we will go to the flat prediction that is correct
|
| 1250 |
+
|
| 1251 |
+
return x_flat.reshape(batch_size, self.action_horizon, self.action_dim)
|
| 1252 |
+
|
| 1253 |
+
# Only apply correction when NEW time > threshold
|
| 1254 |
+
x_t_new = jax.lax.cond(
|
| 1255 |
+
time_new > TIME_THRESHOLD_INPAINT,
|
| 1256 |
+
apply_correlated_correction,
|
| 1257 |
+
lambda x: x,
|
| 1258 |
+
x_t_new
|
| 1259 |
+
)
|
| 1260 |
+
|
| 1261 |
+
return x_t_new, time + dt, step_rng
|
| 1262 |
+
|
| 1263 |
+
def cond(carry):
|
| 1264 |
+
x_t, time, step_rng = carry
|
| 1265 |
+
# Robust to floating-point error
|
| 1266 |
+
return time >= -dt / 2
|
| 1267 |
+
|
| 1268 |
+
x_0, _, _ = jax.lax.while_loop(cond, step, (noise, 1.0, step_rng))
|
| 1269 |
+
|
| 1270 |
+
return x_0, subtask_logits
|
pibehavior_da3_clean_up_your_desk_40k/source_code/model/pi_behavior_config.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PI_BEHAVIOR Model Configuration
|
| 2 |
+
|
| 3 |
+
Configuration for PI_BEHAVIOR model on BEHAVIOR-1K challenge.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import dataclasses
|
| 7 |
+
import json
|
| 8 |
+
import pathlib
|
| 9 |
+
from typing import TYPE_CHECKING
|
| 10 |
+
|
| 11 |
+
import flax.nnx as nnx
|
| 12 |
+
import jax
|
| 13 |
+
import jax.numpy as jnp
|
| 14 |
+
from typing_extensions import override
|
| 15 |
+
|
| 16 |
+
from openpi.models import model as _model
|
| 17 |
+
from openpi.models import gemma as _gemma
|
| 18 |
+
from openpi.shared import array_typing as at
|
| 19 |
+
import openpi.shared.nnx_utils as nnx_utils
|
| 20 |
+
|
| 21 |
+
from b1k.models.observation import Observation
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from b1k.models.pi_behavior import PiBehavior
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# Per-task stage counts (based on avg_episode_length / 900, capped between 5-15)
|
| 28 |
+
# Use tuple for immutability and to avoid JAX device allocation at import time
|
| 29 |
+
TASK_NUM_STAGES = (
|
| 30 |
+
5, 6, 15, 15, 14, 12, 9, 15, 10, 15, # Tasks 0-9
|
| 31 |
+
7, 13, 10, 15, 15, 15, 15, 11, 13, 12, # Tasks 10-19
|
| 32 |
+
14, 15, 9, 15, 15, 15, 15, 15, 15, 15, # Tasks 20-29
|
| 33 |
+
11, 10, 10, 13, 5, 5, 14, 6, 8, 10, # Tasks 30-39
|
| 34 |
+
5, 15, 8, 15, 12, 11, 9, 14, 15, 15, # Tasks 40-49
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
MAX_NUM_STAGES = 15 # Maximum stages per task
|
| 38 |
+
TOTAL_TASK_STAGE_EMBEDDINGS = sum(TASK_NUM_STAGES) # 596 total embeddings
|
| 39 |
+
|
| 40 |
+
# Cumulative offsets for indexing into task_stage_embeddings (as tuple)
|
| 41 |
+
TASK_STAGE_OFFSETS = tuple([0] + [sum(TASK_NUM_STAGES[:i+1]) for i in range(len(TASK_NUM_STAGES) - 1)])
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclasses.dataclass(frozen=True)
|
| 45 |
+
class B1KDA3Config:
|
| 46 |
+
"""DA3 spatial-language branch for PiBehavior (inline extraction; b1k cameras are square)."""
|
| 47 |
+
|
| 48 |
+
enabled: bool = True
|
| 49 |
+
num_views: int = 3 # zed head (main), left/right realsense (wrist branches)
|
| 50 |
+
da3_channels: int = 1536 # GIANT embed dim
|
| 51 |
+
da3_layers: int = 4 # out_layers (19, 26, 33, 39)
|
| 52 |
+
grid_hw: tuple[int, int] = (18, 18) # 252x252 square DA3 input / patch 14
|
| 53 |
+
hidden_dim: int = 1024 # == action-expert width
|
| 54 |
+
lang_dim: int = 1024 # ModernBERT-large (task-name embeddings)
|
| 55 |
+
lang_max_len: int = 32
|
| 56 |
+
num_heads: int = 8
|
| 57 |
+
lang_fusion_depth: int = 2
|
| 58 |
+
num_inject_layers: int = 6 # last 6 of 18 action-expert blocks
|
| 59 |
+
spatial_scale: float = 2.0
|
| 60 |
+
# V2 "force-spatial-on" defaults (now that geometry is CORRECT). Zero-init lets the model learn to
|
| 61 |
+
# IGNORE spatial (image path fits first, no gradient left to turn the injection on). Nonzero init +
|
| 62 |
+
# per-head logit-gain keep the injection ACTIVE and the attention SHARP/learnable from step 0, so the
|
| 63 |
+
# model must account for the (now-sane) banks. This only hurt before because geometry was garbage.
|
| 64 |
+
spatial_init_std: float = 0.01
|
| 65 |
+
attn_logit_gain: bool = True
|
| 66 |
+
attn_logit_gain_init: float = 32.0
|
| 67 |
+
bank_token_embed: bool = True
|
| 68 |
+
perceiver_query_std: float = 0.05
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@dataclasses.dataclass(frozen=True)
|
| 72 |
+
class PiBehaviorConfig(_model.BaseModelConfig):
|
| 73 |
+
dtype: str = "bfloat16"
|
| 74 |
+
paligemma_variant: _gemma.Variant = "gemma_2b"
|
| 75 |
+
action_expert_variant: _gemma.Variant = "gemma_300m"
|
| 76 |
+
|
| 77 |
+
# Set the model specific defaults.
|
| 78 |
+
action_dim: int = 32
|
| 79 |
+
action_horizon: int = 30
|
| 80 |
+
max_token_len: int = 200 # Only used for compatibility, not for actual tokenization
|
| 81 |
+
|
| 82 |
+
# Number of tasks in the behavior dataset
|
| 83 |
+
num_tasks: int = 50
|
| 84 |
+
# Task embedding dimension - will match the paligemma width
|
| 85 |
+
task_embedding_dim: int = None # type: ignore
|
| 86 |
+
# Maximum number of subtask states across all tasks
|
| 87 |
+
max_num_subtask_states: int = MAX_NUM_STAGES
|
| 88 |
+
|
| 89 |
+
# Path to task data JSON file for initialization
|
| 90 |
+
task_data_path: str = "b1k/BEHAVIOR-1K/docs/challenge/task_data.json"
|
| 91 |
+
|
| 92 |
+
# Whether to use correlated noise matching action covariance structure
|
| 93 |
+
# Requires correlation matrix in norm_stats (computed by compute_norm_stats.py)
|
| 94 |
+
use_correlated_noise: bool = True
|
| 95 |
+
|
| 96 |
+
# Shrinkage parameter for correlation regularization
|
| 97 |
+
# Applied as: S_regularized = beta * S + (1-beta) * I
|
| 98 |
+
# beta=1.0 means full correlation (no shrinkage)
|
| 99 |
+
# beta=0.7 means 70% correlation + 30% independence (recommended for robustness)
|
| 100 |
+
# beta=0.0 means independence (no correlation)
|
| 101 |
+
correlation_beta: float = 0.5
|
| 102 |
+
|
| 103 |
+
# FAST auxiliary training configuration
|
| 104 |
+
use_fast_auxiliary: bool = False # Enable FAST during training
|
| 105 |
+
fast_loss_weight: float = 0.1 # Weight for FAST loss (vs flow loss)
|
| 106 |
+
|
| 107 |
+
# Action dimensions to encode with FAST (default: 0:6, 7:23 = 22 dims)
|
| 108 |
+
# Format: "0:6,7:23" or list of tuples [(0, 6), (7, 23)]
|
| 109 |
+
fast_encoded_dims: str | list[tuple[int, int]] = "0:6,7:23"
|
| 110 |
+
|
| 111 |
+
# FAST tokenizer vocab size
|
| 112 |
+
fast_vocab_size: int = 1024
|
| 113 |
+
|
| 114 |
+
# Max FAST tokens to predict (truncate if exceeded)
|
| 115 |
+
max_fast_tokens: int = 32
|
| 116 |
+
|
| 117 |
+
# FAST tokenizer path (set during initialization, relative to assets_dir/asset_id)
|
| 118 |
+
fast_tokenizer_path: str | None = None
|
| 119 |
+
|
| 120 |
+
# KV cache transformation for cross-layer attention between VLM and action expert
|
| 121 |
+
# Allows each action expert layer to attend to a learned combination of all VLM layers
|
| 122 |
+
use_kv_transform: bool = True
|
| 123 |
+
|
| 124 |
+
# Knowledge insulation: stop action expert gradients from flowing to VLM backbone
|
| 125 |
+
# VLM trains on FAST tokens only, action expert on flow matching with frozen VLM features
|
| 126 |
+
# Implements approach from https://www.physicalintelligence.company/research/knowledge_insulation
|
| 127 |
+
use_knowledge_insulation: bool = True
|
| 128 |
+
|
| 129 |
+
# Subtask/stage prediction auxiliary loss weight (relative to action loss)
|
| 130 |
+
# Higher values emphasize stage prediction accuracy at the expense of action quality
|
| 131 |
+
subtask_loss_weight: float = 0.1
|
| 132 |
+
|
| 133 |
+
# Time threshold for inpainting during inference
|
| 134 |
+
# Stop enforcing inpainting constraint when t < threshold (let model be free in final steps)
|
| 135 |
+
time_threshold_inpaint: float = 0.3
|
| 136 |
+
|
| 137 |
+
# Vision backbone finetuning control
|
| 138 |
+
freeze_vision_backbone: bool = True
|
| 139 |
+
|
| 140 |
+
# DA3 spatial-language adapter. The DA3/ModernBERT branch is computed
|
| 141 |
+
# offline and supplied as tokens in Observation.spatial_tokens.
|
| 142 |
+
use_spatial_action_cross_attention: bool = False
|
| 143 |
+
spatial_token_dim: int = 1024
|
| 144 |
+
spatial_num_tokens: int = 320 # DA3 perc bank default: 128 + 96 + 96
|
| 145 |
+
spatial_num_heads: int = 8
|
| 146 |
+
spatial_residual_scale: float = 1.0
|
| 147 |
+
|
| 148 |
+
# Full DA3 spatial-language branch (supersedes the flat spatial_tokens adapter above):
|
| 149 |
+
# frozen DA3-GIANT runs INLINE in the data pipeline; the trainable bank builder + method-B
|
| 150 |
+
# cross-attention injection (action-expert layers 12-17) live in the model. Proven on RoboReal.
|
| 151 |
+
da3: "B1KDA3Config | None" = None
|
| 152 |
+
|
| 153 |
+
def __post_init__(self):
|
| 154 |
+
if self.task_embedding_dim is None:
|
| 155 |
+
paligemma_config = _gemma.get_config(self.paligemma_variant)
|
| 156 |
+
object.__setattr__(self, "task_embedding_dim", paligemma_config.width)
|
| 157 |
+
|
| 158 |
+
def get_fast_dim_ranges(self) -> list[tuple[int, int]]:
|
| 159 |
+
"""Parse fast_encoded_dims into list of ranges."""
|
| 160 |
+
if isinstance(self.fast_encoded_dims, str):
|
| 161 |
+
ranges = []
|
| 162 |
+
for range_str in self.fast_encoded_dims.split(','):
|
| 163 |
+
start, end = map(int, range_str.strip().split(':'))
|
| 164 |
+
ranges.append((start, end))
|
| 165 |
+
return ranges
|
| 166 |
+
return self.fast_encoded_dims
|
| 167 |
+
|
| 168 |
+
def get_total_fast_dims(self) -> int:
|
| 169 |
+
"""Get total number of dimensions encoded by FAST."""
|
| 170 |
+
return sum(end - start for start, end in self.get_fast_dim_ranges())
|
| 171 |
+
|
| 172 |
+
@property
|
| 173 |
+
@override
|
| 174 |
+
def model_type(self):
|
| 175 |
+
return "pi_behavior"
|
| 176 |
+
|
| 177 |
+
@override
|
| 178 |
+
def create(self, rng: at.KeyArrayLike) -> "PiBehavior":
|
| 179 |
+
from b1k.models.pi_behavior import PiBehavior
|
| 180 |
+
|
| 181 |
+
return PiBehavior(self, rngs=nnx.Rngs(rng))
|
| 182 |
+
|
| 183 |
+
@override
|
| 184 |
+
def inputs_spec(self, *, batch_size: int = 1) -> tuple["Observation", _model.Actions]:
|
| 185 |
+
image_spec = jax.ShapeDtypeStruct([batch_size, *_model.IMAGE_RESOLUTION, 3], jnp.float32)
|
| 186 |
+
image_mask_spec = jax.ShapeDtypeStruct([batch_size], jnp.bool_)
|
| 187 |
+
|
| 188 |
+
with at.disable_typechecking():
|
| 189 |
+
obs_kwargs = {
|
| 190 |
+
"images": {
|
| 191 |
+
"base_0_rgb": image_spec,
|
| 192 |
+
"left_wrist_0_rgb": image_spec,
|
| 193 |
+
"right_wrist_0_rgb": image_spec,
|
| 194 |
+
},
|
| 195 |
+
"image_masks": {
|
| 196 |
+
"base_0_rgb": image_mask_spec,
|
| 197 |
+
"left_wrist_0_rgb": image_mask_spec,
|
| 198 |
+
"right_wrist_0_rgb": image_mask_spec,
|
| 199 |
+
},
|
| 200 |
+
"state": jax.ShapeDtypeStruct([batch_size, self.action_dim], jnp.float32),
|
| 201 |
+
"tokenized_prompt": jax.ShapeDtypeStruct([batch_size, 2], jnp.int32),
|
| 202 |
+
"tokenized_prompt_mask": jax.ShapeDtypeStruct([batch_size, 2], bool),
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
if self.use_fast_auxiliary:
|
| 206 |
+
obs_kwargs["fast_tokens"] = jax.ShapeDtypeStruct([batch_size, self.max_fast_tokens], jnp.int32)
|
| 207 |
+
obs_kwargs["fast_token_mask"] = jax.ShapeDtypeStruct([batch_size, self.max_fast_tokens], bool)
|
| 208 |
+
|
| 209 |
+
if self.da3 is not None and self.da3.enabled:
|
| 210 |
+
d = self.da3
|
| 211 |
+
gh, gw = d.grid_hw
|
| 212 |
+
obs_kwargs["da3_features"] = jax.ShapeDtypeStruct(
|
| 213 |
+
[batch_size, d.da3_layers, d.num_views, d.da3_channels, gh, gw], jnp.uint16
|
| 214 |
+
)
|
| 215 |
+
obs_kwargs["da3_ray"] = jax.ShapeDtypeStruct([batch_size, d.num_views, 3, gh, gw], jnp.float32)
|
| 216 |
+
obs_kwargs["da3_depth"] = jax.ShapeDtypeStruct([batch_size, d.num_views, 1, gh, gw], jnp.float32)
|
| 217 |
+
obs_kwargs["camera_extrinsics"] = jax.ShapeDtypeStruct([batch_size, d.num_views, 4, 4], jnp.float32)
|
| 218 |
+
obs_kwargs["lang_feat"] = jax.ShapeDtypeStruct([batch_size, d.lang_max_len, d.lang_dim], jnp.float32)
|
| 219 |
+
obs_kwargs["lang_mask"] = jax.ShapeDtypeStruct([batch_size, d.lang_max_len], bool)
|
| 220 |
+
|
| 221 |
+
if self.use_spatial_action_cross_attention:
|
| 222 |
+
obs_kwargs["spatial_tokens"] = jax.ShapeDtypeStruct(
|
| 223 |
+
[batch_size, self.spatial_num_tokens, self.spatial_token_dim],
|
| 224 |
+
jnp.float32,
|
| 225 |
+
)
|
| 226 |
+
obs_kwargs["spatial_token_mask"] = jax.ShapeDtypeStruct(
|
| 227 |
+
[batch_size, self.spatial_num_tokens],
|
| 228 |
+
bool,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
observation_spec = Observation(**obs_kwargs)
|
| 232 |
+
|
| 233 |
+
action_spec = jax.ShapeDtypeStruct([batch_size, self.action_horizon, self.action_dim], jnp.float32)
|
| 234 |
+
return observation_spec, action_spec
|
pibehavior_da3_clean_up_your_desk_40k/source_code/model/spatial_da3.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DA3 spatial-language branch for pi0.5 (JAX/Flax nnx port of the X-VLA addon).
|
| 2 |
+
|
| 3 |
+
Faithful reimplementation of the TRAINABLE modules from
|
| 4 |
+
`DA3-XVLA-cache/models/spatial_language.py` (H=1024, GIANT C=1536, grid 18x24=432,
|
| 5 |
+
perceiver tokens 128/96/96, 7-ch scale-aware ray, ModernBERT language fusion).
|
| 6 |
+
|
| 7 |
+
The FROZEN DA3 backbone + ModernBERT run offline (features precached); this module
|
| 8 |
+
consumes their outputs as arrays and produces per-view "banks" that are cross-attended
|
| 9 |
+
into the action-expert's late blocks (see gemma.py `SpatialActionInjection`).
|
| 10 |
+
|
| 11 |
+
Only the bank BUILDER lives here (nnx, a submodule of Pi0). The injection layer lives
|
| 12 |
+
in gemma.py (linen, inside the action-expert scan). Both use identical X-VLA math.
|
| 13 |
+
|
| 14 |
+
Reference math (verified by the understand-phase spec):
|
| 15 |
+
- ResidualCrossAttention: out = q_hidden + scale * MHA(LN_q(q_hidden), LN_kv(kv), LN_kv(kv))
|
| 16 |
+
- MHA matches torch nn.MultiheadAttention: separate q/k/v/out Linears w/ bias, 1/sqrt(head_dim).
|
| 17 |
+
- GELU is the tanh approximation everywhere; LayerNorm eps=1e-5.
|
| 18 |
+
- Perceiver residual adds the RAW learned query (not the normalized one).
|
| 19 |
+
- View order everywhere: 0=main/countertop, 1=left wrist, 2=right wrist.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import einops
|
| 23 |
+
import flax.nnx as nnx
|
| 24 |
+
import jax
|
| 25 |
+
import jax.numpy as jnp
|
| 26 |
+
|
| 27 |
+
import openpi.shared.array_typing as at
|
| 28 |
+
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
# primitives
|
| 31 |
+
# ---------------------------------------------------------------------------
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _gelu(x):
|
| 35 |
+
return nnx.gelu(x, approximate=True) # tanh approximation (matches torch GELU(approximate="tanh"))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class MHACrossAttn(nnx.Module):
|
| 39 |
+
"""Multi-head cross-attention matching torch nn.MultiheadAttention math (no residual, no norm)."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, dim: int, num_heads: int, *, rngs: nnx.Rngs):
|
| 42 |
+
assert dim % num_heads == 0
|
| 43 |
+
self.num_heads = num_heads
|
| 44 |
+
self.head_dim = dim // num_heads
|
| 45 |
+
self.q_proj = nnx.Linear(dim, dim, rngs=rngs)
|
| 46 |
+
self.k_proj = nnx.Linear(dim, dim, rngs=rngs)
|
| 47 |
+
self.v_proj = nnx.Linear(dim, dim, rngs=rngs)
|
| 48 |
+
self.out_proj = nnx.Linear(dim, dim, rngs=rngs)
|
| 49 |
+
|
| 50 |
+
def __call__(self, q, kv, key_pad_mask=None):
|
| 51 |
+
# q:[b,Lq,d] kv:[b,Lk,d] key_pad_mask:[b,Lk] True=pad (ignored)
|
| 52 |
+
h = self.num_heads
|
| 53 |
+
Q = einops.rearrange(self.q_proj(q), "b l (h d) -> b h l d", h=h)
|
| 54 |
+
K = einops.rearrange(self.k_proj(kv), "b l (h d) -> b h l d", h=h)
|
| 55 |
+
V = einops.rearrange(self.v_proj(kv), "b l (h d) -> b h l d", h=h)
|
| 56 |
+
logits = jnp.einsum("bhqd,bhkd->bhqk", Q, K) * (self.head_dim**-0.5)
|
| 57 |
+
if key_pad_mask is not None:
|
| 58 |
+
logits = jnp.where(key_pad_mask[:, None, None, :], jnp.asarray(-1e30, logits.dtype), logits)
|
| 59 |
+
probs = jax.nn.softmax(logits.astype(jnp.float32), axis=-1).astype(logits.dtype)
|
| 60 |
+
ctx = jnp.einsum("bhqk,bhkd->bhqd", probs, V)
|
| 61 |
+
ctx = einops.rearrange(ctx, "b h q d -> b q (h d)")
|
| 62 |
+
return self.out_proj(ctx)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class ResidualCrossAttn(nnx.Module):
|
| 66 |
+
"""Pre-LN residual cross-attention: out = q_hidden + scale * MHA(LN_q(q_hidden), LN_kv(kv))."""
|
| 67 |
+
|
| 68 |
+
def __init__(self, dim: int, num_heads: int, *, rngs: nnx.Rngs):
|
| 69 |
+
self.q_norm = nnx.LayerNorm(dim, epsilon=1e-5, rngs=rngs)
|
| 70 |
+
self.kv_norm = nnx.LayerNorm(dim, epsilon=1e-5, rngs=rngs)
|
| 71 |
+
self.attn = MHACrossAttn(dim, num_heads, rngs=rngs)
|
| 72 |
+
|
| 73 |
+
def __call__(self, q_hidden, kv_hidden, key_pad_mask=None, residual_scale: float = 1.0):
|
| 74 |
+
q = self.q_norm(q_hidden)
|
| 75 |
+
kv = self.kv_norm(kv_hidden)
|
| 76 |
+
out = self.attn(q, kv, key_pad_mask=key_pad_mask)
|
| 77 |
+
return q_hidden + residual_scale * out
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class ResidualMlp(nnx.Module):
|
| 81 |
+
"""Pre-LN residual MLP: x + Linear2(gelu(Linear1(LN(x))))."""
|
| 82 |
+
|
| 83 |
+
def __init__(self, dim: int, mlp_ratio: float, *, rngs: nnx.Rngs):
|
| 84 |
+
hidden = int(dim * mlp_ratio)
|
| 85 |
+
self.norm = nnx.LayerNorm(dim, epsilon=1e-5, rngs=rngs)
|
| 86 |
+
self.fc1 = nnx.Linear(dim, hidden, rngs=rngs)
|
| 87 |
+
self.fc2 = nnx.Linear(hidden, dim, rngs=rngs)
|
| 88 |
+
|
| 89 |
+
def __call__(self, x):
|
| 90 |
+
return x + self.fc2(_gelu(self.fc1(self.norm(x))))
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class ProjLN(nnx.Module):
|
| 94 |
+
"""Linear(in->H) -> gelu -> Linear(H->H) -> LayerNorm(H). Used for layer projectors & t5_projector."""
|
| 95 |
+
|
| 96 |
+
def __init__(self, in_dim: int, dim: int, *, rngs: nnx.Rngs):
|
| 97 |
+
self.fc1 = nnx.Linear(in_dim, dim, rngs=rngs)
|
| 98 |
+
self.fc2 = nnx.Linear(dim, dim, rngs=rngs)
|
| 99 |
+
self.norm = nnx.LayerNorm(dim, epsilon=1e-5, rngs=rngs)
|
| 100 |
+
|
| 101 |
+
def __call__(self, x):
|
| 102 |
+
return self.norm(self.fc2(_gelu(self.fc1(x))))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class Mlp2(nnx.Module):
|
| 106 |
+
"""Linear(in->hidden) -> gelu -> Linear(hidden->out). Used for ray_mlp & pos2d_mlp (no LN)."""
|
| 107 |
+
|
| 108 |
+
def __init__(self, in_dim: int, hidden: int, out_dim: int, *, rngs: nnx.Rngs):
|
| 109 |
+
self.fc1 = nnx.Linear(in_dim, hidden, rngs=rngs)
|
| 110 |
+
self.fc2 = nnx.Linear(hidden, out_dim, rngs=rngs)
|
| 111 |
+
|
| 112 |
+
def __call__(self, x):
|
| 113 |
+
return self.fc2(_gelu(self.fc1(x)))
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class PerceiverDownsampler(nnx.Module):
|
| 117 |
+
"""432 grid tokens -> K learned-query tokens (single cross-attn + residual MLP)."""
|
| 118 |
+
|
| 119 |
+
def __init__(self, dim: int, num_queries: int, num_heads: int, *, query_std: float = 0.02, rngs: nnx.Rngs):
|
| 120 |
+
key = rngs.params()
|
| 121 |
+
self.query = nnx.Param(jax.random.normal(key, (1, num_queries, dim)) * query_std)
|
| 122 |
+
self.xattn = ResidualCrossAttn(dim, num_heads, rngs=rngs)
|
| 123 |
+
self.mlp = ResidualMlp(dim, mlp_ratio=2.0, rngs=rngs)
|
| 124 |
+
|
| 125 |
+
def __call__(self, tokens):
|
| 126 |
+
b = tokens.shape[0]
|
| 127 |
+
q = jnp.broadcast_to(self.query.value, (b, *self.query.value.shape[1:]))
|
| 128 |
+
z = self.xattn(q, tokens, residual_scale=1.0) # residual adds RAW q (matches X-VLA)
|
| 129 |
+
return self.mlp(z)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class LanguageFusionStack(nnx.Module):
|
| 133 |
+
"""N x [cross-attn(bank, lang) + residual-MLP], with language padding mask."""
|
| 134 |
+
|
| 135 |
+
def __init__(self, dim: int, depth: int, num_heads: int, *, rngs: nnx.Rngs):
|
| 136 |
+
self.layers = [
|
| 137 |
+
(ResidualCrossAttn(dim, num_heads, rngs=rngs), ResidualMlp(dim, mlp_ratio=4.0, rngs=rngs))
|
| 138 |
+
for _ in range(depth)
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
def __call__(self, geo, lang_tokens, lang_pad_mask):
|
| 142 |
+
for xattn, mlp in self.layers:
|
| 143 |
+
geo = xattn(geo, lang_tokens, key_pad_mask=lang_pad_mask, residual_scale=1.0)
|
| 144 |
+
geo = mlp(geo)
|
| 145 |
+
return geo
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# ---------------------------------------------------------------------------
|
| 149 |
+
# geometry helpers
|
| 150 |
+
# ---------------------------------------------------------------------------
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def compute_world_ray_6d(ray_local, ext_w2c):
|
| 154 |
+
"""ray_local [b,3,h,w] cam-local unit dir; ext_w2c [b,4,4] OpenCV world->cam.
|
| 155 |
+
|
| 156 |
+
Returns [b,6,h,w] = concat([origin_world(camera center), dir_world]).
|
| 157 |
+
"""
|
| 158 |
+
R_w2c = ext_w2c[:, :3, :3] # [b,3,3]
|
| 159 |
+
t_w2c = ext_w2c[:, :3, 3] # [b,3]
|
| 160 |
+
R_c2w = jnp.swapaxes(R_w2c, -1, -2)
|
| 161 |
+
pos_world = -jnp.einsum("bij,bj->bi", R_c2w, t_w2c) # [b,3] camera center in world
|
| 162 |
+
b, _, h, w = ray_local.shape
|
| 163 |
+
dir_world = jnp.einsum("bij,bjk->bik", R_c2w, ray_local.reshape(b, 3, h * w)).reshape(b, 3, h, w)
|
| 164 |
+
origin = jnp.broadcast_to(pos_world[:, :, None, None], (b, 3, h, w))
|
| 165 |
+
return jnp.concatenate([origin, dir_world], axis=1) # [b,6,h,w]
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _grid_coords(h: int, w: int):
|
| 169 |
+
v = 2.0 * jnp.arange(h) / (h - 1) - 1.0
|
| 170 |
+
u = 2.0 * jnp.arange(w) / (w - 1) - 1.0
|
| 171 |
+
yy, xx = jnp.meshgrid(v, u, indexing="ij")
|
| 172 |
+
return jnp.stack([xx, yy], axis=-1).reshape(1, h * w, 2) # [1,432,2] (x=u, y=v), row-major
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ---------------------------------------------------------------------------
|
| 176 |
+
# bank builder
|
| 177 |
+
# ---------------------------------------------------------------------------
|
| 178 |
+
|
| 179 |
+
_VIEWS = (("main", 0, 128), ("left", 1, 96), ("right", 2, 96))
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class SpatialBankBuilder(nnx.Module):
|
| 183 |
+
"""Cached DA3 (feats/ray/depth) + extrinsics + ModernBERT feats -> 3 per-view banks."""
|
| 184 |
+
|
| 185 |
+
def __init__(
|
| 186 |
+
self,
|
| 187 |
+
*,
|
| 188 |
+
hidden_dim: int = 1024,
|
| 189 |
+
da3_channels: int = 1536,
|
| 190 |
+
num_layers: int = 4,
|
| 191 |
+
grid_hw: tuple[int, int] = (18, 24),
|
| 192 |
+
lang_dim: int = 1024, # ModernBERT-large last_hidden width (768) -> set by config
|
| 193 |
+
num_heads: int = 8,
|
| 194 |
+
lang_fusion_depth: int = 2,
|
| 195 |
+
perceiver_query_std: float = 0.02,
|
| 196 |
+
bank_token_embed: bool = False,
|
| 197 |
+
rngs: nnx.Rngs,
|
| 198 |
+
):
|
| 199 |
+
H = hidden_dim
|
| 200 |
+
self.hidden_dim = H
|
| 201 |
+
self.num_layers = num_layers
|
| 202 |
+
self.grid_hw = grid_hw
|
| 203 |
+
# (a) per-tap projectors + layer embed + fuse
|
| 204 |
+
self.layer_projectors = [ProjLN(da3_channels, H, rngs=rngs) for _ in range(num_layers)]
|
| 205 |
+
self.layer_embed = nnx.Param(jax.random.normal(rngs.params(), (num_layers, H)) * 0.02)
|
| 206 |
+
self.layer_fuse = nnx.Linear(num_layers * H, H, rngs=rngs)
|
| 207 |
+
# (b) scale-aware ray (Plucker-6 + log-depth = 7)
|
| 208 |
+
self.ray_mlp = Mlp2(7, 256, H, rngs=rngs)
|
| 209 |
+
# (c) 2D grid pos + per-view embedding
|
| 210 |
+
self.pos2d_mlp = Mlp2(2, 256, H, rngs=rngs)
|
| 211 |
+
self.view_embed = nnx.Embed(3, H, rngs=rngs)
|
| 212 |
+
# (d) language projector (ModernBERT feat -> H)
|
| 213 |
+
self.t5_projector = ProjLN(lang_dim, H, rngs=rngs)
|
| 214 |
+
# (e) per-view perceiver + language fusion
|
| 215 |
+
self.perceivers = {
|
| 216 |
+
name: PerceiverDownsampler(H, k, num_heads, query_std=perceiver_query_std, rngs=rngs)
|
| 217 |
+
for name, _, k in _VIEWS
|
| 218 |
+
}
|
| 219 |
+
self.lang_fusers = {name: LanguageFusionStack(H, lang_fusion_depth, num_heads, rngs=rngs) for name, _, _ in _VIEWS}
|
| 220 |
+
# (f) v2: learned per-token embedding added to each view's FINAL bank tokens. Guarantees
|
| 221 |
+
# persistent cross-token diversity — the quantity that drives softmax gradients to the
|
| 222 |
+
# injection's Q/K (shared content cancels in the softmax jacobian, so without this the
|
| 223 |
+
# attention pattern barely trains; measured ~1000x slower than V/out in v1).
|
| 224 |
+
self.bank_token_embeds = (
|
| 225 |
+
{name: nnx.Param(jax.random.normal(rngs.params(), (1, k, H)) * 0.05) for name, _, k in _VIEWS}
|
| 226 |
+
if bank_token_embed
|
| 227 |
+
else None
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
def _fuse_layers(self, feats_v):
|
| 231 |
+
# feats_v: [b, num_layers, C, h, w] -> [b, 432, H]
|
| 232 |
+
b, L, C, h, w = feats_v.shape
|
| 233 |
+
parts = []
|
| 234 |
+
for li in range(self.num_layers):
|
| 235 |
+
flat = einops.rearrange(feats_v[:, li], "b c h w -> b (h w) c") # row-major
|
| 236 |
+
p = self.layer_projectors[li](flat) + self.layer_embed.value[li][None, None, :]
|
| 237 |
+
parts.append(p)
|
| 238 |
+
return self.layer_fuse(jnp.concatenate(parts, axis=-1))
|
| 239 |
+
|
| 240 |
+
def _ray7(self, ray_v, depth_v, ext_v):
|
| 241 |
+
# ray_v [b,3,h,w], depth_v [b,1,h,w], ext_v [b,4,4] -> [b,432,7]
|
| 242 |
+
ray6 = compute_world_ray_6d(ray_v, ext_v) # [b,6,h,w]
|
| 243 |
+
logd = jnp.log(jnp.clip(depth_v.astype(jnp.float32), a_min=1e-3)).astype(ray6.dtype) # [b,1,h,w]
|
| 244 |
+
ray7 = jnp.concatenate([ray6, logd], axis=1) # [b,7,h,w]
|
| 245 |
+
return einops.rearrange(ray7, "b c h w -> b (h w) c")
|
| 246 |
+
|
| 247 |
+
def __call__(self, feats, ray, depth, extrinsics, lang_feat, lang_mask):
|
| 248 |
+
# feats [b,L,V,C,h,w]; ray [b,V,3,h,w]; depth [b,V,1,h,w]; extrinsics [b,V,4,4]
|
| 249 |
+
# lang_feat [b,Lt,lang_dim]; lang_mask [b,Lt] True=real token
|
| 250 |
+
h, w = self.grid_hw
|
| 251 |
+
pos_emb = self.pos2d_mlp(_grid_coords(h, w).astype(feats.dtype)) # [1,432,H]
|
| 252 |
+
lang_tokens = self.t5_projector(lang_feat) # [b,Lt,H]
|
| 253 |
+
lang_pad = jnp.logical_not(lang_mask) # True=pad
|
| 254 |
+
banks = {}
|
| 255 |
+
for name, vidx, _k in _VIEWS:
|
| 256 |
+
fused = self._fuse_layers(feats[:, :, vidx]) # [b,432,H]
|
| 257 |
+
ray_flat = self._ray7(ray[:, vidx], depth[:, vidx], extrinsics[:, vidx]) # [b,432,7]
|
| 258 |
+
ray_emb = self.ray_mlp(ray_flat.astype(feats.dtype)) # [b,432,H]
|
| 259 |
+
view_emb = self.view_embed(jnp.asarray(vidx))[None, None, :] # [1,1,H]
|
| 260 |
+
spatial = fused + view_emb + pos_emb + ray_emb # [b,432,H]
|
| 261 |
+
geo = self.perceivers[name](spatial) # [b,K,H]
|
| 262 |
+
bank = self.lang_fusers[name](geo, lang_tokens, lang_pad) # [b,K,H]
|
| 263 |
+
if self.bank_token_embeds is not None:
|
| 264 |
+
bank = bank + self.bank_token_embeds[name].value
|
| 265 |
+
banks[name] = bank
|
| 266 |
+
return banks
|
pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/data_loader.py
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections.abc import Iterator, Sequence
|
| 2 |
+
import logging
|
| 3 |
+
import multiprocessing
|
| 4 |
+
import os
|
| 5 |
+
import queue
|
| 6 |
+
import threading
|
| 7 |
+
import typing
|
| 8 |
+
from typing import Literal, Protocol, SupportsIndex, TypeVar
|
| 9 |
+
|
| 10 |
+
import jax
|
| 11 |
+
import jax.numpy as jnp
|
| 12 |
+
import lerobot.datasets.lerobot_dataset as lerobot_dataset
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
import openpi.models.model as _model
|
| 17 |
+
import openpi.training.config as _config
|
| 18 |
+
from openpi.training.droid_rlds_dataset import DroidRldsDataset
|
| 19 |
+
import openpi.transforms as _transforms
|
| 20 |
+
|
| 21 |
+
T_co = TypeVar("T_co", covariant=True)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Dataset(Protocol[T_co]):
|
| 25 |
+
"""Interface for a dataset with random access."""
|
| 26 |
+
|
| 27 |
+
def __getitem__(self, index: SupportsIndex) -> T_co:
|
| 28 |
+
raise NotImplementedError("Subclasses of Dataset should implement __getitem__.")
|
| 29 |
+
|
| 30 |
+
def __len__(self) -> int:
|
| 31 |
+
raise NotImplementedError("Subclasses of Dataset should implement __len__.")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class IterableDataset(Protocol[T_co]):
|
| 35 |
+
"""Interface for an iterable dataset."""
|
| 36 |
+
|
| 37 |
+
def __iter__(self) -> Iterator[T_co]:
|
| 38 |
+
raise NotImplementedError("Subclasses of IterableDataset should implement __iter__.")
|
| 39 |
+
|
| 40 |
+
def __len__(self) -> int:
|
| 41 |
+
raise NotImplementedError("Subclasses of Dataset should implement __len__.")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class DataLoader(Protocol[T_co]):
|
| 45 |
+
"""Interface for a data loader."""
|
| 46 |
+
|
| 47 |
+
def data_config(self) -> _config.DataConfig:
|
| 48 |
+
"""Get the data config for this data loader."""
|
| 49 |
+
raise NotImplementedError("Subclasses of DataLoader should implement data_config.")
|
| 50 |
+
|
| 51 |
+
def __iter__(self) -> Iterator[T_co]:
|
| 52 |
+
raise NotImplementedError("Subclasses of DataLoader should implement __iter__.")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class TransformedDataset(Dataset[T_co]):
|
| 56 |
+
def __init__(self, dataset: Dataset, transforms: Sequence[_transforms.DataTransformFn]):
|
| 57 |
+
self._dataset = dataset
|
| 58 |
+
self._transform = _transforms.compose(transforms)
|
| 59 |
+
|
| 60 |
+
def __getitem__(self, index: SupportsIndex) -> T_co:
|
| 61 |
+
return self._transform(self._dataset[index])
|
| 62 |
+
|
| 63 |
+
def __len__(self) -> int:
|
| 64 |
+
return len(self._dataset)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class IterableTransformedDataset(IterableDataset[T_co]):
|
| 68 |
+
def __init__(
|
| 69 |
+
self,
|
| 70 |
+
dataset: IterableDataset,
|
| 71 |
+
transforms: Sequence[_transforms.DataTransformFn],
|
| 72 |
+
*,
|
| 73 |
+
is_batched: bool = False,
|
| 74 |
+
):
|
| 75 |
+
self._dataset = dataset
|
| 76 |
+
self._transform = _transforms.compose(transforms)
|
| 77 |
+
self._is_batched = is_batched
|
| 78 |
+
|
| 79 |
+
def __iter__(self):
|
| 80 |
+
for sample in self._dataset:
|
| 81 |
+
if self._is_batched:
|
| 82 |
+
# Transforms are designed to be applied to individual samples. So we need to split the batch into
|
| 83 |
+
# individual samples and apply the transform to each sample individually.
|
| 84 |
+
batch_size = next(v.shape[0] for v in sample.values())
|
| 85 |
+
|
| 86 |
+
# Split batch into individual samples using tree_map
|
| 87 |
+
individual_samples = [jax.tree.map(lambda x: x[i], sample) for i in range(batch_size)] # noqa: B023
|
| 88 |
+
|
| 89 |
+
# Transform each sample
|
| 90 |
+
transformed = [self._transform(s) for s in individual_samples]
|
| 91 |
+
|
| 92 |
+
# Recombine batch with tree_map
|
| 93 |
+
yield jax.tree.map(lambda *x: np.stack(x, axis=0), *transformed)
|
| 94 |
+
else:
|
| 95 |
+
yield self._transform(sample)
|
| 96 |
+
|
| 97 |
+
def __len__(self) -> int:
|
| 98 |
+
return len(self._dataset)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class FakeDataset(Dataset):
|
| 102 |
+
def __init__(self, model_config: _model.BaseModelConfig, num_samples: int):
|
| 103 |
+
self._num_samples = num_samples
|
| 104 |
+
self._observation_spec, self._action_spec = model_config.inputs_spec()
|
| 105 |
+
|
| 106 |
+
def __getitem__(self, index: SupportsIndex) -> dict:
|
| 107 |
+
rng = jax.random.key(index.__index__())
|
| 108 |
+
|
| 109 |
+
def make_from_spec(spec: jax.ShapeDtypeStruct):
|
| 110 |
+
nonlocal rng
|
| 111 |
+
rng, data_rng = jax.random.split(rng)
|
| 112 |
+
# Remove the batch dimension.
|
| 113 |
+
shape = spec.shape[1:]
|
| 114 |
+
if spec.dtype == jnp.float32:
|
| 115 |
+
return jax.random.uniform(data_rng, shape=shape, minval=-1.0, maxval=1.0)
|
| 116 |
+
if spec.dtype == jnp.int32:
|
| 117 |
+
return jax.random.randint(data_rng, shape=shape, minval=0, maxval=2048)
|
| 118 |
+
return jnp.zeros(shape=shape, dtype=spec.dtype)
|
| 119 |
+
|
| 120 |
+
observation = jax.tree.map(make_from_spec, self._observation_spec)
|
| 121 |
+
action = jax.tree.map(make_from_spec, self._action_spec)
|
| 122 |
+
|
| 123 |
+
return {
|
| 124 |
+
**observation.to_dict(),
|
| 125 |
+
"actions": action,
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
def __len__(self) -> int:
|
| 129 |
+
return self._num_samples
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def create_behavior_dataset(data_config: _config.DataConfig, action_horizon: int) -> Dataset:
|
| 133 |
+
"""Create a dataset for training."""
|
| 134 |
+
from omnigibson.learning.datas.lerobot_dataset import BehaviorLeRobotDataset
|
| 135 |
+
|
| 136 |
+
dataset = BehaviorLeRobotDataset(
|
| 137 |
+
repo_id=data_config.repo_id,
|
| 138 |
+
root=data_config.behavior_dataset_root,
|
| 139 |
+
tasks=["turning_on_radio"],
|
| 140 |
+
modalities=["rgb"],
|
| 141 |
+
local_only=True,
|
| 142 |
+
delta_timestamps={
|
| 143 |
+
key: [t / 30.0 for t in range(action_horizon)] for key in data_config.action_sequence_keys
|
| 144 |
+
},
|
| 145 |
+
episodes=data_config.episodes_index,
|
| 146 |
+
chunk_streaming_using_keyframe=True,
|
| 147 |
+
shuffle=True,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
if data_config.prompt_from_task:
|
| 151 |
+
dataset = TransformedDataset(dataset, [_transforms.PromptFromLeRobotTask(dataset.meta.tasks)])
|
| 152 |
+
|
| 153 |
+
return dataset
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def create_torch_dataset(
|
| 157 |
+
data_config: _config.DataConfig, action_horizon: int, model_config: _model.BaseModelConfig
|
| 158 |
+
) -> Dataset:
|
| 159 |
+
"""Create a dataset for training."""
|
| 160 |
+
repo_id = data_config.repo_id
|
| 161 |
+
if repo_id is None:
|
| 162 |
+
raise ValueError("Repo ID is not set. Cannot create dataset.")
|
| 163 |
+
if repo_id == "fake":
|
| 164 |
+
return FakeDataset(model_config, num_samples=1024)
|
| 165 |
+
|
| 166 |
+
dataset_meta = lerobot_dataset.LeRobotDatasetMetadata(repo_id)
|
| 167 |
+
dataset = lerobot_dataset.LeRobotDataset(
|
| 168 |
+
data_config.repo_id,
|
| 169 |
+
delta_timestamps={
|
| 170 |
+
key: [t / dataset_meta.fps for t in range(action_horizon)] for key in data_config.action_sequence_keys
|
| 171 |
+
},
|
| 172 |
+
episodes=data_config.episodes_index,
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
if data_config.prompt_from_task:
|
| 176 |
+
dataset = TransformedDataset(dataset, [_transforms.PromptFromLeRobotTask(dataset_meta.tasks)])
|
| 177 |
+
|
| 178 |
+
return dataset
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def create_rlds_dataset(
|
| 182 |
+
data_config: _config.DataConfig,
|
| 183 |
+
action_horizon: int,
|
| 184 |
+
batch_size: int,
|
| 185 |
+
*,
|
| 186 |
+
shuffle: bool = False,
|
| 187 |
+
) -> Dataset:
|
| 188 |
+
# At the moment, we only support DROID for RLDS datasets.
|
| 189 |
+
return DroidRldsDataset(
|
| 190 |
+
data_dir=data_config.rlds_data_dir,
|
| 191 |
+
batch_size=batch_size,
|
| 192 |
+
shuffle=shuffle,
|
| 193 |
+
action_chunk_size=action_horizon,
|
| 194 |
+
action_space=data_config.action_space,
|
| 195 |
+
filter_dict_path=data_config.filter_dict_path,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def transform_dataset(dataset: Dataset, data_config: _config.DataConfig, *, skip_norm_stats: bool = False) -> Dataset:
|
| 200 |
+
"""Transform the dataset by applying the data transforms."""
|
| 201 |
+
norm_stats = {}
|
| 202 |
+
if data_config.repo_id != "fake" and not skip_norm_stats:
|
| 203 |
+
if data_config.norm_stats is None:
|
| 204 |
+
raise ValueError(
|
| 205 |
+
"Normalization stats not found. "
|
| 206 |
+
"Make sure to run `scripts/compute_norm_stats.py --config-name=<your-config>`."
|
| 207 |
+
)
|
| 208 |
+
norm_stats = data_config.norm_stats
|
| 209 |
+
|
| 210 |
+
return TransformedDataset(
|
| 211 |
+
dataset,
|
| 212 |
+
[
|
| 213 |
+
*data_config.repack_transforms.inputs,
|
| 214 |
+
*data_config.data_transforms.inputs,
|
| 215 |
+
_transforms.Normalize(norm_stats, use_quantiles=data_config.use_quantile_norm),
|
| 216 |
+
*data_config.model_transforms.inputs,
|
| 217 |
+
],
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def transform_iterable_dataset(
|
| 222 |
+
dataset: IterableDataset,
|
| 223 |
+
data_config: _config.DataConfig,
|
| 224 |
+
*,
|
| 225 |
+
skip_norm_stats: bool = False,
|
| 226 |
+
is_batched: bool = False,
|
| 227 |
+
) -> IterableDataset:
|
| 228 |
+
"""Transform the dataset by applying the data transforms."""
|
| 229 |
+
norm_stats = {}
|
| 230 |
+
if data_config.repo_id != "fake" and not skip_norm_stats:
|
| 231 |
+
if data_config.norm_stats is None:
|
| 232 |
+
raise ValueError(
|
| 233 |
+
"Normalization stats not found. "
|
| 234 |
+
"Make sure to run `scripts/compute_norm_stats.py --config-name=<your-config>`."
|
| 235 |
+
)
|
| 236 |
+
norm_stats = data_config.norm_stats
|
| 237 |
+
|
| 238 |
+
return IterableTransformedDataset(
|
| 239 |
+
dataset,
|
| 240 |
+
[
|
| 241 |
+
*data_config.repack_transforms.inputs,
|
| 242 |
+
*data_config.data_transforms.inputs,
|
| 243 |
+
_transforms.Normalize(norm_stats, use_quantiles=data_config.use_quantile_norm),
|
| 244 |
+
*data_config.model_transforms.inputs,
|
| 245 |
+
],
|
| 246 |
+
is_batched=is_batched,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def create_data_loader(
|
| 251 |
+
config: _config.TrainConfig,
|
| 252 |
+
*,
|
| 253 |
+
sharding: jax.sharding.Sharding | None = None,
|
| 254 |
+
shuffle: bool = False,
|
| 255 |
+
num_batches: int | None = None,
|
| 256 |
+
skip_norm_stats: bool = False,
|
| 257 |
+
framework: Literal["jax", "pytorch"] = "jax",
|
| 258 |
+
) -> DataLoader[tuple[_model.Observation, _model.Actions]]:
|
| 259 |
+
"""Create a data loader for training.
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
config: The training configuration.
|
| 263 |
+
sharding: The sharding to use for the data loader (JAX only).
|
| 264 |
+
shuffle: Whether to shuffle the data.
|
| 265 |
+
num_batches: Determines the number of batches to return.
|
| 266 |
+
skip_norm_stats: Whether to skip data normalization.
|
| 267 |
+
framework: The framework to use ("jax" or "pytorch").
|
| 268 |
+
"""
|
| 269 |
+
data_config = config.data.create(config.assets_dirs, config.model)
|
| 270 |
+
logging.info(f"data_config: {data_config}")
|
| 271 |
+
|
| 272 |
+
if data_config.rlds_data_dir is not None:
|
| 273 |
+
return create_rlds_data_loader(
|
| 274 |
+
data_config,
|
| 275 |
+
action_horizon=config.model.action_horizon,
|
| 276 |
+
batch_size=config.batch_size,
|
| 277 |
+
sharding=sharding,
|
| 278 |
+
shuffle=shuffle,
|
| 279 |
+
num_batches=num_batches,
|
| 280 |
+
skip_norm_stats=skip_norm_stats,
|
| 281 |
+
framework=framework,
|
| 282 |
+
)
|
| 283 |
+
return create_torch_data_loader(
|
| 284 |
+
data_config,
|
| 285 |
+
model_config=config.model,
|
| 286 |
+
action_horizon=config.model.action_horizon,
|
| 287 |
+
batch_size=config.batch_size,
|
| 288 |
+
sharding=sharding,
|
| 289 |
+
shuffle=shuffle,
|
| 290 |
+
num_batches=num_batches,
|
| 291 |
+
num_workers=config.num_workers,
|
| 292 |
+
seed=config.seed,
|
| 293 |
+
skip_norm_stats=skip_norm_stats,
|
| 294 |
+
framework=framework,
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def create_behavior_data_loader(
|
| 299 |
+
config: _config.TrainConfig,
|
| 300 |
+
*,
|
| 301 |
+
sharding: jax.sharding.Sharding | None = None,
|
| 302 |
+
shuffle: bool = False,
|
| 303 |
+
num_batches: int | None = None,
|
| 304 |
+
skip_norm_stats: bool = False,
|
| 305 |
+
) -> DataLoader[tuple[_model.Observation, _model.Actions]]:
|
| 306 |
+
data_config = config.data.create(config.assets_dirs, config.model)
|
| 307 |
+
dataset = create_behavior_dataset(data_config, action_horizon=config.model.action_horizon)
|
| 308 |
+
dataset = transform_dataset(dataset, data_config, skip_norm_stats=skip_norm_stats)
|
| 309 |
+
|
| 310 |
+
data_loader = TorchDataLoader(
|
| 311 |
+
dataset,
|
| 312 |
+
local_batch_size=config.batch_size // jax.process_count(),
|
| 313 |
+
sharding=sharding,
|
| 314 |
+
shuffle=shuffle,
|
| 315 |
+
num_batches=num_batches,
|
| 316 |
+
num_workers=config.num_workers,
|
| 317 |
+
seed=config.seed,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
return DataLoaderImpl(data_config, data_loader)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def create_torch_data_loader(
|
| 324 |
+
data_config: _config.DataConfig,
|
| 325 |
+
model_config: _model.BaseModelConfig,
|
| 326 |
+
action_horizon: int,
|
| 327 |
+
batch_size: int,
|
| 328 |
+
*,
|
| 329 |
+
sharding: jax.sharding.Sharding | None = None,
|
| 330 |
+
skip_norm_stats: bool = False,
|
| 331 |
+
shuffle: bool = False,
|
| 332 |
+
num_batches: int | None = None,
|
| 333 |
+
num_workers: int = 0,
|
| 334 |
+
seed: int = 0,
|
| 335 |
+
framework: str = "jax",
|
| 336 |
+
) -> DataLoader[tuple[_model.Observation, _model.Actions]]:
|
| 337 |
+
"""Create a data loader for training.
|
| 338 |
+
|
| 339 |
+
Args:
|
| 340 |
+
data_config: The data configuration.
|
| 341 |
+
action_horizon: The action horizon.
|
| 342 |
+
batch_size: The batch size.
|
| 343 |
+
sharding: The sharding to use for the data loader. If None, the data loader will
|
| 344 |
+
use a single device sharding.
|
| 345 |
+
skip_norm_stats: Whether to skip data normalization.
|
| 346 |
+
shuffle: Whether to shuffle the data.
|
| 347 |
+
num_batches: Determines the number of batches to return. If the number exceeds the
|
| 348 |
+
number of batches in the dataset, the data loader will loop over the dataset.
|
| 349 |
+
If not provided, will iterate over the dataset indefinitely.
|
| 350 |
+
num_workers: The number of worker processes to use. If zero, the data loader will
|
| 351 |
+
execute in the main process.
|
| 352 |
+
seed: The seed to use for shuffling the data.
|
| 353 |
+
"""
|
| 354 |
+
dataset = create_torch_dataset(data_config, action_horizon, model_config)
|
| 355 |
+
dataset = transform_dataset(dataset, data_config, skip_norm_stats=skip_norm_stats)
|
| 356 |
+
|
| 357 |
+
# Use TorchDataLoader for both frameworks
|
| 358 |
+
# For PyTorch DDP, create DistributedSampler and divide batch size by world size
|
| 359 |
+
# For JAX, divide by process count
|
| 360 |
+
sampler = None
|
| 361 |
+
if framework == "pytorch":
|
| 362 |
+
if torch.distributed.is_initialized():
|
| 363 |
+
sampler = torch.utils.data.distributed.DistributedSampler(
|
| 364 |
+
dataset,
|
| 365 |
+
num_replicas=torch.distributed.get_world_size(),
|
| 366 |
+
rank=torch.distributed.get_rank(),
|
| 367 |
+
shuffle=shuffle,
|
| 368 |
+
drop_last=True,
|
| 369 |
+
)
|
| 370 |
+
local_batch_size = batch_size // torch.distributed.get_world_size()
|
| 371 |
+
else:
|
| 372 |
+
local_batch_size = batch_size
|
| 373 |
+
else:
|
| 374 |
+
local_batch_size = batch_size // jax.process_count()
|
| 375 |
+
|
| 376 |
+
logging.info(f"local_batch_size: {local_batch_size}")
|
| 377 |
+
data_loader = TorchDataLoader(
|
| 378 |
+
dataset,
|
| 379 |
+
local_batch_size=local_batch_size,
|
| 380 |
+
sharding=None if framework == "pytorch" else sharding,
|
| 381 |
+
shuffle=(sampler is None and shuffle), # Don't shuffle if using sampler
|
| 382 |
+
sampler=sampler,
|
| 383 |
+
num_batches=num_batches,
|
| 384 |
+
num_workers=num_workers,
|
| 385 |
+
seed=seed,
|
| 386 |
+
framework=framework,
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
return DataLoaderImpl(data_config, data_loader)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def create_rlds_data_loader(
|
| 393 |
+
data_config: _config.DataConfig,
|
| 394 |
+
action_horizon: int,
|
| 395 |
+
batch_size: int,
|
| 396 |
+
*,
|
| 397 |
+
sharding: jax.sharding.Sharding | None = None,
|
| 398 |
+
skip_norm_stats: bool = False,
|
| 399 |
+
shuffle: bool = False,
|
| 400 |
+
num_batches: int | None = None,
|
| 401 |
+
framework: str = "jax",
|
| 402 |
+
) -> DataLoader[tuple[_model.Observation, _model.Actions]]:
|
| 403 |
+
"""Create an RLDS data loader for training.
|
| 404 |
+
|
| 405 |
+
Note: This data loader requires some extra dependencies -- see examples/droid/README_train.md
|
| 406 |
+
|
| 407 |
+
Args:
|
| 408 |
+
data_config: The data configuration.
|
| 409 |
+
action_horizon: The action horizon.
|
| 410 |
+
batch_size: The batch size.
|
| 411 |
+
sharding: The sharding to use for the data loader. If None, the data loader will
|
| 412 |
+
use a single device sharding.
|
| 413 |
+
skip_norm_stats: Whether to skip data normalization.
|
| 414 |
+
shuffle: Whether to shuffle the data.
|
| 415 |
+
num_batches: Determines the number of batches to return. If the number exceeds the
|
| 416 |
+
number of batches in the dataset, the data loader will loop over the dataset.
|
| 417 |
+
If not provided, will iterate over the dataset indefinitely.
|
| 418 |
+
"""
|
| 419 |
+
if framework == "pytorch":
|
| 420 |
+
raise NotImplementedError("PyTorch RLDS data loader is not supported yet")
|
| 421 |
+
dataset = create_rlds_dataset(data_config, action_horizon, batch_size, shuffle=shuffle)
|
| 422 |
+
dataset = transform_iterable_dataset(dataset, data_config, skip_norm_stats=skip_norm_stats, is_batched=True)
|
| 423 |
+
|
| 424 |
+
data_loader = RLDSDataLoader(
|
| 425 |
+
dataset,
|
| 426 |
+
sharding=sharding,
|
| 427 |
+
num_batches=num_batches,
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
return DataLoaderImpl(data_config, data_loader)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
class TorchDataLoader:
|
| 434 |
+
"""Torch data loader implementation."""
|
| 435 |
+
|
| 436 |
+
def __init__(
|
| 437 |
+
self,
|
| 438 |
+
dataset,
|
| 439 |
+
local_batch_size: int,
|
| 440 |
+
*,
|
| 441 |
+
sharding: jax.sharding.Sharding | None = None,
|
| 442 |
+
shuffle: bool = False,
|
| 443 |
+
sampler: torch.utils.data.Sampler | None = None,
|
| 444 |
+
num_batches: int | None = None,
|
| 445 |
+
num_workers: int = 0,
|
| 446 |
+
seed: int = 0,
|
| 447 |
+
framework: str = "jax",
|
| 448 |
+
batch_transform=None,
|
| 449 |
+
):
|
| 450 |
+
"""Create a PyTorch data loader.
|
| 451 |
+
|
| 452 |
+
Args:
|
| 453 |
+
dataset: The dataset to load.
|
| 454 |
+
local_batch_size: The local batch size for each process.
|
| 455 |
+
sharding: The sharding to use for the data loader.
|
| 456 |
+
shuffle: Whether to shuffle the data.
|
| 457 |
+
num_batches: If provided, determines the number of returned batches. If the
|
| 458 |
+
number is larger than the number of batches in the dataset, the data loader
|
| 459 |
+
will loop over the dataset. If not provided, will iterate over the dataset
|
| 460 |
+
indefinitely.
|
| 461 |
+
num_workers: The number of worker processes to use. If zero, the data loader will
|
| 462 |
+
execute in the main process.
|
| 463 |
+
seed: The seed to use for shuffling the data.
|
| 464 |
+
"""
|
| 465 |
+
if jax.process_count() > 1:
|
| 466 |
+
raise NotImplementedError("Data loading with multiple processes is not supported.")
|
| 467 |
+
|
| 468 |
+
if len(dataset) < local_batch_size:
|
| 469 |
+
raise ValueError(f"Local batch size ({local_batch_size}) is larger than the dataset size ({len(dataset)}).")
|
| 470 |
+
|
| 471 |
+
# Store sharding - None for PyTorch, JAX sharding for JAX
|
| 472 |
+
self._sharding = sharding
|
| 473 |
+
if sharding is None and framework == "jax":
|
| 474 |
+
# Use data parallel sharding by default for JAX only.
|
| 475 |
+
self._sharding = jax.sharding.NamedSharding(
|
| 476 |
+
jax.sharding.Mesh(jax.devices(), ("B",)),
|
| 477 |
+
jax.sharding.PartitionSpec("B"),
|
| 478 |
+
)
|
| 479 |
+
self._num_batches = num_batches
|
| 480 |
+
# optional per-batch hook applied to the (numpy) batch before sharding — used for inline
|
| 481 |
+
# DA3 feature extraction (runs the frozen GIANT on the batch's raw frames on GPU).
|
| 482 |
+
self._batch_transform = batch_transform
|
| 483 |
+
|
| 484 |
+
mp_context = None
|
| 485 |
+
if num_workers > 0:
|
| 486 |
+
mp_context = multiprocessing.get_context("spawn")
|
| 487 |
+
|
| 488 |
+
generator = torch.Generator()
|
| 489 |
+
generator.manual_seed(seed)
|
| 490 |
+
data_loader_kwargs = {}
|
| 491 |
+
if num_workers > 0:
|
| 492 |
+
prefetch_factor = int(os.environ.get("B1K_PREFETCH_FACTOR", "2"))
|
| 493 |
+
if prefetch_factor > 0:
|
| 494 |
+
data_loader_kwargs["prefetch_factor"] = prefetch_factor
|
| 495 |
+
if _TORCH_COLLATE:
|
| 496 |
+
# Only meaningful for torch tensors; pins in the loader's pin thread so the
|
| 497 |
+
# extractor's H2D can be a true async DMA (non_blocking) instead of a pageable copy.
|
| 498 |
+
data_loader_kwargs["pin_memory"] = True
|
| 499 |
+
self._data_loader = torch.utils.data.DataLoader(
|
| 500 |
+
typing.cast(torch.utils.data.Dataset, dataset),
|
| 501 |
+
batch_size=local_batch_size,
|
| 502 |
+
shuffle=(sampler is None and shuffle), # Don't shuffle if using sampler
|
| 503 |
+
sampler=sampler,
|
| 504 |
+
num_workers=num_workers,
|
| 505 |
+
multiprocessing_context=mp_context,
|
| 506 |
+
persistent_workers=num_workers > 0,
|
| 507 |
+
collate_fn=_collate_fn,
|
| 508 |
+
worker_init_fn=_worker_init_fn,
|
| 509 |
+
drop_last=True,
|
| 510 |
+
generator=generator,
|
| 511 |
+
**data_loader_kwargs,
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
@property
|
| 515 |
+
def torch_loader(self) -> torch.utils.data.DataLoader:
|
| 516 |
+
return self._data_loader
|
| 517 |
+
|
| 518 |
+
def _transformed_batches(self):
|
| 519 |
+
"""Yields batch_transform-ed batches, looping over the dataset indefinitely.
|
| 520 |
+
|
| 521 |
+
When a batch_transform is set (inline DA3 GPU extraction), it runs ONE BATCH AHEAD in a
|
| 522 |
+
background thread, so the frozen DA3 forward for batch N+1 overlaps the train step for
|
| 523 |
+
batch N instead of serializing with it (torch GPU kernels release the GIL).
|
| 524 |
+
"""
|
| 525 |
+
def epochs():
|
| 526 |
+
epoch = 0
|
| 527 |
+
sampler = getattr(self._data_loader, "sampler", None)
|
| 528 |
+
while True:
|
| 529 |
+
if hasattr(sampler, "set_epoch"):
|
| 530 |
+
sampler.set_epoch(epoch)
|
| 531 |
+
yield from self._data_loader
|
| 532 |
+
epoch += 1
|
| 533 |
+
|
| 534 |
+
if self._batch_transform is None:
|
| 535 |
+
yield from epochs()
|
| 536 |
+
return
|
| 537 |
+
|
| 538 |
+
# Depth = how many extracted batches can be buffered ahead of training. Default 2 (unchanged).
|
| 539 |
+
# On a dedicated-extraction-GPU setup (extract on spare GPUs, train on others) bump this via
|
| 540 |
+
# B1K_EXTRACT_QUEUE=3-4 so extraction runs further ahead and smooths over per-batch variance.
|
| 541 |
+
q: queue.Queue = queue.Queue(maxsize=int(os.environ.get("B1K_EXTRACT_QUEUE", "2")))
|
| 542 |
+
|
| 543 |
+
def producer():
|
| 544 |
+
try:
|
| 545 |
+
for raw in epochs():
|
| 546 |
+
b = self._batch_transform(raw)
|
| 547 |
+
# Stage the REMAINING host fields (base RGB / actions / state) onto the training
|
| 548 |
+
# GPUs here in the producer instead of on the main thread at consume time. Same
|
| 549 |
+
# make_array, just moved off the train loop's critical path so the host->GPU copy
|
| 550 |
+
# overlaps the in-flight train step. (DA3 fields are already jax.Array via DLPack.)
|
| 551 |
+
if self._sharding is not None:
|
| 552 |
+
b = jax.tree.map(
|
| 553 |
+
lambda x: x if isinstance(x, jax.Array)
|
| 554 |
+
else jax.make_array_from_process_local_data(self._sharding, _host_np(x)), b)
|
| 555 |
+
q.put(b)
|
| 556 |
+
except BaseException as e: # noqa: BLE001 — propagate any failure to the consumer
|
| 557 |
+
q.put(e)
|
| 558 |
+
|
| 559 |
+
threading.Thread(target=producer, daemon=True, name="batch-transform-prefetch").start()
|
| 560 |
+
while True:
|
| 561 |
+
item = q.get()
|
| 562 |
+
if isinstance(item, BaseException):
|
| 563 |
+
raise item
|
| 564 |
+
yield item
|
| 565 |
+
|
| 566 |
+
def __iter__(self):
|
| 567 |
+
num_items = 0
|
| 568 |
+
batches = self._transformed_batches()
|
| 569 |
+
while True:
|
| 570 |
+
# Check BEFORE pulling: the generator prefetches, and pulling past num_batches would
|
| 571 |
+
# trigger (and discard) a whole extra batch across the epoch boundary.
|
| 572 |
+
if self._num_batches is not None and num_items >= self._num_batches:
|
| 573 |
+
return
|
| 574 |
+
batch = next(batches)
|
| 575 |
+
num_items += 1
|
| 576 |
+
# For JAX, convert to sharded arrays; for PyTorch, return torch tensors.
|
| 577 |
+
# Fields already placed as jax.Array (DLPack GPU->GPU handoff) pass through untouched.
|
| 578 |
+
if self._sharding is not None:
|
| 579 |
+
yield jax.tree.map(
|
| 580 |
+
lambda x: x if isinstance(x, jax.Array)
|
| 581 |
+
else jax.make_array_from_process_local_data(self._sharding, _host_np(x)), batch)
|
| 582 |
+
else:
|
| 583 |
+
yield jax.tree.map(torch.as_tensor, batch)
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
_TORCH_COLLATE = os.environ.get("B1K_TORCH_COLLATE") == "1"
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def _host_np(x):
|
| 590 |
+
"""torch CPU tensor -> numpy (zero-copy view); pass anything else through unchanged."""
|
| 591 |
+
return x.numpy() if isinstance(x, torch.Tensor) else x
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
def _collate_fn(items):
|
| 595 |
+
"""Collate the batch elements into batched arrays.
|
| 596 |
+
|
| 597 |
+
With B1K_TORCH_COLLATE=1 the batch is built as torch tensors instead of numpy. The DataLoader
|
| 598 |
+
hands torch tensors worker->main through SHARED MEMORY (fd passing) rather than pickling and
|
| 599 |
+
copying them, which is a real saving at ~131MB/batch, and it makes pin_memory (hence async
|
| 600 |
+
non_blocking H2D) possible. Values are bit-identical either way.
|
| 601 |
+
"""
|
| 602 |
+
if _TORCH_COLLATE:
|
| 603 |
+
def _stack(*xs):
|
| 604 |
+
a = [np.asarray(x) for x in xs]
|
| 605 |
+
if a[0].dtype.kind in "biufc": # numeric -> torch tensor (shared-memory IPC + pinnable)
|
| 606 |
+
return torch.stack([torch.as_tensor(v) for v in a], 0)
|
| 607 |
+
return np.stack(a, axis=0) # strings/objects have no torch equivalent; keep numpy
|
| 608 |
+
return jax.tree.map(_stack, *items)
|
| 609 |
+
# Make sure to convert to numpy arrays before stacking since some of the incoming elements
|
| 610 |
+
# may be JAX arrays.
|
| 611 |
+
return jax.tree.map(lambda *xs: np.stack([np.asarray(x) for x in xs], axis=0), *items)
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def _worker_init_fn(worker_id: int) -> None:
|
| 615 |
+
"""Tell JAX inside the worker process not to preallocate the GPU memory."""
|
| 616 |
+
# NOTE: This is called after jax is imported inside the worker process. This
|
| 617 |
+
# means that this approach will not work for selecting the backend.
|
| 618 |
+
os.environ["XLA_PYTHON_CLIENT_PREALLOCATE"] = "false"
|
| 619 |
+
os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform"
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
class RLDSDataLoader:
|
| 623 |
+
"""Shallow wrapper around the DROID data loader to make it compatible with openpi.
|
| 624 |
+
|
| 625 |
+
All batching already happens in the DROID dataset, so we don't need to do anything here.
|
| 626 |
+
"""
|
| 627 |
+
|
| 628 |
+
def __init__(
|
| 629 |
+
self,
|
| 630 |
+
dataset: DroidRldsDataset,
|
| 631 |
+
*,
|
| 632 |
+
sharding: jax.sharding.Sharding | None = None,
|
| 633 |
+
num_batches: int | None = None,
|
| 634 |
+
):
|
| 635 |
+
self._dataset = dataset
|
| 636 |
+
self._num_batches = num_batches
|
| 637 |
+
|
| 638 |
+
if jax.process_count() > 1:
|
| 639 |
+
raise NotImplementedError("Data loading with multiple processes is not supported.")
|
| 640 |
+
|
| 641 |
+
if sharding is None:
|
| 642 |
+
# Use data parallel sharding by default.
|
| 643 |
+
sharding = jax.sharding.NamedSharding(
|
| 644 |
+
jax.sharding.Mesh(jax.devices(), ("B",)),
|
| 645 |
+
jax.sharding.PartitionSpec("B"),
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
self._sharding = sharding
|
| 649 |
+
self._num_batches = num_batches
|
| 650 |
+
|
| 651 |
+
def __iter__(self):
|
| 652 |
+
num_items = 0
|
| 653 |
+
while True:
|
| 654 |
+
data_iter = iter(self._dataset)
|
| 655 |
+
while True:
|
| 656 |
+
if self._num_batches is not None and num_items >= self._num_batches:
|
| 657 |
+
return
|
| 658 |
+
try:
|
| 659 |
+
batch = next(data_iter)
|
| 660 |
+
except StopIteration:
|
| 661 |
+
break # We've exhausted the dataset. Create a new iterator and start over.
|
| 662 |
+
num_items += 1
|
| 663 |
+
yield jax.tree.map(lambda x: jax.make_array_from_process_local_data(self._sharding, x), batch)
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
class DataLoaderImpl(DataLoader):
|
| 667 |
+
def __init__(self, data_config: _config.DataConfig, data_loader: TorchDataLoader | RLDSDataLoader):
|
| 668 |
+
self._data_config = data_config
|
| 669 |
+
self._data_loader = data_loader
|
| 670 |
+
|
| 671 |
+
def data_config(self) -> _config.DataConfig:
|
| 672 |
+
return self._data_config
|
| 673 |
+
|
| 674 |
+
def __iter__(self):
|
| 675 |
+
for batch in self._data_loader:
|
| 676 |
+
yield _model.Observation.from_dict(batch), batch["actions"]
|
pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/gemma.py
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Big Vision Authors.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Gemma adaptation for Pi, taken from big_vision.
|
| 16 |
+
|
| 17 |
+
We follow this einsum axis naming convention:
|
| 18 |
+
B: batch
|
| 19 |
+
T: query length
|
| 20 |
+
S: k/v length
|
| 21 |
+
N: num query heads
|
| 22 |
+
K: num k/v heads
|
| 23 |
+
G: num query heads per k/v head
|
| 24 |
+
H: head dim
|
| 25 |
+
D: d_model ("features")
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from collections.abc import Sequence
|
| 29 |
+
import dataclasses
|
| 30 |
+
from typing import Literal, TypeAlias
|
| 31 |
+
|
| 32 |
+
import os
|
| 33 |
+
import einops
|
| 34 |
+
import flax.linen as nn
|
| 35 |
+
import jax
|
| 36 |
+
import jax.numpy as jnp
|
| 37 |
+
|
| 38 |
+
import openpi.models.lora as lora
|
| 39 |
+
import openpi.shared.array_typing as at
|
| 40 |
+
import openpi.training.sharding as sharding
|
| 41 |
+
|
| 42 |
+
PALIGEMMA_VOCAB_SIZE = 257_152
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclasses.dataclass
|
| 46 |
+
class Config:
|
| 47 |
+
width: int
|
| 48 |
+
depth: int
|
| 49 |
+
mlp_dim: int
|
| 50 |
+
num_heads: int
|
| 51 |
+
num_kv_heads: int
|
| 52 |
+
head_dim: int
|
| 53 |
+
lora_configs: dict[str, lora.LoRAConfig] = dataclasses.field(default_factory=dict)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
Variant = Literal["dummy", "gemma_300m", "gemma_300m_lora", "gemma_2b", "gemma_2b_lora"]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def get_config(variant: Variant) -> Config:
|
| 60 |
+
"""Returns config for specified gemma variant."""
|
| 61 |
+
if variant == "dummy":
|
| 62 |
+
return Config(
|
| 63 |
+
width=64,
|
| 64 |
+
depth=4,
|
| 65 |
+
mlp_dim=128,
|
| 66 |
+
num_heads=8,
|
| 67 |
+
num_kv_heads=1,
|
| 68 |
+
head_dim=16,
|
| 69 |
+
)
|
| 70 |
+
if variant == "gemma_300m":
|
| 71 |
+
# 311M params
|
| 72 |
+
return Config(
|
| 73 |
+
width=1024,
|
| 74 |
+
depth=18,
|
| 75 |
+
mlp_dim=4096,
|
| 76 |
+
num_heads=8,
|
| 77 |
+
num_kv_heads=1,
|
| 78 |
+
head_dim=256,
|
| 79 |
+
)
|
| 80 |
+
if variant == "gemma_2b":
|
| 81 |
+
return Config(
|
| 82 |
+
width=2048,
|
| 83 |
+
depth=18,
|
| 84 |
+
mlp_dim=16_384,
|
| 85 |
+
num_heads=8,
|
| 86 |
+
num_kv_heads=1,
|
| 87 |
+
head_dim=256,
|
| 88 |
+
)
|
| 89 |
+
if variant == "gemma_2b_lora":
|
| 90 |
+
return Config(
|
| 91 |
+
width=2048,
|
| 92 |
+
depth=18,
|
| 93 |
+
mlp_dim=16_384,
|
| 94 |
+
num_heads=8,
|
| 95 |
+
num_kv_heads=1,
|
| 96 |
+
head_dim=256,
|
| 97 |
+
lora_configs={"attn": lora.LoRAConfig(rank=16, alpha=16.0), "ffn": lora.LoRAConfig(rank=16, alpha=16.0)},
|
| 98 |
+
)
|
| 99 |
+
if variant == "gemma_2b_lora_32":
|
| 100 |
+
return Config(
|
| 101 |
+
width=2048,
|
| 102 |
+
depth=18,
|
| 103 |
+
mlp_dim=16_384,
|
| 104 |
+
num_heads=8,
|
| 105 |
+
num_kv_heads=1,
|
| 106 |
+
head_dim=256,
|
| 107 |
+
lora_configs={"attn": lora.LoRAConfig(rank=32, alpha=32.0), "ffn": lora.LoRAConfig(rank=32, alpha=32.0)},
|
| 108 |
+
)
|
| 109 |
+
if variant == "gemma_300m_lora":
|
| 110 |
+
# 311M params
|
| 111 |
+
return Config(
|
| 112 |
+
width=1024,
|
| 113 |
+
depth=18,
|
| 114 |
+
mlp_dim=4096,
|
| 115 |
+
num_heads=8,
|
| 116 |
+
num_kv_heads=1,
|
| 117 |
+
head_dim=256,
|
| 118 |
+
lora_configs={"attn": lora.LoRAConfig(rank=32, alpha=32.0), "ffn": lora.LoRAConfig(rank=32, alpha=32.0)},
|
| 119 |
+
)
|
| 120 |
+
raise ValueError(f"Unknown variant: {variant}")
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@at.typecheck
|
| 124 |
+
class RMSNorm(nn.Module):
|
| 125 |
+
@nn.compact
|
| 126 |
+
def __call__(self, x, cond):
|
| 127 |
+
dtype = x.dtype # original dtype, could be half-precision
|
| 128 |
+
var = jnp.mean(jnp.square(x.astype(jnp.float32)), axis=-1, keepdims=True) # compute variance in float32
|
| 129 |
+
normed_inputs = jnp.asarray(x * jnp.reciprocal(jnp.sqrt(var + 1e-06))) # compute normalization in float32
|
| 130 |
+
if cond is None:
|
| 131 |
+
# regular RMSNorm
|
| 132 |
+
scale = self.param("scale", nn.initializers.zeros_init(), (x.shape[-1]))
|
| 133 |
+
normed_inputs = normed_inputs * (
|
| 134 |
+
1 + scale
|
| 135 |
+
) # scale by learned parameter in float32 (matches Flax implementation)
|
| 136 |
+
return normed_inputs.astype(dtype), None # return in original dtype
|
| 137 |
+
|
| 138 |
+
# adaptive RMSNorm
|
| 139 |
+
modulation = nn.Dense(x.shape[-1] * 3, kernel_init=nn.initializers.zeros, dtype=dtype)(cond)
|
| 140 |
+
scale, shift, gate = jnp.split(modulation[:, None, :], 3, axis=-1)
|
| 141 |
+
normed_inputs = normed_inputs * (1 + scale) + shift # scale and shift in float32
|
| 142 |
+
return normed_inputs.astype(dtype), gate
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@at.typecheck
|
| 146 |
+
class Embedder(nn.Module):
|
| 147 |
+
"""Embedder module."""
|
| 148 |
+
|
| 149 |
+
vocab_size: int
|
| 150 |
+
embed_dim: int
|
| 151 |
+
|
| 152 |
+
def setup(self):
|
| 153 |
+
self.input_embedding_table = self.param(
|
| 154 |
+
"input_embedding",
|
| 155 |
+
nn.initializers.normal(),
|
| 156 |
+
(self.vocab_size, self.embed_dim),
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
def encode(self, x):
|
| 160 |
+
x = self.input_embedding_table[(x,)]
|
| 161 |
+
x *= jnp.sqrt(self.embed_dim).astype(x.dtype)
|
| 162 |
+
return x
|
| 163 |
+
|
| 164 |
+
def decode(self, x):
|
| 165 |
+
return jnp.dot(x, self.input_embedding_table.T)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
@at.typecheck
|
| 169 |
+
class Attention(nn.Module):
|
| 170 |
+
"""Attention module."""
|
| 171 |
+
|
| 172 |
+
configs: Sequence[Config]
|
| 173 |
+
|
| 174 |
+
@nn.compact
|
| 175 |
+
def __call__(self, xs, positions, attn_mask, kv_cache):
|
| 176 |
+
# all experts must share the same head dim, num heads, and num kv heads for self-attention to work
|
| 177 |
+
assert all(config.head_dim == self.configs[0].head_dim for config in self.configs)
|
| 178 |
+
assert all(config.num_heads == self.configs[0].num_heads for config in self.configs)
|
| 179 |
+
assert all(config.num_kv_heads == self.configs[0].num_kv_heads for config in self.configs)
|
| 180 |
+
|
| 181 |
+
dtype = next(x.dtype for x in xs if x is not None) # original dtype, could be half-precision
|
| 182 |
+
|
| 183 |
+
qkvs = []
|
| 184 |
+
for i, (x, config) in enumerate(zip(xs, self.configs, strict=True)):
|
| 185 |
+
if x is None:
|
| 186 |
+
continue
|
| 187 |
+
if config.num_kv_heads == config.num_heads:
|
| 188 |
+
qkv_einsum = lora.Einsum(
|
| 189 |
+
shape=(3, config.num_heads, config.width, config.head_dim),
|
| 190 |
+
name=_name("qkv_einsum", i),
|
| 191 |
+
init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0, 1)),
|
| 192 |
+
lora_config=config.lora_configs.get("attn"),
|
| 193 |
+
)
|
| 194 |
+
qkvs.append(qkv_einsum("BSD,3KDH->3BSKH", x))
|
| 195 |
+
else:
|
| 196 |
+
q_einsum = lora.Einsum(
|
| 197 |
+
shape=(config.num_heads, config.width, config.head_dim),
|
| 198 |
+
name=_name("q_einsum", i),
|
| 199 |
+
init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0,)),
|
| 200 |
+
lora_config=config.lora_configs.get("attn"),
|
| 201 |
+
)
|
| 202 |
+
q = q_einsum("BTD,NDH->BTNH", x)
|
| 203 |
+
kv_einsum = lora.Einsum(
|
| 204 |
+
shape=(2, config.num_kv_heads, config.width, config.head_dim),
|
| 205 |
+
name=_name("kv_einsum", i),
|
| 206 |
+
init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0, 1)),
|
| 207 |
+
lora_config=config.lora_configs.get("attn"),
|
| 208 |
+
)
|
| 209 |
+
k, v = kv_einsum("BSD,2KDH->2BSKH", x)
|
| 210 |
+
qkvs.append((q, k, v))
|
| 211 |
+
|
| 212 |
+
q, k, v = (jnp.concatenate(y, axis=1) for y in zip(*qkvs, strict=True))
|
| 213 |
+
|
| 214 |
+
q = _apply_rope(q, positions=positions)
|
| 215 |
+
q *= self.configs[0].head_dim ** -0.5
|
| 216 |
+
|
| 217 |
+
k = _apply_rope(k, positions=positions)
|
| 218 |
+
|
| 219 |
+
# should still be half-precision here (if input was half-precision)
|
| 220 |
+
assert q.dtype == k.dtype == v.dtype == dtype
|
| 221 |
+
|
| 222 |
+
if kv_cache is not None:
|
| 223 |
+
cache_k, cache_v = kv_cache
|
| 224 |
+
k = jnp.concatenate([cache_k, k], axis=1)
|
| 225 |
+
v = jnp.concatenate([cache_v, v], axis=1)
|
| 226 |
+
|
| 227 |
+
q = einops.rearrange(q, "B T (K G) H -> B T K G H", K=self.configs[0].num_kv_heads)
|
| 228 |
+
logits = jnp.einsum("BTKGH,BSKH->BKGTS", q, k, preferred_element_type=jnp.float32)
|
| 229 |
+
|
| 230 |
+
if attn_mask.shape != (q.shape[0], 1, q.shape[1], k.shape[1]):
|
| 231 |
+
raise ValueError(
|
| 232 |
+
f"Attention mask with shape {attn_mask.shape} but shapes for q and k are: {q.shape} and {k.shape}"
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# big_neg = jnp.finfo(logits.dtype).min
|
| 236 |
+
big_neg = -2.3819763e38 # See gemma/modules.py
|
| 237 |
+
masked_logits = jnp.where(attn_mask[:, :, None, :, :], logits, big_neg)
|
| 238 |
+
|
| 239 |
+
probs = jax.nn.softmax(masked_logits, axis=-1).astype(dtype)
|
| 240 |
+
|
| 241 |
+
encoded = jnp.einsum("BKGTS,BSKH->BTKGH", probs, v)
|
| 242 |
+
encoded = einops.rearrange(encoded, "B T K G H -> B T (K G) H")
|
| 243 |
+
|
| 244 |
+
out = []
|
| 245 |
+
start = 0
|
| 246 |
+
for i, (x, config) in enumerate(zip(xs, self.configs, strict=True)):
|
| 247 |
+
if x is not None:
|
| 248 |
+
end = start + x.shape[1]
|
| 249 |
+
out_einsum = lora.Einsum(
|
| 250 |
+
shape=(config.num_heads, config.head_dim, config.width),
|
| 251 |
+
name=_name("attn_vec_einsum", i),
|
| 252 |
+
init_fn=nn.initializers.lecun_normal(in_axis=(-3, -2), out_axis=-1),
|
| 253 |
+
lora_config=config.lora_configs.get("attn"),
|
| 254 |
+
)
|
| 255 |
+
out.append(out_einsum("BTNH,NHD->BTD", encoded[:, start:end]))
|
| 256 |
+
start = end
|
| 257 |
+
else:
|
| 258 |
+
out.append(None)
|
| 259 |
+
|
| 260 |
+
return out, (k, v)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
@at.typecheck
|
| 264 |
+
class FeedForward(nn.Module):
|
| 265 |
+
"""Feed forward module."""
|
| 266 |
+
|
| 267 |
+
features: int
|
| 268 |
+
hidden_dim: int
|
| 269 |
+
|
| 270 |
+
@nn.compact
|
| 271 |
+
def __call__(self, x):
|
| 272 |
+
dtype = x.dtype # original dtype, could be half-precision
|
| 273 |
+
w_gating = self.param(
|
| 274 |
+
"gating_einsum",
|
| 275 |
+
nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0,)),
|
| 276 |
+
(2, self.features, self.hidden_dim),
|
| 277 |
+
).astype(dtype)
|
| 278 |
+
ff_gate = jnp.dot(x, w_gating[0])
|
| 279 |
+
gate_value = nn.gelu(ff_gate)
|
| 280 |
+
|
| 281 |
+
ff1 = jnp.dot(x, w_gating[1])
|
| 282 |
+
activations = gate_value * ff1
|
| 283 |
+
|
| 284 |
+
w_linear = self.param(
|
| 285 |
+
"linear",
|
| 286 |
+
nn.initializers.lecun_normal(in_axis=-2, out_axis=-1),
|
| 287 |
+
(self.hidden_dim, self.features),
|
| 288 |
+
).astype(dtype)
|
| 289 |
+
outputs = jnp.dot(activations, w_linear)
|
| 290 |
+
assert outputs.dtype == dtype
|
| 291 |
+
return outputs
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class _LinenCrossAttn(nn.Module):
|
| 295 |
+
"""Pre-LN cross-attention. out_proj is zero-init by default (identity at step 0); out_init_std>0
|
| 296 |
+
gives it a small random init so q/k/v receive gradient from step 0 (with zero-init their grads
|
| 297 |
+
are zero until out_proj grows off zero, LoRA-style bootstrap)."""
|
| 298 |
+
|
| 299 |
+
d_model: int
|
| 300 |
+
num_heads: int
|
| 301 |
+
out_init_std: float = 0.0
|
| 302 |
+
logit_gain: bool = False # learnable per-head gain on the attention logits (v2 fix)
|
| 303 |
+
logit_gain_init: float = 1.0 # initial gain value; >1 = sharper attention at init (bigger softmax
|
| 304 |
+
# jacobian => stronger, more consistent Q/K gradients; uniform attention scales them by ~1/K)
|
| 305 |
+
|
| 306 |
+
@nn.compact
|
| 307 |
+
def __call__(self, q_hidden, kv):
|
| 308 |
+
hd = self.d_model // self.num_heads
|
| 309 |
+
q = nn.LayerNorm(epsilon=1e-5, name="q_norm")(q_hidden)
|
| 310 |
+
kvn = nn.LayerNorm(epsilon=1e-5, name="kv_norm")(kv)
|
| 311 |
+
Q = einops.rearrange(nn.Dense(self.d_model, name="q_proj")(q), "b l (h d) -> b h l d", h=self.num_heads)
|
| 312 |
+
K = einops.rearrange(nn.Dense(self.d_model, name="k_proj")(kvn), "b l (h d) -> b h l d", h=self.num_heads)
|
| 313 |
+
V = einops.rearrange(nn.Dense(self.d_model, name="v_proj")(kvn), "b l (h d) -> b h l d", h=self.num_heads)
|
| 314 |
+
logits = jnp.einsum("bhqd,bhkd->bhqk", Q, K) * (hd**-0.5)
|
| 315 |
+
if self.logit_gain:
|
| 316 |
+
# exp-parameterized per-head gain, init logit_gain_init (v2 uses 32); multiplies dL/dQ,K so
|
| 317 |
+
# the attention pattern can actually train (v1 measured Q/K ~1000x slower than V/out).
|
| 318 |
+
log_gain = self.param(
|
| 319 |
+
"log_gain", nn.initializers.constant(jnp.log(self.logit_gain_init)), (self.num_heads,)
|
| 320 |
+
)
|
| 321 |
+
logits = logits * jnp.exp(log_gain)[None, :, None, None].astype(logits.dtype)
|
| 322 |
+
probs = jax.nn.softmax(logits.astype(jnp.float32), axis=-1).astype(Q.dtype)
|
| 323 |
+
ctx = einops.rearrange(jnp.einsum("bhqk,bhkd->bhqd", probs, V), "b h q d -> b q (h d)")
|
| 324 |
+
out_init = nn.initializers.normal(stddev=self.out_init_std) if self.out_init_std > 0 else nn.initializers.zeros
|
| 325 |
+
return nn.Dense(
|
| 326 |
+
self.d_model, name="out_proj", kernel_init=out_init, bias_init=nn.initializers.zeros
|
| 327 |
+
)(ctx)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class SpatialActionInjection(nn.Module):
|
| 331 |
+
"""X-VLA Method-B injection (main x-attn -> left/right branches -> merge), scaled residual.
|
| 332 |
+
|
| 333 |
+
init_std=0 (default): output projections zero-init => returns 0 (exact identity at step 0, pi05_base
|
| 334 |
+
reproduced bit-exactly). init_std>0: small normal init on the xattn out_projs and merge_proj => a
|
| 335 |
+
small nonzero spatial delta from step 0 and immediate gradient flow to all injection weights.
|
| 336 |
+
Returns the DELTA to add to the action-token stream (external per-layer gate selects it).
|
| 337 |
+
"""
|
| 338 |
+
|
| 339 |
+
d_model: int
|
| 340 |
+
num_heads: int = 8
|
| 341 |
+
spatial_scale: float = 2.0
|
| 342 |
+
init_std: float = 0.0
|
| 343 |
+
logit_gain: bool = False
|
| 344 |
+
logit_gain_init: float = 1.0
|
| 345 |
+
|
| 346 |
+
@nn.compact
|
| 347 |
+
def __call__(self, h0, banks):
|
| 348 |
+
s = self.spatial_scale
|
| 349 |
+
std = self.init_std
|
| 350 |
+
g = self.logit_gain
|
| 351 |
+
gi = self.logit_gain_init
|
| 352 |
+
h1 = h0 + s * _LinenCrossAttn(self.d_model, self.num_heads, std, g, gi, name="main_xattn")(h0, banks["main"])
|
| 353 |
+
hL = nn.Dense(self.d_model, name="left_branch")(h1)
|
| 354 |
+
hR = nn.Dense(self.d_model, name="right_branch")(h1)
|
| 355 |
+
hL = hL + s * _LinenCrossAttn(self.d_model, self.num_heads, std, g, gi, name="left_xattn")(hL, banks["left"])
|
| 356 |
+
hR = hR + s * _LinenCrossAttn(self.d_model, self.num_heads, std, g, gi, name="right_xattn")(hR, banks["right"])
|
| 357 |
+
merge_init = nn.initializers.normal(stddev=std) if std > 0 else nn.initializers.zeros
|
| 358 |
+
merge = nn.Dense(
|
| 359 |
+
self.d_model, name="merge_proj", kernel_init=merge_init, bias_init=nn.initializers.zeros
|
| 360 |
+
)(jnp.concatenate([hL, hR], axis=-1))
|
| 361 |
+
out = h1 + s * merge
|
| 362 |
+
return out - h0
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
@at.typecheck
|
| 366 |
+
class Block(nn.Module):
|
| 367 |
+
"""Transformer block."""
|
| 368 |
+
|
| 369 |
+
configs: tuple[Config, ...]
|
| 370 |
+
|
| 371 |
+
dropout: float = 0.0
|
| 372 |
+
dropout_bdims: tuple[int, ...] = ()
|
| 373 |
+
spatial_scale: float = 2.0
|
| 374 |
+
spatial_init_std: float = 0.0
|
| 375 |
+
spatial_logit_gain: bool = False
|
| 376 |
+
spatial_logit_gain_init: float = 1.0
|
| 377 |
+
|
| 378 |
+
@nn.compact
|
| 379 |
+
def __call__( # noqa: FBT002
|
| 380 |
+
self, xs, kv_cache, positions, attn_mask, adarms_cond, deterministic=True, banks=None, layer_gate=None
|
| 381 |
+
):
|
| 382 |
+
xs = sharding.activation_sharding_constraint(xs)
|
| 383 |
+
drop = nn.Dropout(self.dropout, self.dropout_bdims) if self.dropout else lambda x, _: x
|
| 384 |
+
|
| 385 |
+
attn = Attention(configs=self.configs, name="attn")
|
| 386 |
+
|
| 387 |
+
pre_attn = []
|
| 388 |
+
gates = []
|
| 389 |
+
for i, x in enumerate(xs):
|
| 390 |
+
if x is not None:
|
| 391 |
+
x, gate = RMSNorm(name=_name("pre_attention_norm", i))(x, adarms_cond[i]) # noqa: PLW2901
|
| 392 |
+
pre_attn.append(x)
|
| 393 |
+
gates.append(gate if x is not None else None)
|
| 394 |
+
|
| 395 |
+
pre_attn = sharding.activation_sharding_constraint(pre_attn)
|
| 396 |
+
post_attn, kv_cache = attn(pre_attn, positions, attn_mask, kv_cache)
|
| 397 |
+
post_attn = jax.tree.map(lambda x: drop(x, deterministic), post_attn)
|
| 398 |
+
post_attn = sharding.activation_sharding_constraint(post_attn)
|
| 399 |
+
xs = [_gated_residual(x, y, gate) for x, y, gate in zip(xs, post_attn, gates, strict=True)]
|
| 400 |
+
xs = sharding.activation_sharding_constraint(xs)
|
| 401 |
+
|
| 402 |
+
out = []
|
| 403 |
+
gates = []
|
| 404 |
+
for i, (x, config) in enumerate(zip(xs, self.configs, strict=True)):
|
| 405 |
+
if x is not None:
|
| 406 |
+
x, gate = RMSNorm(name=_name("pre_ffw_norm", i))(x, adarms_cond[i]) # noqa: PLW2901
|
| 407 |
+
x = lora.FeedForward( # noqa: PLW2901
|
| 408 |
+
features=config.width,
|
| 409 |
+
hidden_dim=config.mlp_dim,
|
| 410 |
+
name=_name("mlp", i),
|
| 411 |
+
lora_config=config.lora_configs.get("ffn"),
|
| 412 |
+
)(x)
|
| 413 |
+
out.append(x)
|
| 414 |
+
gates.append(gate if x is not None else None)
|
| 415 |
+
|
| 416 |
+
out = sharding.activation_sharding_constraint(out)
|
| 417 |
+
out = jax.tree.map(lambda x: drop(x, deterministic), out)
|
| 418 |
+
xs = [_gated_residual(x, y, gate) for x, y, gate in zip(xs, out, gates, strict=True)]
|
| 419 |
+
xs = sharding.activation_sharding_constraint(xs)
|
| 420 |
+
|
| 421 |
+
# ---- DA3 spatial injection on the SUFFIX (action-expert, expert 1) stream ONLY ----
|
| 422 |
+
# layer_gate is a per-layer scalar (0 for early blocks, 1 for the last N); with init_std=0 the
|
| 423 |
+
# injection delta is 0 (zero-init output) so this is an exact no-op regardless of the gate.
|
| 424 |
+
if banks is not None and len(xs) > 1 and xs[1] is not None:
|
| 425 |
+
suf = xs[1]
|
| 426 |
+
delta = SpatialActionInjection(
|
| 427 |
+
d_model=self.configs[1].width,
|
| 428 |
+
spatial_scale=self.spatial_scale,
|
| 429 |
+
init_std=self.spatial_init_std,
|
| 430 |
+
logit_gain=self.spatial_logit_gain,
|
| 431 |
+
logit_gain_init=self.spatial_logit_gain_init,
|
| 432 |
+
name="spatial_inject_1",
|
| 433 |
+
)(suf, banks)
|
| 434 |
+
# cast back to the suffix (carry) dtype: the injection runs in fp32 but the scan carry is bf16
|
| 435 |
+
xs = [xs[0], (suf + layer_gate.astype(delta.dtype) * delta).astype(suf.dtype)]
|
| 436 |
+
xs = sharding.activation_sharding_constraint(xs)
|
| 437 |
+
|
| 438 |
+
return xs, kv_cache
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
KVCache: TypeAlias = tuple[at.Float[at.Array, "l b _t _k _h"], at.Float[at.Array, "l b _t _v _h"]]
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
@at.typecheck
|
| 445 |
+
class Module(nn.Module):
|
| 446 |
+
"""Transformer model, supporting a mixture of different weights for different tokens."""
|
| 447 |
+
|
| 448 |
+
configs: Sequence[Config] # list of configs, one for each expert
|
| 449 |
+
embed_dtype: str
|
| 450 |
+
|
| 451 |
+
dropout: float = 0.0
|
| 452 |
+
dropout_bdims: tuple[int, ...] = () # Every float is dropped independently.
|
| 453 |
+
adarms: bool = False
|
| 454 |
+
# DA3 spatial injection: inject per-view banks into the SUFFIX (action-expert) stream at the
|
| 455 |
+
# last `num_spatial_layers` blocks, via a gated cross-attention inside the same scan.
|
| 456 |
+
spatial_inject: bool = False
|
| 457 |
+
num_spatial_layers: int = 6
|
| 458 |
+
spatial_scale: float = 2.0
|
| 459 |
+
spatial_init_std: float = 0.0
|
| 460 |
+
spatial_logit_gain: bool = False # learnable per-head attention-logit gain in the injection (v2)
|
| 461 |
+
spatial_logit_gain_init: float = 1.0 # v2 load-bearing fix: init the per-head gain to 32
|
| 462 |
+
|
| 463 |
+
def setup(self):
|
| 464 |
+
# all experts must have the same depth
|
| 465 |
+
assert all(config.depth == self.configs[0].depth for config in self.configs)
|
| 466 |
+
|
| 467 |
+
self.embedder = Embedder(
|
| 468 |
+
vocab_size=PALIGEMMA_VOCAB_SIZE,
|
| 469 |
+
embed_dim=self.configs[0].width, # embedder for first expert only
|
| 470 |
+
name="embedder",
|
| 471 |
+
)
|
| 472 |
+
# Rematerialization trades memory for compute: activations are recomputed in the backward
|
| 473 |
+
# pass instead of stored. `nothing_saveable` (the default here) is the MOST aggressive
|
| 474 |
+
# setting -- minimum memory, maximum recompute (~30% extra FLOPs). Relaxing it is
|
| 475 |
+
# NUMERICALLY IDENTICAL (it only changes how gradients are computed, not their values), so
|
| 476 |
+
# where VRAM allows, a lighter policy trains faster:
|
| 477 |
+
# B1K_REMAT=full (default) -> nothing_saveable : least memory, slowest
|
| 478 |
+
# B1K_REMAT=dots -> dots_saveable : keeps matmul outputs, much less recompute
|
| 479 |
+
# B1K_REMAT=none -> no remat : most memory, fastest
|
| 480 |
+
_remat = os.environ.get("B1K_REMAT", "full").lower()
|
| 481 |
+
if _remat == "none":
|
| 482 |
+
block_cls = Block
|
| 483 |
+
else:
|
| 484 |
+
block_cls = nn.remat(
|
| 485 |
+
Block,
|
| 486 |
+
prevent_cse=False,
|
| 487 |
+
static_argnums=(5,), # 0=self, 6=deterministic
|
| 488 |
+
policy=jax.checkpoint_policies.dots_saveable
|
| 489 |
+
if _remat == "dots"
|
| 490 |
+
else jax.checkpoint_policies.nothing_saveable,
|
| 491 |
+
)
|
| 492 |
+
# banks are broadcast (same for all blocks, computed once); layer_gate is scanned on axis 0
|
| 493 |
+
# so each block receives its own scalar gate[l] (0 for early blocks, 1 for the last N).
|
| 494 |
+
base_in_axes = (0, nn.broadcast, nn.broadcast, nn.broadcast, nn.broadcast)
|
| 495 |
+
in_axes = (*base_in_axes, nn.broadcast, 0) if self.spatial_inject else base_in_axes
|
| 496 |
+
self.layers = nn.scan(
|
| 497 |
+
block_cls,
|
| 498 |
+
variable_axes={"params": 0},
|
| 499 |
+
split_rngs={"params": True, "dropout": True},
|
| 500 |
+
in_axes=in_axes,
|
| 501 |
+
length=self.configs[0].depth,
|
| 502 |
+
)(
|
| 503 |
+
configs=self.configs,
|
| 504 |
+
dropout=self.dropout,
|
| 505 |
+
dropout_bdims=self.dropout_bdims,
|
| 506 |
+
spatial_scale=self.spatial_scale,
|
| 507 |
+
spatial_init_std=self.spatial_init_std,
|
| 508 |
+
spatial_logit_gain=self.spatial_logit_gain,
|
| 509 |
+
spatial_logit_gain_init=self.spatial_logit_gain_init,
|
| 510 |
+
)
|
| 511 |
+
self.final_norms = [RMSNorm(name=_name("final_norm", i)) for i in range(len(self.configs))]
|
| 512 |
+
|
| 513 |
+
@at.typecheck
|
| 514 |
+
def embed(self, tokens: at.Int[at.Array, "b t"]) -> at.Float[at.Array, "b t d"]:
|
| 515 |
+
return self.embedder.encode(tokens).astype(self.embed_dtype)
|
| 516 |
+
|
| 517 |
+
@at.typecheck
|
| 518 |
+
def __call__(
|
| 519 |
+
self,
|
| 520 |
+
# list of token arrays, one for each expert, or None if that expert should not be run
|
| 521 |
+
embedded: Sequence[at.Float[at.Array, "b _t _d"] | None],
|
| 522 |
+
positions: at.Int[at.Array, "b t"],
|
| 523 |
+
mask: at.Bool[at.Array, "b t s"],
|
| 524 |
+
adarms_cond: Sequence[at.Float[at.Array, "b _d"] | None] | None = None,
|
| 525 |
+
*,
|
| 526 |
+
kv_cache: KVCache | None = None,
|
| 527 |
+
deterministic: bool = True,
|
| 528 |
+
banks: dict | None = None,
|
| 529 |
+
) -> tuple[Sequence[at.Float[at.Array, "b _t _d"] | None], KVCache]:
|
| 530 |
+
embedded = jax.tree.map(lambda e: e.astype(self.embed_dtype), embedded)
|
| 531 |
+
mask = jnp.asarray(mask)[:, None, :, :]
|
| 532 |
+
if adarms_cond is None:
|
| 533 |
+
adarms_cond = [None] * len(self.configs)
|
| 534 |
+
|
| 535 |
+
if self.spatial_inject:
|
| 536 |
+
depth = self.configs[0].depth
|
| 537 |
+
if banks is None:
|
| 538 |
+
# Prefix-only passes carry no banks. The scan's in_axes arity is fixed at build time,
|
| 539 |
+
# so feed DUMMY banks + a zero gate — the Block skips injection anyway when the
|
| 540 |
+
# suffix stream (xs[1]) is None, and the zero gate kills it otherwise.
|
| 541 |
+
b = next(e.shape[0] for e in embedded if e is not None)
|
| 542 |
+
w = self.configs[1].width
|
| 543 |
+
banks = {k: jnp.zeros((b, 1, w), dtype=jnp.dtype(self.embed_dtype)) for k in ("main", "left", "right")}
|
| 544 |
+
layer_gate = jnp.zeros((depth,), jnp.float32)
|
| 545 |
+
else:
|
| 546 |
+
# gate[l] = 1 for the last num_spatial_layers blocks, else 0 (per-layer scalar via scan axis 0)
|
| 547 |
+
layer_gate = (jnp.arange(depth) >= depth - self.num_spatial_layers).astype(jnp.float32)
|
| 548 |
+
embedded, kv_cache = self.layers(
|
| 549 |
+
embedded, kv_cache, positions, mask, adarms_cond, deterministic, banks, layer_gate
|
| 550 |
+
)
|
| 551 |
+
else:
|
| 552 |
+
embedded, kv_cache = self.layers(embedded, kv_cache, positions, mask, adarms_cond, deterministic)
|
| 553 |
+
|
| 554 |
+
assert all(e.dtype == jnp.dtype(self.embed_dtype) for e in embedded if e is not None)
|
| 555 |
+
|
| 556 |
+
return [
|
| 557 |
+
f(e, a)[0] if e is not None else e for f, e, a in zip(self.final_norms, embedded, adarms_cond, strict=True)
|
| 558 |
+
], kv_cache
|
| 559 |
+
|
| 560 |
+
def init(self, use_adarms: Sequence[bool]):
|
| 561 |
+
"""Convenience method for initializing all parameters, necessary due to the quirks of linen."""
|
| 562 |
+
self.embed(jnp.zeros((1, 1), dtype=jnp.int32))
|
| 563 |
+
# dummy banks so the spatial-injection params are materialized during lazy_init (bank dim
|
| 564 |
+
# == action-expert width == configs[1].width); 1 dummy token per view is enough.
|
| 565 |
+
banks = None
|
| 566 |
+
if self.spatial_inject:
|
| 567 |
+
w = self.configs[1].width
|
| 568 |
+
banks = {k: jnp.zeros((1, 1, w)) for k in ("main", "left", "right")}
|
| 569 |
+
self(
|
| 570 |
+
[jnp.zeros((1, 1, c.width)) for c in self.configs],
|
| 571 |
+
jnp.zeros((1, len(self.configs)), dtype=jnp.int32),
|
| 572 |
+
jnp.zeros((1, len(self.configs), len(self.configs)), dtype=bool),
|
| 573 |
+
adarms_cond=[jnp.zeros((1, c.width)) if u else None for u, c in zip(use_adarms, self.configs, strict=True)],
|
| 574 |
+
banks=banks,
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
def _apply_rope(x, *, positions, max_wavelength=10_000):
|
| 579 |
+
"""Applies RoPE positions [B, L] to x [B, L, H, D]."""
|
| 580 |
+
freq_exponents = (2.0 / x.shape[-1]) * jnp.arange(x.shape[-1] // 2, dtype=jnp.float32)
|
| 581 |
+
timescale = max_wavelength**freq_exponents
|
| 582 |
+
radians = positions[..., None] / timescale[None, None, :]
|
| 583 |
+
radians = radians[..., None, :]
|
| 584 |
+
assert radians.dtype == jnp.float32
|
| 585 |
+
# radians.shape = [...,L,1,d=D/2]
|
| 586 |
+
sin, cos = jnp.sin(radians), jnp.cos(radians)
|
| 587 |
+
x1, x2 = jnp.split(x, 2, axis=-1)
|
| 588 |
+
res = jnp.concatenate([x1 * cos - x2 * sin, x2 * cos + x1 * sin], axis=-1)
|
| 589 |
+
assert res.dtype == jnp.float32
|
| 590 |
+
# The original bigvision impl allows RoPE to upcast to float32. It is then immediately downcast again to the cache
|
| 591 |
+
# dtype when in inference mode (but not in training mode). I don't think any of this was intentional. Based on the
|
| 592 |
+
# original DeepMind impl, as well as the widely-used transformers impl, it is ok to always downcast back to bfloat16
|
| 593 |
+
# here.
|
| 594 |
+
return res.astype(x.dtype)
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def _name(name, i):
|
| 598 |
+
# we name layers like this because we want the first expert's weights to have no suffix (e.g., "attn"), so that they
|
| 599 |
+
# can be loaded seamlessly from the existing PaliGemma checkpoint. subsequent experts will have a suffix (e.g.,
|
| 600 |
+
# "attn_1") and their weights will be initialized from scratch. in practice, we only use two experts -- PaliGemma,
|
| 601 |
+
# and the action expert.
|
| 602 |
+
if i == 0:
|
| 603 |
+
return name
|
| 604 |
+
return f"{name}_{i}"
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def _gated_residual(x, y, gate):
|
| 608 |
+
assert (x is None) == (y is None)
|
| 609 |
+
if x is None:
|
| 610 |
+
return None
|
| 611 |
+
if gate is None:
|
| 612 |
+
return x + y
|
| 613 |
+
return x + y * gate
|
pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/optimizer.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import dataclasses
|
| 2 |
+
from typing import Protocol, runtime_checkable
|
| 3 |
+
|
| 4 |
+
import jax.numpy as jnp
|
| 5 |
+
import optax
|
| 6 |
+
|
| 7 |
+
import openpi.shared.array_typing as at
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@runtime_checkable
|
| 11 |
+
class LRScheduleConfig(Protocol):
|
| 12 |
+
def create(self) -> optax.Schedule: ...
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclasses.dataclass(frozen=True)
|
| 16 |
+
class CosineDecaySchedule(LRScheduleConfig):
|
| 17 |
+
"""Cosine decay schedule with warmup."""
|
| 18 |
+
|
| 19 |
+
warmup_steps: int = 1_000
|
| 20 |
+
peak_lr: float = 2.5e-5
|
| 21 |
+
decay_steps: int = 30_000
|
| 22 |
+
decay_lr: float = 2.5e-6
|
| 23 |
+
|
| 24 |
+
def create(self) -> optax.Schedule:
|
| 25 |
+
return optax.warmup_cosine_decay_schedule(
|
| 26 |
+
init_value=self.peak_lr / (self.warmup_steps + 1),
|
| 27 |
+
peak_value=self.peak_lr,
|
| 28 |
+
warmup_steps=self.warmup_steps,
|
| 29 |
+
decay_steps=self.decay_steps,
|
| 30 |
+
end_value=self.decay_lr,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclasses.dataclass(frozen=True)
|
| 35 |
+
class DelayedRampCosineSchedule(LRScheduleConfig):
|
| 36 |
+
"""Hold at exactly 0 for `delay_steps`, linearly ramp 0 -> peak over `ramp_steps`, then cosine
|
| 37 |
+
decay to `decay_lr` at `decay_steps` (total).
|
| 38 |
+
|
| 39 |
+
For the DA3 *geometry* group: the randomly-initialized ray-MLP is held frozen while the
|
| 40 |
+
pretrained trunk takes the initial adaptation, then phased in slowly instead of hitting peak LR
|
| 41 |
+
alongside everything else. Geometry modules are sensitive to being driven hard from init.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
delay_steps: int = 500
|
| 45 |
+
ramp_steps: int = 10_000
|
| 46 |
+
peak_lr: float = 7e-4
|
| 47 |
+
decay_steps: int = 25_000
|
| 48 |
+
decay_lr: float = 1e-4
|
| 49 |
+
|
| 50 |
+
def create(self) -> optax.Schedule:
|
| 51 |
+
tail = max(1, self.decay_steps - self.delay_steps - self.ramp_steps)
|
| 52 |
+
return optax.join_schedules(
|
| 53 |
+
[
|
| 54 |
+
optax.constant_schedule(0.0), # frozen: geometry group takes no updates
|
| 55 |
+
optax.linear_schedule(init_value=0.0, end_value=self.peak_lr, transition_steps=self.ramp_steps),
|
| 56 |
+
optax.cosine_decay_schedule(
|
| 57 |
+
init_value=self.peak_lr, decay_steps=tail, alpha=self.decay_lr / self.peak_lr
|
| 58 |
+
),
|
| 59 |
+
],
|
| 60 |
+
[self.delay_steps, self.delay_steps + self.ramp_steps],
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclasses.dataclass(frozen=True)
|
| 65 |
+
class RsqrtDecaySchedule(LRScheduleConfig):
|
| 66 |
+
"""Inverse square root decay schedule with warmup."""
|
| 67 |
+
|
| 68 |
+
warmup_steps: int = 1_000
|
| 69 |
+
peak_lr: float = 5e-5
|
| 70 |
+
timescale: float = 10_000
|
| 71 |
+
|
| 72 |
+
def create(self) -> optax.Schedule:
|
| 73 |
+
return optax.join_schedules(
|
| 74 |
+
[
|
| 75 |
+
optax.linear_schedule(
|
| 76 |
+
init_value=self.peak_lr / (self.warmup_steps + 1),
|
| 77 |
+
end_value=self.peak_lr,
|
| 78 |
+
transition_steps=self.warmup_steps,
|
| 79 |
+
),
|
| 80 |
+
lambda step: self.peak_lr / jnp.sqrt((self.timescale + step) / self.timescale),
|
| 81 |
+
],
|
| 82 |
+
[self.warmup_steps],
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@runtime_checkable
|
| 87 |
+
class OptimizerConfig(Protocol):
|
| 88 |
+
def create(
|
| 89 |
+
self,
|
| 90 |
+
lr: optax.ScalarOrSchedule,
|
| 91 |
+
weight_decay_mask: at.PyTree | None = None,
|
| 92 |
+
) -> optax.GradientTransformation: ...
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@dataclasses.dataclass(frozen=True)
|
| 96 |
+
class AdamW(OptimizerConfig):
|
| 97 |
+
"""AdamW optimizer."""
|
| 98 |
+
|
| 99 |
+
b1: float = 0.9
|
| 100 |
+
b2: float = 0.95
|
| 101 |
+
eps: float = 1e-8
|
| 102 |
+
# Changing this to 0 can cause out-of-memory errors for some reason, so we set it to a negligible value.
|
| 103 |
+
weight_decay: float = 1e-10
|
| 104 |
+
clip_gradient_norm: float = 1.0
|
| 105 |
+
|
| 106 |
+
def create(
|
| 107 |
+
self,
|
| 108 |
+
lr: optax.ScalarOrSchedule,
|
| 109 |
+
weight_decay_mask: at.PyTree | None = None,
|
| 110 |
+
) -> optax.GradientTransformation:
|
| 111 |
+
tx = optax.adamw(
|
| 112 |
+
lr, b1=self.b1, b2=self.b2, eps=self.eps, weight_decay=self.weight_decay, mask=weight_decay_mask
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
return optax.chain(optax.clip_by_global_norm(self.clip_gradient_norm), tx)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@dataclasses.dataclass(frozen=True)
|
| 119 |
+
class SGD(OptimizerConfig):
|
| 120 |
+
"""SGD optimizer."""
|
| 121 |
+
|
| 122 |
+
lr: float = 5e-5
|
| 123 |
+
momentum: float = 0.9
|
| 124 |
+
nesterov: bool = False
|
| 125 |
+
|
| 126 |
+
def create(
|
| 127 |
+
self,
|
| 128 |
+
lr: optax.ScalarOrSchedule,
|
| 129 |
+
weight_decay_mask: at.PyTree | None = None,
|
| 130 |
+
) -> optax.GradientTransformation:
|
| 131 |
+
assert weight_decay_mask is None, "Weight decay is not supported for SGD"
|
| 132 |
+
return optax.sgd(lr, momentum=self.momentum, nesterov=self.nesterov)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def create_optimizer(
|
| 136 |
+
optimizer: OptimizerConfig, lr_schedule: LRScheduleConfig, weight_decay_mask: at.PyTree | None = None
|
| 137 |
+
) -> optax.GradientTransformation:
|
| 138 |
+
lr = lr_schedule.create()
|
| 139 |
+
return optimizer.create(lr, weight_decay_mask=weight_decay_mask)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def spatial_group_labels(params: at.PyTree) -> at.PyTree:
|
| 143 |
+
"""Label each param leaf 'geom' | 'core' | 'vlm' by its keypath, for the DA3 multi-LR setup.
|
| 144 |
+
|
| 145 |
+
geom = the DA3 geometry encoder (ray MLP); core = the trainable spatial branch (bank builder +
|
| 146 |
+
cross-attn injection); vlm = the PiBehavior backbone (VLM + action expert + task/stage/kv heads).
|
| 147 |
+
"""
|
| 148 |
+
import jax
|
| 149 |
+
|
| 150 |
+
def label(kp, _leaf):
|
| 151 |
+
s = jax.tree_util.keystr(kp)
|
| 152 |
+
if "ray_mlp" in s:
|
| 153 |
+
return "geom"
|
| 154 |
+
if "spatial_bank_builder" in s or "spatial_inject" in s:
|
| 155 |
+
return "core"
|
| 156 |
+
return "vlm"
|
| 157 |
+
|
| 158 |
+
return jax.tree_util.tree_map_with_path(label, params)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def create_multi_group_optimizer(
|
| 162 |
+
optimizer: OptimizerConfig,
|
| 163 |
+
lr_schedules: dict[str, LRScheduleConfig],
|
| 164 |
+
label_fn,
|
| 165 |
+
weight_decay_mask: at.PyTree | None = None,
|
| 166 |
+
) -> optax.GradientTransformation:
|
| 167 |
+
"""Per-parameter-group optimizer: a distinct LR schedule per group via optax.multi_transform.
|
| 168 |
+
|
| 169 |
+
`lr_schedules` maps group name -> schedule; `label_fn(params)` returns a matching-structure tree of
|
| 170 |
+
group names. Every leaf must map to a group present in `lr_schedules`.
|
| 171 |
+
"""
|
| 172 |
+
txs = {
|
| 173 |
+
group: optimizer.create(sched.create(), weight_decay_mask=weight_decay_mask)
|
| 174 |
+
for group, sched in lr_schedules.items()
|
| 175 |
+
}
|
| 176 |
+
return optax.multi_transform(txs, label_fn)
|
pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/sharding.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import jax
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
BATCH_AXIS = "batch"
|
| 9 |
+
FSDP_AXIS = "fsdp"
|
| 10 |
+
# In FSDP, we shard the data across both the batch and FSDP axes.
|
| 11 |
+
DATA_AXIS = (BATCH_AXIS, FSDP_AXIS)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class _MeshState:
|
| 15 |
+
active_mesh: jax.sharding.Mesh | None = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def make_mesh(num_fsdp_devices: int) -> jax.sharding.Mesh:
|
| 19 |
+
devices = jax.devices()
|
| 20 |
+
# B1K DA3 inline-extraction speedup: hold GPU 0 out of the training mesh so the frozen DA3-GIANT
|
| 21 |
+
# extractor (pinned to cuda:0) runs there WITHOUT contending with the JAX train step. The prefetch
|
| 22 |
+
# thread then overlaps extraction of batch N+1 with the training of batch N on GPUs 1..N.
|
| 23 |
+
holdout = int(os.environ.get("B1K_HOLDOUT_GPU0", "0"))
|
| 24 |
+
if holdout > 0:
|
| 25 |
+
devices = devices[holdout:]
|
| 26 |
+
logging.info("make_mesh: holding out %d device(s) for extraction; training on %d devices %s",
|
| 27 |
+
holdout, len(devices), [d.id for d in devices])
|
| 28 |
+
n = len(devices)
|
| 29 |
+
if n % num_fsdp_devices != 0:
|
| 30 |
+
raise ValueError(
|
| 31 |
+
f"Number of training devices {n} must be divisible by the number of FSDP devices {num_fsdp_devices}."
|
| 32 |
+
)
|
| 33 |
+
mesh_shape = (n // num_fsdp_devices, num_fsdp_devices)
|
| 34 |
+
mesh_devices = np.array(devices).reshape(mesh_shape)
|
| 35 |
+
return jax.sharding.Mesh(mesh_devices, (BATCH_AXIS, FSDP_AXIS))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@contextlib.contextmanager
|
| 39 |
+
def set_mesh(mesh: jax.sharding.Mesh):
|
| 40 |
+
"""Plumbing the mesh deep into the module tree is extremeley cumbersome; until the JAX team lands a better API, a
|
| 41 |
+
custom context manager like this one is the recommended way to maintain a reference to a global mesh. This is only used
|
| 42 |
+
in `activation_sharding_constraint` below."""
|
| 43 |
+
if _MeshState.active_mesh is not None:
|
| 44 |
+
raise ValueError("Cannot nest set_mesh context managers.")
|
| 45 |
+
_MeshState.active_mesh = mesh
|
| 46 |
+
try:
|
| 47 |
+
yield
|
| 48 |
+
finally:
|
| 49 |
+
_MeshState.active_mesh = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def activation_sharding_constraint(pytree):
|
| 53 |
+
if _MeshState.active_mesh is None:
|
| 54 |
+
return pytree
|
| 55 |
+
return jax.lax.with_sharding_constraint(
|
| 56 |
+
pytree, jax.sharding.NamedSharding(_MeshState.active_mesh, jax.sharding.PartitionSpec(DATA_AXIS))
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def fsdp_sharding(
|
| 61 |
+
pytree,
|
| 62 |
+
mesh: jax.sharding.Mesh,
|
| 63 |
+
*,
|
| 64 |
+
min_size_mbytes: int = 4, # 4 MiB
|
| 65 |
+
log: bool = False,
|
| 66 |
+
):
|
| 67 |
+
"""Apply FSDP sharding to a pytree of arrays based on the mesh shape.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
pytree: A pytree to be apply sharding specified by the mesh, note that only array types (eg. contains .shape attr)
|
| 71 |
+
will be considered for sharding.
|
| 72 |
+
mesh: The mesh being used for applying sharding on to pytree.
|
| 73 |
+
min_size_mbytes: The minimum size of the array in MiB to be considered for sharding, any array smaller than this
|
| 74 |
+
will be replicated.
|
| 75 |
+
log: If true, will log the sharding decisions for arrays that are being considered for sharding.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
The sharded pytree.
|
| 79 |
+
"""
|
| 80 |
+
min_size_bytes = min_size_mbytes * 2**20
|
| 81 |
+
|
| 82 |
+
def _shard_arr(kp, array: jax.ShapeDtypeStruct):
|
| 83 |
+
# if fsdp is not actually going to be used, replicate everything to avoid extraneous logging
|
| 84 |
+
if mesh.shape[FSDP_AXIS] == 1:
|
| 85 |
+
return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
|
| 86 |
+
# replicate scalar and vector arrays
|
| 87 |
+
if not hasattr(array, "shape"):
|
| 88 |
+
return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
|
| 89 |
+
if len(array.shape) < 2:
|
| 90 |
+
return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
|
| 91 |
+
# replicate small arrays
|
| 92 |
+
if (arr_size := np.prod(array.shape) * np.dtype(array.dtype).itemsize) < min_size_bytes:
|
| 93 |
+
return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
|
| 94 |
+
|
| 95 |
+
# shard matrices and larger tensors along the largest axis that is divisible by the fsdp dimension
|
| 96 |
+
axes = np.argsort(array.shape)[::-1]
|
| 97 |
+
spec = [None] * len(axes)
|
| 98 |
+
for i in axes:
|
| 99 |
+
if array.shape[i] % mesh.shape[FSDP_AXIS] == 0:
|
| 100 |
+
if log:
|
| 101 |
+
logging.info(
|
| 102 |
+
f"Sharding {jax.tree_util.keystr(kp)} of shape {array.shape} ({arr_size / 2**20:.2f} MiB) along axis {i}"
|
| 103 |
+
)
|
| 104 |
+
spec[i] = FSDP_AXIS
|
| 105 |
+
return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec(*spec))
|
| 106 |
+
|
| 107 |
+
# replicate if no valid sharding was found
|
| 108 |
+
if log:
|
| 109 |
+
logging.warning(
|
| 110 |
+
f"Could not find a valid sharding for {jax.tree_util.keystr(kp)} of shape {array.shape} with mesh of shape {mesh.shape}"
|
| 111 |
+
)
|
| 112 |
+
return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
|
| 113 |
+
|
| 114 |
+
return jax.tree_util.tree_map_with_path(_shard_arr, pytree)
|
pibehavior_da3_clean_up_your_desk_40k/source_code/openpi/weight_loaders.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import dataclasses
|
| 2 |
+
import logging
|
| 3 |
+
import re
|
| 4 |
+
from typing import Protocol, runtime_checkable
|
| 5 |
+
|
| 6 |
+
import flax.traverse_util
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
import openpi.models.model as _model
|
| 10 |
+
import openpi.shared.array_typing as at
|
| 11 |
+
import openpi.shared.download as download
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@runtime_checkable
|
| 17 |
+
class WeightLoader(Protocol):
|
| 18 |
+
def load(self, params: at.Params) -> at.Params:
|
| 19 |
+
"""Loads the model weights.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
params: Parameters of the model. This is a nested structure of array-like objects that
|
| 23 |
+
represent the model's parameters.
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
Loaded parameters. The structure must be identical to `params`. If returning a subset of
|
| 27 |
+
the parameters the loader must merge the loaded parameters with `params`.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclasses.dataclass(frozen=True)
|
| 32 |
+
class NoOpWeightLoader(WeightLoader):
|
| 33 |
+
def load(self, params: at.Params) -> at.Params:
|
| 34 |
+
return params
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclasses.dataclass(frozen=True)
|
| 38 |
+
class CheckpointWeightLoader(WeightLoader):
|
| 39 |
+
"""Loads an entire set of weights from a checkpoint.
|
| 40 |
+
|
| 41 |
+
Compatible with:
|
| 42 |
+
trained checkpoints:
|
| 43 |
+
example: "./checkpoints/<config>/<exp>/<step>/params"
|
| 44 |
+
released checkpoints:
|
| 45 |
+
example: "gs://openpi-assets/checkpoints/<model>/params"
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
params_path: str
|
| 49 |
+
|
| 50 |
+
def load(self, params: at.Params) -> at.Params:
|
| 51 |
+
# We are loading np.ndarray and relying on the training code to properly convert and shard the params.
|
| 52 |
+
loaded_params = _model.restore_params(download.maybe_download(self.params_path), restore_type=np.ndarray)
|
| 53 |
+
# Add all missing LoRA weights.
|
| 54 |
+
return _merge_params(loaded_params, params, missing_regex=".*lora.*")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclasses.dataclass(frozen=True)
|
| 58 |
+
class PaliGemmaWeightLoader(WeightLoader):
|
| 59 |
+
"""Loads weights from the official PaliGemma checkpoint.
|
| 60 |
+
|
| 61 |
+
This will overwrite existing weights with similar names while keeping all extra weights intact.
|
| 62 |
+
This allows us to support the action expert which is used by the Pi0 model.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def load(self, params: at.Params) -> at.Params:
|
| 66 |
+
path = download.maybe_download(
|
| 67 |
+
"gs://vertex-model-garden-paligemma-us/paligemma/pt_224.npz", gs={"token": "anon"}
|
| 68 |
+
)
|
| 69 |
+
with path.open("rb") as f:
|
| 70 |
+
flat_params = dict(np.load(f, allow_pickle=False))
|
| 71 |
+
loaded_params = {"PaliGemma": flax.traverse_util.unflatten_dict(flat_params, sep="/")["params"]}
|
| 72 |
+
# Add all missing weights.
|
| 73 |
+
return _merge_params(loaded_params, params, missing_regex=".*")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _merge_params(loaded_params: at.Params, params: at.Params, *, missing_regex: str) -> at.Params:
|
| 77 |
+
"""Merges the loaded parameters with the reference parameters.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
loaded_params: The parameters to merge.
|
| 81 |
+
params: The reference parameters.
|
| 82 |
+
missing_regex: A regex pattern for all missing keys that should be merged from the reference parameters.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
A new dictionary with the merged parameters.
|
| 86 |
+
"""
|
| 87 |
+
# Flatten with TUPLE keys (no sep-join): some modules (e.g. the DA3 spatial bank builder) store
|
| 88 |
+
# submodules in lists, which nnx encodes with INTEGER path elements — sep="/" would crash on
|
| 89 |
+
# `"/".join(path)`. The string path is built only for the regex match below.
|
| 90 |
+
flat_ref = flax.traverse_util.flatten_dict(params)
|
| 91 |
+
flat_loaded = flax.traverse_util.flatten_dict(loaded_params)
|
| 92 |
+
|
| 93 |
+
# First, take all weights that are a subset of the reference weights.
|
| 94 |
+
result = {}
|
| 95 |
+
for k, v in flat_loaded.items():
|
| 96 |
+
if k in flat_ref:
|
| 97 |
+
if v.dtype == flat_ref[k].dtype:
|
| 98 |
+
result[k] = v
|
| 99 |
+
else:
|
| 100 |
+
print(f"Warning: {k} has dtype {v.dtype} but reference has dtype {flat_ref[k].dtype}")
|
| 101 |
+
result[k] = v.astype(flat_ref[k].dtype)
|
| 102 |
+
flat_loaded.clear()
|
| 103 |
+
|
| 104 |
+
# Then, merge any missing weights as defined by the missing regex.
|
| 105 |
+
pattern = re.compile(missing_regex)
|
| 106 |
+
for k in {k for k in flat_ref if pattern.fullmatch("/".join(str(p) for p in k))}:
|
| 107 |
+
if k not in result:
|
| 108 |
+
result[k] = flat_ref[k]
|
| 109 |
+
|
| 110 |
+
return flax.traverse_util.unflatten_dict(result)
|
pibehavior_da3_clean_up_your_desk_40k/source_code/training/_da3_contrib_check.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Measure DA3 contribution on a trained checkpoint, on CORRECTED geometry.
|
| 2 |
+
CPU-only (extractor on CPU) so it never touches the training GPUs.
|
| 3 |
+
|
| 4 |
+
Generates one real batch through the fixed-geometry loader, loads the checkpoint, then compares
|
| 5 |
+
loss WITH banks vs banks-zeroed (same noise). If delta ~ 0 => the model IGNORES spatial (the
|
| 6 |
+
zero-init failure mode). If delta > 0 => DA3 is load-bearing.
|
| 7 |
+
"""
|
| 8 |
+
import os, sys, dataclasses, json
|
| 9 |
+
import numpy as np, jax, jax.numpy as jnp, flax.nnx as nnx
|
| 10 |
+
sys.path.insert(0, "scripts")
|
| 11 |
+
|
| 12 |
+
CKPT = os.environ.get("CONTRIB_CKPT",
|
| 13 |
+
"/work/jack/behavior-1k-solution/outputs/checkpoints/pi_behavior_b1k_fast/b1k_da3_robopro_desk/14000/params")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
os.environ.update(USE_DA3_FULL="1", DA3_INIT_STD="0.01", DA3_LOGIT_GAIN="1",
|
| 18 |
+
B1K_ACTIVITIES="clean_up_your_desk", B1K_EXTRACT_DEVICES="cpu",
|
| 19 |
+
B1K_2026_ROOT="/work/jack/behavior1k/data/behavior_2026_task29_42",
|
| 20 |
+
B1K_INIT_PARAMS=CKPT)
|
| 21 |
+
import train_2026 as t26
|
| 22 |
+
from b1k.training.b1k_da3 import create_v3_behavior_da3_loader
|
| 23 |
+
|
| 24 |
+
config = dataclasses.replace(t26.build_config(), batch_size=2, num_workers=0)
|
| 25 |
+
import jax as _j
|
| 26 |
+
sharding = _j.sharding.SingleDeviceSharding(_j.devices("cpu")[0])
|
| 27 |
+
print("[1] building CPU loader (extractor on CPU, corrected geometry)...", flush=True)
|
| 28 |
+
loader = create_v3_behavior_da3_loader(
|
| 29 |
+
config, os.environ["B1K_2026_ROOT"], ["clean_up_your_desk"], "/work/jack/behavior1k/task_data.json",
|
| 30 |
+
lang_cache="/work/jack/behavior1k/modernbert_b1k_tasks.pkl", sharding=sharding, shuffle=True, num_workers=0)
|
| 31 |
+
obs, actions = next(iter(loader))
|
| 32 |
+
print("[2] got one fixed-geometry batch; loading checkpoint", flush=True)
|
| 33 |
+
|
| 34 |
+
model = config.model.create(jax.random.key(0))
|
| 35 |
+
loaded = config.weight_loader.load(jax.tree.map(np.asarray, nnx.state(model).to_pure_dict()))
|
| 36 |
+
gd, st = nnx.split(model); st.replace_by_pure_dict(loaded); model = nnx.merge(gd, st)
|
| 37 |
+
raw = json.load(open("/work/jack/behavior1k/checkpoints/behavior_50t_checkpoint/assets/IliaLarchenko/behavior_224_rgb/norm_stats.json"))["norm_stats"]
|
| 38 |
+
model.load_correlation_matrix({"actions": {k: (np.asarray(v, np.float32) if isinstance(v, list) else v) for k, v in raw["actions"].items()}})
|
| 39 |
+
|
| 40 |
+
rng = jax.random.key(7); NFS = 8
|
| 41 |
+
ld_with = model.compute_detailed_loss(rng, obs, actions, train=False, num_flow_samples=NFS)
|
| 42 |
+
orig = model._compute_banks; model._compute_banks = lambda o: None
|
| 43 |
+
ld_wo = model.compute_detailed_loss(rng, obs, actions, train=False, num_flow_samples=NFS)
|
| 44 |
+
model._compute_banks = orig
|
| 45 |
+
aw, ao = float(jnp.mean(ld_with["action_loss"])), float(jnp.mean(ld_wo["action_loss"]))
|
| 46 |
+
tw, to = float(jnp.mean(ld_with["total_loss"])), float(jnp.mean(ld_wo["total_loss"]))
|
| 47 |
+
print(f"[3] action_loss WITH banks={aw:.5f} WITHOUT(zeroed)={ao:.5f} delta={ao-aw:+.5f} ({100*(ao-aw)/max(aw,1e-6):+.1f}%)", flush=True)
|
| 48 |
+
print(f"[3] total_loss WITH banks={tw:.5f} WITHOUT(zeroed)={to:.5f} delta={to-tw:+.5f}", flush=True)
|
| 49 |
+
# inspect how far the injection out_proj has grown off zero-init
|
| 50 |
+
flat = nnx.state(model, nnx.Param).flat_state()
|
| 51 |
+
import math
|
| 52 |
+
def gnorm(sub):
|
| 53 |
+
return math.sqrt(sum(float((np.asarray(v.value)**2).sum()) for k,v in flat.items() if sub in "/".join(str(p) for p in k)))
|
| 54 |
+
print(f"[4] injection out_proj L2={gnorm('out_proj'):.4f} merge_proj L2={gnorm('merge_proj'):.4f} "
|
| 55 |
+
f"(near 0 => injection stayed off = model ignoring spatial)", flush=True)
|
| 56 |
+
verdict = ("DA3 IGNORED (zero-init failure — switch to nonzero init)" if (ao-aw) < 0.005 else
|
| 57 |
+
"DA3 load-bearing" if (ao-aw) > 0.02 else "DA3 weakly contributing")
|
| 58 |
+
print(f"[VERDICT] {verdict}", flush=True)
|
| 59 |
+
print("CONTRIB CHECK DONE", flush=True)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
main()
|
pibehavior_da3_clean_up_your_desk_40k/source_code/training/b1k_2026.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BEHAVIOR-1K 2026 (LeRobot v3.0) data loading for the 2025 PiBehavior model.
|
| 2 |
+
|
| 3 |
+
The 2025 winner's loader targets OmniGibson's v2.1 `BehaviorLeRobotDataset`
|
| 4 |
+
(task-partitioned, 256-dim proprio). The 2026 challenge dataset is LeRobot
|
| 5 |
+
**v3.0** (chunk-based `data/chunk-XXX/file-XXX.parquet`, RGB in videos, 61-dim
|
| 6 |
+
proprio, 100 tasks). The pinned lerobot (v2.1) cannot read it, so this module
|
| 7 |
+
provides a self-contained v3 reader that yields items in the exact dict format
|
| 8 |
+
the existing transform pipeline expects, plus the two remaps needed:
|
| 9 |
+
|
| 10 |
+
* camera keys: zed_link -> head, left/right_realsense -> left/right_wrist
|
| 11 |
+
* task_index: 2026 index -> the 2025 index the checkpoint's task-embedding
|
| 12 |
+
table is keyed on (via activity name <-> task_data.json)
|
| 13 |
+
|
| 14 |
+
Videos are decoded with PyAV (torchcodec's ffmpeg libs are absent here).
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import dataclasses
|
| 19 |
+
import functools
|
| 20 |
+
import glob
|
| 21 |
+
import hashlib
|
| 22 |
+
import json
|
| 23 |
+
import logging
|
| 24 |
+
import os
|
| 25 |
+
import time
|
| 26 |
+
from collections import OrderedDict
|
| 27 |
+
from typing import Dict, List, Optional
|
| 28 |
+
|
| 29 |
+
import av
|
| 30 |
+
import einops
|
| 31 |
+
import numpy as np
|
| 32 |
+
import pandas as pd
|
| 33 |
+
import torch
|
| 34 |
+
|
| 35 |
+
from openpi import transforms
|
| 36 |
+
from openpi.models import model as _model
|
| 37 |
+
|
| 38 |
+
logger = logging.getLogger("b1k.2026")
|
| 39 |
+
|
| 40 |
+
# ---- 2026 R1Pro proprioception layout (61-dim), from BEHAVIOR-1K main
|
| 41 |
+
# OmniGibson/omnigibson/eval/utils/eval_utils.py::PROPRIOCEPTION_INDICES["R1Pro"]
|
| 42 |
+
PROP_2026 = {
|
| 43 |
+
"base_qvel": slice(0, 3),
|
| 44 |
+
"arm_left_qpos": slice(3, 10),
|
| 45 |
+
"gripper_left_qpos": slice(24, 26),
|
| 46 |
+
"arm_right_qpos": slice(28, 35),
|
| 47 |
+
"gripper_right_qpos": slice(49, 51),
|
| 48 |
+
"trunk_qpos": slice(53, 57),
|
| 49 |
+
}
|
| 50 |
+
MAX_GRIPPER_WIDTH = 0.1 # matches the 2025 gripper normalization
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def extract_state_2026(proprio: np.ndarray) -> np.ndarray:
|
| 54 |
+
"""61-dim 2026 proprio -> 23-dim model state, in the SAME field order the
|
| 55 |
+
2025 `extract_state_from_proprio` produced (base_qvel, trunk, arm_left,
|
| 56 |
+
gripper_left, arm_right, gripper_right)."""
|
| 57 |
+
p = np.asarray(proprio, dtype=np.float32)
|
| 58 |
+
base_qvel = p[..., PROP_2026["base_qvel"]] # 3
|
| 59 |
+
trunk_qpos = p[..., PROP_2026["trunk_qpos"]] # 4
|
| 60 |
+
arm_left = p[..., PROP_2026["arm_left_qpos"]] # 7
|
| 61 |
+
arm_right = p[..., PROP_2026["arm_right_qpos"]] # 7
|
| 62 |
+
lg = p[..., PROP_2026["gripper_left_qpos"]].sum(-1, keepdims=True)
|
| 63 |
+
rg = p[..., PROP_2026["gripper_right_qpos"]].sum(-1, keepdims=True)
|
| 64 |
+
lg = 2.0 * (lg / MAX_GRIPPER_WIDTH) - 1.0 # -> [-1,1]
|
| 65 |
+
rg = 2.0 * (rg / MAX_GRIPPER_WIDTH) - 1.0
|
| 66 |
+
return np.concatenate([base_qvel, trunk_qpos, arm_left, lg, arm_right, rg], axis=-1)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@dataclasses.dataclass(frozen=True)
|
| 70 |
+
class B1kInputs2026(transforms.DataTransformFn):
|
| 71 |
+
"""Same as b1k_policy.B1kInputs but with 2026 61-dim state extraction and
|
| 72 |
+
no OmniGibson dependency."""
|
| 73 |
+
model_type: object = _model.ModelType.PI0
|
| 74 |
+
|
| 75 |
+
def __call__(self, data: dict) -> dict:
|
| 76 |
+
state = extract_state_2026(data["observation/state"])
|
| 77 |
+
|
| 78 |
+
def _img(x):
|
| 79 |
+
x = np.asarray(x)
|
| 80 |
+
if np.issubdtype(x.dtype, np.floating):
|
| 81 |
+
x = (255 * x).astype(np.uint8)
|
| 82 |
+
if x.shape[0] == 3:
|
| 83 |
+
x = einops.rearrange(x, "c h w -> h w c")
|
| 84 |
+
return x
|
| 85 |
+
|
| 86 |
+
names = ("base_0_rgb", "left_wrist_0_rgb", "right_wrist_0_rgb")
|
| 87 |
+
imgs = (_img(data["observation/egocentric_camera"]),
|
| 88 |
+
_img(data["observation/wrist_image_left"]),
|
| 89 |
+
_img(data["observation/wrist_image_right"]))
|
| 90 |
+
out = {
|
| 91 |
+
"state": state,
|
| 92 |
+
"image": dict(zip(names, imgs, strict=True)),
|
| 93 |
+
"image_mask": {n: np.True_ for n in names},
|
| 94 |
+
}
|
| 95 |
+
for k in ("actions", "task_index", "timestamp", "episode_index",
|
| 96 |
+
"tokenized_prompt", "tokenized_prompt_mask", "subtask_state"):
|
| 97 |
+
if k in data:
|
| 98 |
+
out[k] = data[k]
|
| 99 |
+
return out
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class _V3Meta:
|
| 103 |
+
"""Minimal `dataset.meta` shim for ComputeSubtaskStateFromMeta.
|
| 104 |
+
`.episodes` maps episode_index -> {'length': int}."""
|
| 105 |
+
def __init__(self, episodes: Dict[int, dict]):
|
| 106 |
+
self.episodes = episodes
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# --------------------------------------------------------------------------- #
|
| 110 |
+
# task-index remapping: 2026 index -> 2025 index (what the checkpoint knows)
|
| 111 |
+
# --------------------------------------------------------------------------- #
|
| 112 |
+
def build_task_index_maps(root_2026: str, task_data_json: str):
|
| 113 |
+
"""Return (name->2025idx, 2026idx->2025idx, name->2026idx)."""
|
| 114 |
+
td = json.load(open(task_data_json))["tasks"]
|
| 115 |
+
name2025 = {t["id"]: i for i, t in enumerate(td)} # activity -> 2025 idx
|
| 116 |
+
dt = pd.read_parquet(os.path.join(root_2026, "meta", "tasks.parquet"))
|
| 117 |
+
# tasks.parquet: index = activity name, column task_index (2026)
|
| 118 |
+
name2026 = {name: int(row["task_index"]) for name, row in dt.iterrows()}
|
| 119 |
+
idx2026_to_2025 = {name2026[n]: name2025[n] for n in name2026 if n in name2025}
|
| 120 |
+
return name2025, idx2026_to_2025, name2026
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class BehaviorV3Dataset(torch.utils.data.Dataset):
|
| 124 |
+
"""LeRobot v3.0 reader for a subset of activities. Yields per-frame items in
|
| 125 |
+
the 2025-style LeRobot dict format (pre-repack keys), with task_index already
|
| 126 |
+
remapped to the 2025 index and RGB decoded to uint8 HWC."""
|
| 127 |
+
|
| 128 |
+
RGB_KEYS = OrderedDict([
|
| 129 |
+
("observation.images.rgb.head", "observation.rgb.zed_link_camera_0"),
|
| 130 |
+
("observation.images.rgb.left_wrist", "observation.rgb.left_realsense_link_camera_0"),
|
| 131 |
+
("observation.images.rgb.right_wrist", "observation.rgb.right_realsense_link_camera_0"),
|
| 132 |
+
])
|
| 133 |
+
|
| 134 |
+
def __init__(self, root: str, activities: List[str], action_horizon: int,
|
| 135 |
+
task_data_json: str, seed: int = 0, parquet_cache: int = 16):
|
| 136 |
+
self.root = root
|
| 137 |
+
self.H = int(action_horizon)
|
| 138 |
+
self.fps = float(json.load(open(os.path.join(root, "meta", "info.json")))["fps"])
|
| 139 |
+
_, self.idx2026_to_2025, self.name2026 = build_task_index_maps(root, task_data_json)
|
| 140 |
+
acts = set(activities)
|
| 141 |
+
|
| 142 |
+
# episode metadata (filtered to our activities)
|
| 143 |
+
ep = pd.concat([pd.read_parquet(f) for f in sorted(
|
| 144 |
+
glob.glob(os.path.join(root, "meta", "episodes", "**", "*.parquet"), recursive=True))],
|
| 145 |
+
ignore_index=True)
|
| 146 |
+
ep["task0"] = ep["tasks"].apply(lambda v: v[0] if hasattr(v, "__len__") and not isinstance(v, str) else v)
|
| 147 |
+
ep = ep[ep["task0"].isin(acts)].reset_index(drop=True)
|
| 148 |
+
|
| 149 |
+
self.episodes: List[dict] = []
|
| 150 |
+
meta_eps: Dict[int, dict] = {}
|
| 151 |
+
samples: List[tuple] = []
|
| 152 |
+
for _, r in ep.iterrows():
|
| 153 |
+
E = int(r["episode_index"]); L = int(r["length"])
|
| 154 |
+
rec = {
|
| 155 |
+
"episode_index": E, "length": L, "task0": r["task0"],
|
| 156 |
+
"data": os.path.join(root, "data", f"chunk-{int(r['data/chunk_index']):03d}",
|
| 157 |
+
f"file-{int(r['data/file_index']):03d}.parquet"),
|
| 158 |
+
"video": {}, "from_ts": {},
|
| 159 |
+
}
|
| 160 |
+
for dst, src in self.RGB_KEYS.items():
|
| 161 |
+
rec["video"][dst] = os.path.join(
|
| 162 |
+
root, "videos", src,
|
| 163 |
+
f"chunk-{int(r[f'videos/{src}/chunk_index']):03d}",
|
| 164 |
+
f"file-{int(r[f'videos/{src}/file_index']):03d}.mp4")
|
| 165 |
+
rec["from_ts"][dst] = float(r[f"videos/{src}/from_timestamp"])
|
| 166 |
+
ei = len(self.episodes)
|
| 167 |
+
self.episodes.append(rec)
|
| 168 |
+
meta_eps[E] = {"length": L}
|
| 169 |
+
# only frames with a full future action window
|
| 170 |
+
for t in range(max(1, L - self.H)):
|
| 171 |
+
samples.append((ei, t))
|
| 172 |
+
self.samples = samples
|
| 173 |
+
self.meta = _V3Meta(meta_eps)
|
| 174 |
+
self._pq_cache: "OrderedDict[str, pd.DataFrame]" = OrderedDict()
|
| 175 |
+
self._pq_cache_max = parquet_cache
|
| 176 |
+
self._video_cache: "OrderedDict[str, av.container.InputContainer]" = OrderedDict()
|
| 177 |
+
self._video_cache_max = int(os.environ.get("B1K_VIDEO_CACHE_SIZE", "12"))
|
| 178 |
+
self._decode_resize = int(os.environ.get("B1K_DECODE_RESIZE", "224"))
|
| 179 |
+
self._frame_cache_dir = os.environ.get("B1K_FRAME_CACHE_DIR")
|
| 180 |
+
self._frame_cache_max_bytes = int(float(os.environ.get("B1K_FRAME_CACHE_MAX_GB", "4")) * (1024 ** 3))
|
| 181 |
+
self._frame_cache_prune_every = max(1, int(os.environ.get("B1K_FRAME_CACHE_PRUNE_EVERY", "2048")))
|
| 182 |
+
self._frame_cache_checks = 0
|
| 183 |
+
self._frame_mem_cache: "OrderedDict[tuple[str, int], np.ndarray]" = OrderedDict()
|
| 184 |
+
self._frame_mem_cache_bytes = 0
|
| 185 |
+
self._frame_mem_cache_max_bytes = int(float(os.environ.get("B1K_FRAME_MEM_CACHE_GB", "0")) * (1024 ** 3))
|
| 186 |
+
self._frame_cache_touch_disk = os.environ.get("B1K_FRAME_CACHE_TOUCH", "0") == "1"
|
| 187 |
+
if self._frame_cache_dir:
|
| 188 |
+
os.makedirs(self._frame_cache_dir, exist_ok=True)
|
| 189 |
+
if os.environ.get("B1K_FRAME_CACHE_PRUNE_ON_INIT", "0") == "1":
|
| 190 |
+
self._prune_frame_cache(force=True)
|
| 191 |
+
logger.info("BehaviorV3Dataset: %d episodes, %d frame-samples, %d activities",
|
| 192 |
+
len(self.episodes), len(self.samples), len(acts))
|
| 193 |
+
|
| 194 |
+
def __getstate__(self):
|
| 195 |
+
state = self.__dict__.copy()
|
| 196 |
+
state["_pq_cache"] = OrderedDict()
|
| 197 |
+
state["_video_cache"] = OrderedDict()
|
| 198 |
+
state["_frame_mem_cache"] = OrderedDict()
|
| 199 |
+
state["_frame_mem_cache_bytes"] = 0
|
| 200 |
+
return state
|
| 201 |
+
|
| 202 |
+
def __del__(self):
|
| 203 |
+
for container in getattr(self, "_video_cache", {}).values():
|
| 204 |
+
try:
|
| 205 |
+
container.close()
|
| 206 |
+
except Exception:
|
| 207 |
+
pass
|
| 208 |
+
|
| 209 |
+
def __len__(self):
|
| 210 |
+
return len(self.samples)
|
| 211 |
+
|
| 212 |
+
def _episode_frames(self, rec) -> pd.DataFrame:
|
| 213 |
+
"""Cached per-episode frame table (state, action, timestamp), sorted."""
|
| 214 |
+
key = rec["data"]
|
| 215 |
+
if key not in self._pq_cache:
|
| 216 |
+
df = pd.read_parquet(key, columns=["episode_index", "frame_index",
|
| 217 |
+
"observation.state", "action",
|
| 218 |
+
"timestamp", "task_index"])
|
| 219 |
+
self._pq_cache[key] = df
|
| 220 |
+
if len(self._pq_cache) > self._pq_cache_max:
|
| 221 |
+
self._pq_cache.popitem(last=False)
|
| 222 |
+
df = self._pq_cache[key]
|
| 223 |
+
sub = df[df["episode_index"] == rec["episode_index"]].sort_values("frame_index")
|
| 224 |
+
return sub
|
| 225 |
+
|
| 226 |
+
def _cached_container(self, path: str):
|
| 227 |
+
container = self._video_cache.get(path)
|
| 228 |
+
if container is not None:
|
| 229 |
+
self._video_cache.move_to_end(path)
|
| 230 |
+
return container
|
| 231 |
+
|
| 232 |
+
container = av.open(path)
|
| 233 |
+
# Cap ffmpeg decode threads per stream. HEVC's default thread_type=AUTO spawns up to ncores
|
| 234 |
+
# (240 here) threads PER container; with many workers x 6 containers/sample this blows past the
|
| 235 |
+
# kernel/cgroup thread ceiling ("can't start new thread"). A small fixed count is plenty since
|
| 236 |
+
# parallelism comes from the dataloader workers, not per-decode threads.
|
| 237 |
+
_dt = int(os.environ.get("B1K_DECODE_THREADS", "1"))
|
| 238 |
+
try:
|
| 239 |
+
vs0 = container.streams.video[0]
|
| 240 |
+
vs0.thread_count = _dt
|
| 241 |
+
vs0.thread_type = "NONE" if _dt <= 1 else "FRAME"
|
| 242 |
+
except Exception:
|
| 243 |
+
pass
|
| 244 |
+
self._video_cache[path] = container
|
| 245 |
+
if len(self._video_cache) > self._video_cache_max:
|
| 246 |
+
_, old = self._video_cache.popitem(last=False)
|
| 247 |
+
old.close()
|
| 248 |
+
return container
|
| 249 |
+
|
| 250 |
+
def _frame_cache_path(self, path: str, frame_idx: int) -> str | None:
|
| 251 |
+
if not self._frame_cache_dir:
|
| 252 |
+
return None
|
| 253 |
+
key = hashlib.blake2b(f"{path}|{frame_idx}|{self._decode_resize}".encode(), digest_size=16).hexdigest()
|
| 254 |
+
return os.path.join(self._frame_cache_dir, f"{key}.npy")
|
| 255 |
+
|
| 256 |
+
def _get_frame_mem_cache(self, key: tuple[str, int]) -> np.ndarray | None:
|
| 257 |
+
if self._frame_mem_cache_max_bytes <= 0:
|
| 258 |
+
return None
|
| 259 |
+
img = self._frame_mem_cache.get(key)
|
| 260 |
+
if img is None:
|
| 261 |
+
return None
|
| 262 |
+
self._frame_mem_cache.move_to_end(key)
|
| 263 |
+
return img
|
| 264 |
+
|
| 265 |
+
def _put_frame_mem_cache(self, key: tuple[str, int], img: np.ndarray) -> None:
|
| 266 |
+
if self._frame_mem_cache_max_bytes <= 0:
|
| 267 |
+
return
|
| 268 |
+
old = self._frame_mem_cache.pop(key, None)
|
| 269 |
+
if old is not None:
|
| 270 |
+
self._frame_mem_cache_bytes -= old.nbytes
|
| 271 |
+
self._frame_mem_cache[key] = img
|
| 272 |
+
self._frame_mem_cache_bytes += img.nbytes
|
| 273 |
+
while self._frame_mem_cache_bytes > self._frame_mem_cache_max_bytes and self._frame_mem_cache:
|
| 274 |
+
_, evicted = self._frame_mem_cache.popitem(last=False)
|
| 275 |
+
self._frame_mem_cache_bytes -= evicted.nbytes
|
| 276 |
+
|
| 277 |
+
def _prune_frame_cache(self, *, force: bool = False):
|
| 278 |
+
if not self._frame_cache_dir or self._frame_cache_max_bytes <= 0:
|
| 279 |
+
return
|
| 280 |
+
self._frame_cache_checks += 1
|
| 281 |
+
if not force and self._frame_cache_checks % self._frame_cache_prune_every:
|
| 282 |
+
return
|
| 283 |
+
files = []
|
| 284 |
+
total = 0
|
| 285 |
+
for p in glob.glob(os.path.join(self._frame_cache_dir, "*.npy")):
|
| 286 |
+
try:
|
| 287 |
+
st = os.stat(p)
|
| 288 |
+
except FileNotFoundError:
|
| 289 |
+
continue
|
| 290 |
+
total += st.st_size
|
| 291 |
+
files.append((st.st_mtime, st.st_size, p))
|
| 292 |
+
if total <= self._frame_cache_max_bytes:
|
| 293 |
+
return
|
| 294 |
+
for _, size, p in sorted(files):
|
| 295 |
+
try:
|
| 296 |
+
os.remove(p)
|
| 297 |
+
total -= size
|
| 298 |
+
except FileNotFoundError:
|
| 299 |
+
pass
|
| 300 |
+
if total <= int(self._frame_cache_max_bytes * 0.85):
|
| 301 |
+
break
|
| 302 |
+
|
| 303 |
+
def _decode_rgb(self, path: str, ts: float) -> np.ndarray:
|
| 304 |
+
frame_idx = int(round(ts * self.fps))
|
| 305 |
+
mem_key = (path, frame_idx)
|
| 306 |
+
cached_img = self._get_frame_mem_cache(mem_key)
|
| 307 |
+
if cached_img is not None:
|
| 308 |
+
return cached_img
|
| 309 |
+
cache_path = self._frame_cache_path(path, frame_idx)
|
| 310 |
+
if cache_path and os.path.exists(cache_path):
|
| 311 |
+
try:
|
| 312 |
+
if self._frame_cache_touch_disk:
|
| 313 |
+
os.utime(cache_path, None)
|
| 314 |
+
img = np.load(cache_path)
|
| 315 |
+
self._put_frame_mem_cache(mem_key, img)
|
| 316 |
+
return img
|
| 317 |
+
except Exception:
|
| 318 |
+
try:
|
| 319 |
+
os.remove(cache_path)
|
| 320 |
+
except FileNotFoundError:
|
| 321 |
+
pass
|
| 322 |
+
|
| 323 |
+
container = self._cached_container(path)
|
| 324 |
+
vs = container.streams.video[0]
|
| 325 |
+
container.seek(int(max(0.0, ts) / vs.time_base), stream=vs, backward=True)
|
| 326 |
+
frame = None
|
| 327 |
+
for fr in container.decode(vs):
|
| 328 |
+
if fr.time is not None and fr.time >= ts - 1e-3:
|
| 329 |
+
frame = fr
|
| 330 |
+
break
|
| 331 |
+
if frame is None: # ts past end — take last decoded
|
| 332 |
+
container.seek(int(max(0.0, ts) / vs.time_base), stream=vs, backward=True)
|
| 333 |
+
for fr in container.decode(vs):
|
| 334 |
+
frame = fr
|
| 335 |
+
if self._decode_resize > 0:
|
| 336 |
+
frame = frame.reformat(width=self._decode_resize, height=self._decode_resize, format="rgb24")
|
| 337 |
+
img = frame.to_ndarray(format="rgb24")
|
| 338 |
+
self._put_frame_mem_cache(mem_key, img)
|
| 339 |
+
if cache_path:
|
| 340 |
+
self._prune_frame_cache()
|
| 341 |
+
tmp = f"{cache_path}.{os.getpid()}.{time.time_ns()}.tmp"
|
| 342 |
+
try:
|
| 343 |
+
with open(tmp, "wb") as f:
|
| 344 |
+
np.save(f, img)
|
| 345 |
+
os.replace(tmp, cache_path)
|
| 346 |
+
except Exception:
|
| 347 |
+
try:
|
| 348 |
+
os.remove(tmp)
|
| 349 |
+
except FileNotFoundError:
|
| 350 |
+
pass
|
| 351 |
+
return img # HWC uint8
|
| 352 |
+
|
| 353 |
+
def __getitem__(self, i):
|
| 354 |
+
ei, t = self.samples[i]
|
| 355 |
+
rec = self.episodes[ei]
|
| 356 |
+
sub = self._episode_frames(rec)
|
| 357 |
+
states = np.stack(sub["observation.state"].to_numpy()) # [L,61]
|
| 358 |
+
actions = np.stack(sub["action"].to_numpy()) # [L,23]
|
| 359 |
+
ts = float(sub["timestamp"].iloc[t])
|
| 360 |
+
act_win = actions[t:t + self.H] # [H,23]
|
| 361 |
+
item = {
|
| 362 |
+
"observation.state": states[t].astype(np.float32), # raw 61-dim
|
| 363 |
+
"action": act_win.astype(np.float32),
|
| 364 |
+
"task_index": np.int64(self.idx2026_to_2025[int(sub["task_index"].iloc[t])]),
|
| 365 |
+
"timestamp": np.float32(ts),
|
| 366 |
+
"episode_index": np.int64(rec["episode_index"]),
|
| 367 |
+
"index": np.int64(i),
|
| 368 |
+
}
|
| 369 |
+
for dst in self.RGB_KEYS:
|
| 370 |
+
frame_ts = rec["from_ts"][dst] + t / self.fps
|
| 371 |
+
item[dst] = self._decode_rgb(rec["video"][dst], frame_ts) # HWC uint8
|
| 372 |
+
return item
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def create_v3_behavior_data_loader(config, root_2026: str, activities: List[str],
|
| 376 |
+
task_data_json: str, *, sharding=None,
|
| 377 |
+
shuffle: bool = True, num_workers: Optional[int] = None,
|
| 378 |
+
seed: int = 0):
|
| 379 |
+
"""Build a training data loader over the 2026 v3 subset, reusing the 2025
|
| 380 |
+
transform pipeline but with 2026 state extraction (B1kInputs2026)."""
|
| 381 |
+
import jax
|
| 382 |
+
import dataclasses as _dc
|
| 383 |
+
from b1k.policies import b1k_policy
|
| 384 |
+
from b1k.training.data_loader import transform_dataset, DataLoaderImpl
|
| 385 |
+
from openpi.training.data_loader import TorchDataLoader
|
| 386 |
+
|
| 387 |
+
data_config = config.data.create(config.assets_dirs, config.model)
|
| 388 |
+
# Swap the OmniGibson-dependent B1kInputs for the 2026 61-dim variant.
|
| 389 |
+
new_inputs = tuple(
|
| 390 |
+
B1kInputs2026(model_type=config.model.model_type)
|
| 391 |
+
if isinstance(x, b1k_policy.B1kInputs) else x
|
| 392 |
+
for x in data_config.data_transforms.inputs
|
| 393 |
+
)
|
| 394 |
+
data_config = _dc.replace(
|
| 395 |
+
data_config,
|
| 396 |
+
data_transforms=_dc.replace(data_config.data_transforms, inputs=new_inputs),
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
ds = BehaviorV3Dataset(root_2026, activities=activities,
|
| 400 |
+
action_horizon=config.model.action_horizon,
|
| 401 |
+
task_data_json=task_data_json, seed=seed)
|
| 402 |
+
ds = transform_dataset(ds, data_config) # adds dataset-aware subtask + per-ts norm
|
| 403 |
+
loader = TorchDataLoader(
|
| 404 |
+
ds,
|
| 405 |
+
local_batch_size=config.batch_size // jax.process_count(),
|
| 406 |
+
sharding=sharding, shuffle=shuffle,
|
| 407 |
+
num_workers=config.num_workers if num_workers is None else num_workers,
|
| 408 |
+
seed=seed,
|
| 409 |
+
)
|
| 410 |
+
return DataLoaderImpl(data_config, loader)
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
__all__ = ["BehaviorV3Dataset", "B1kInputs2026", "extract_state_2026",
|
| 414 |
+
"build_task_index_maps", "PROP_2026", "create_v3_behavior_data_loader"]
|
pibehavior_da3_clean_up_your_desk_40k/source_code/training/b1k_da3.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DA3 spatial inputs for the 2026 v3 pipeline.
|
| 2 |
+
|
| 3 |
+
Extends BehaviorV3Dataset with per-frame DA3 inputs (3 cams at DA3 resolution + robot->cam
|
| 4 |
+
OpenCV extrinsics + intrinsics + ModernBERT task-language), and provides a data-loader factory
|
| 5 |
+
that runs the frozen DA3-GIANT extractor once per BATCH (GPU) via the loader's batch hook.
|
| 6 |
+
|
| 7 |
+
Geometry (empirically calibrated against GT depth, see /work/jack/behavior1k/calib):
|
| 8 |
+
* robot2cam_pose[7] = [pos(3), quat_wxyz(4)] = the CAMERA POSE IN THE ROBOT FRAME,
|
| 9 |
+
already OpenCV-convention (+Z optical axis). robot->cam = inv(pose_matrix).
|
| 10 |
+
* intrinsics: fx = fy = W * 17.0/20.995 (OmniGibson VisionSensor defaults), cx=cy=W/2.
|
| 11 |
+
"""
|
| 12 |
+
import logging
|
| 13 |
+
import os
|
| 14 |
+
import pickle
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
from b1k.training.b1k_2026 import BehaviorV3Dataset, B1kInputs2026
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
FOCAL_RATIO = 17.0 / 20.995 # OmniGibson VisionSensor default focal/aperture
|
| 23 |
+
|
| 24 |
+
# dst rgb key -> pose parquet column (same camera)
|
| 25 |
+
POSE_COLS = {
|
| 26 |
+
"observation.images.rgb.head": "observation.robot2cam_pose.zed_link_camera_0",
|
| 27 |
+
"observation.images.rgb.left_wrist": "observation.robot2cam_pose.left_realsense_link_camera_0",
|
| 28 |
+
"observation.images.rgb.right_wrist": "observation.robot2cam_pose.right_realsense_link_camera_0",
|
| 29 |
+
}
|
| 30 |
+
# view order MUST match the bank builder: 0=main(head), 1=left, 2=right
|
| 31 |
+
VIEW_ORDER = (
|
| 32 |
+
"observation.images.rgb.head",
|
| 33 |
+
"observation.images.rgb.left_wrist",
|
| 34 |
+
"observation.images.rgb.right_wrist",
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def quat_wxyz_to_R(q):
|
| 39 |
+
w, x, y, z = q / (np.linalg.norm(q) + 1e-12)
|
| 40 |
+
return np.array([
|
| 41 |
+
[1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y)],
|
| 42 |
+
[2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x)],
|
| 43 |
+
[2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)],
|
| 44 |
+
])
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# OmniGibson cameras use the OpenGL optical convention (-Z forward, +Y up); the DA3 pinhole
|
| 48 |
+
# projection assumes OpenCV (+Z forward, +Y down). This diag(1,-1,-1) flips the camera Y/Z axes.
|
| 49 |
+
# WITHOUT it, cross-view GT-depth reprojection is 0.00 (cameras point the wrong way); WITH it, 0.20+
|
| 50 |
+
# (best of all 8 conventions), and the head camera lands at its true +1.56 m height. (calib/calibrate_v3.py)
|
| 51 |
+
_GL2CV = np.diag([1.0, -1.0, -1.0, 1.0]).astype(np.float32)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def pose7_to_robot2cam(p7: np.ndarray) -> np.ndarray:
|
| 55 |
+
"""[pos3, quat_wxyz] camera-pose-in-robot-frame -> 4x4 robot->cam (OpenCV).
|
| 56 |
+
|
| 57 |
+
Convention (validated in calib/): quat=wxyz, pose is the CAMERA-IN-ROBOT transform so
|
| 58 |
+
robot->cam = inv(T), then GL->CV optical flip.
|
| 59 |
+
"""
|
| 60 |
+
T = np.eye(4, dtype=np.float32)
|
| 61 |
+
T[:3, :3] = quat_wxyz_to_R(np.asarray(p7[3:], np.float64))
|
| 62 |
+
T[:3, 3] = p7[:3]
|
| 63 |
+
return (_GL2CV @ np.linalg.inv(T)).astype(np.float32)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class BehaviorV3DA3Dataset(BehaviorV3Dataset):
|
| 67 |
+
"""BehaviorV3Dataset + DA3 inputs (frames @ da3_hw, extrinsics, intrinsics, task language)."""
|
| 68 |
+
|
| 69 |
+
def __init__(self, *args, da3_hw=(252, 252), lang_cache: str | None = None, lang_max_len: int = 32, **kwargs):
|
| 70 |
+
super().__init__(*args, **kwargs)
|
| 71 |
+
self._da3_hw = tuple(da3_hw)
|
| 72 |
+
self._lang_max_len = int(lang_max_len)
|
| 73 |
+
self._lang = None
|
| 74 |
+
if lang_cache:
|
| 75 |
+
with open(lang_cache, "rb") as f:
|
| 76 |
+
self._lang = pickle.load(f)
|
| 77 |
+
logger.info("DA3 lang cache: %d tasks from %s", len(self._lang), lang_cache)
|
| 78 |
+
|
| 79 |
+
def _episode_poses(self, rec):
|
| 80 |
+
"""Cached per-episode pose table (the base reader's parquet cache omits pose columns)."""
|
| 81 |
+
import pandas as pd
|
| 82 |
+
if not hasattr(self, "_pose_cache"):
|
| 83 |
+
from collections import OrderedDict
|
| 84 |
+
self._pose_cache = OrderedDict()
|
| 85 |
+
key = rec["data"]
|
| 86 |
+
if key not in self._pose_cache:
|
| 87 |
+
df = pd.read_parquet(key, columns=["episode_index", "frame_index", *POSE_COLS.values()])
|
| 88 |
+
self._pose_cache[key] = df
|
| 89 |
+
if len(self._pose_cache) > 8:
|
| 90 |
+
self._pose_cache.popitem(last=False)
|
| 91 |
+
df = self._pose_cache[key]
|
| 92 |
+
return df[df["episode_index"] == rec["episode_index"]].sort_values("frame_index")
|
| 93 |
+
|
| 94 |
+
def _decode_native(self, path: str, ts: float):
|
| 95 |
+
"""Decode ONE frame at NATIVE resolution, cached by (path, frame_idx). Returns (HWC uint8, native_w).
|
| 96 |
+
Shared by the base (224) and DA3 (252) decode so each frame is decoded ONCE, not twice.
|
| 97 |
+
Enabled only when B1K_SHARED_DECODE=1 (default off => original two-decode behavior)."""
|
| 98 |
+
if not hasattr(self, "_native_cache"):
|
| 99 |
+
from collections import OrderedDict
|
| 100 |
+
self._native_cache = OrderedDict()
|
| 101 |
+
fidx = int(round(ts * self.fps))
|
| 102 |
+
key = (path, fidx)
|
| 103 |
+
hit = self._native_cache.get(key)
|
| 104 |
+
if hit is not None:
|
| 105 |
+
self._native_cache.move_to_end(key)
|
| 106 |
+
return hit
|
| 107 |
+
container = self._cached_container(path)
|
| 108 |
+
vs = container.streams.video[0]
|
| 109 |
+
container.seek(int(max(0.0, ts) / vs.time_base), stream=vs, backward=True)
|
| 110 |
+
frame = None
|
| 111 |
+
for fr in container.decode(vs):
|
| 112 |
+
if fr.time is not None and fr.time >= ts - 1e-3:
|
| 113 |
+
frame = fr
|
| 114 |
+
break
|
| 115 |
+
if frame is None:
|
| 116 |
+
container.seek(int(max(0.0, ts) / vs.time_base), stream=vs, backward=True)
|
| 117 |
+
for fr in container.decode(vs):
|
| 118 |
+
frame = fr
|
| 119 |
+
out = (frame.to_ndarray(format="rgb24"), int(frame.width)) # native HWC uint8, no resize
|
| 120 |
+
self._native_cache[key] = out
|
| 121 |
+
if len(self._native_cache) > 12: # a few cams x a couple frames in flight
|
| 122 |
+
self._native_cache.popitem(last=False)
|
| 123 |
+
return out
|
| 124 |
+
|
| 125 |
+
def _decode_rgb(self, path: str, ts: float) -> np.ndarray:
|
| 126 |
+
"""Base VLM (224) frame. With shared-decode, resize from the single native decode (no 2nd decode)."""
|
| 127 |
+
if os.environ.get("B1K_SHARED_DECODE") != "1":
|
| 128 |
+
return super()._decode_rgb(path, ts)
|
| 129 |
+
import cv2
|
| 130 |
+
native, _ = self._decode_native(path, ts)
|
| 131 |
+
r = self._decode_resize
|
| 132 |
+
return cv2.resize(native, (r, r), interpolation=cv2.INTER_AREA) if r > 0 else native
|
| 133 |
+
|
| 134 |
+
def _decode_da3(self, path: str, ts: float) -> np.ndarray:
|
| 135 |
+
"""Decode one frame at DA3 resolution (252). HWC uint8 + native width (for intrinsics)."""
|
| 136 |
+
if os.environ.get("B1K_SHARED_DECODE") == "1":
|
| 137 |
+
import cv2
|
| 138 |
+
native, native_w = self._decode_native(path, ts) # reuses the base decode (no 2nd decode)
|
| 139 |
+
h, w = self._da3_hw
|
| 140 |
+
return cv2.resize(native, (w, h), interpolation=cv2.INTER_AREA), native_w
|
| 141 |
+
container = self._cached_container(path)
|
| 142 |
+
vs = container.streams.video[0]
|
| 143 |
+
container.seek(int(max(0.0, ts) / vs.time_base), stream=vs, backward=True)
|
| 144 |
+
frame = None
|
| 145 |
+
for fr in container.decode(vs):
|
| 146 |
+
if fr.time is not None and fr.time >= ts - 1e-3:
|
| 147 |
+
frame = fr
|
| 148 |
+
break
|
| 149 |
+
if frame is None:
|
| 150 |
+
container.seek(int(max(0.0, ts) / vs.time_base), stream=vs, backward=True)
|
| 151 |
+
for fr in container.decode(vs):
|
| 152 |
+
frame = fr
|
| 153 |
+
h, w = self._da3_hw
|
| 154 |
+
native_w = frame.width
|
| 155 |
+
frame = frame.reformat(width=w, height=h, format="rgb24")
|
| 156 |
+
img = frame.to_ndarray(format="rgb24")
|
| 157 |
+
return img, native_w
|
| 158 |
+
|
| 159 |
+
def _lang_entry(self, task_name: str):
|
| 160 |
+
if self._lang is None:
|
| 161 |
+
L = self._lang_max_len
|
| 162 |
+
return np.zeros((L, 1024), np.float32), np.zeros((L,), bool)
|
| 163 |
+
feat, mask = self._lang[task_name]
|
| 164 |
+
return np.asarray(feat, np.float32), np.asarray(mask, bool)
|
| 165 |
+
|
| 166 |
+
def da3_fields(self, i):
|
| 167 |
+
"""Compute ONLY the DA3 input fields for sample i (attached AFTER the transform stack,
|
| 168 |
+
which constructs fresh dicts and would drop unknown keys)."""
|
| 169 |
+
item = {}
|
| 170 |
+
ei, t = self.samples[i]
|
| 171 |
+
rec = self.episodes[ei]
|
| 172 |
+
sub = self._episode_poses(rec)
|
| 173 |
+
|
| 174 |
+
h, w = self._da3_hw
|
| 175 |
+
imgs, extr, intr = [], [], []
|
| 176 |
+
for dst in VIEW_ORDER:
|
| 177 |
+
frame_ts = rec["from_ts"][dst] + t / self.fps
|
| 178 |
+
img, native_w = self._decode_da3(rec["video"][dst], frame_ts)
|
| 179 |
+
imgs.append(img)
|
| 180 |
+
p7 = np.asarray(sub[POSE_COLS[dst]].iloc[t], np.float64)
|
| 181 |
+
extr.append(pose7_to_robot2cam(p7))
|
| 182 |
+
f_native = FOCAL_RATIO * native_w
|
| 183 |
+
# native (square) -> da3_hw rescale: fx,cx scale by w/native_w; fy,cy by h/native_h(=native_w)
|
| 184 |
+
K = np.array([
|
| 185 |
+
[f_native * w / native_w, 0, (native_w / 2) * w / native_w],
|
| 186 |
+
[0, f_native * h / native_w, (native_w / 2) * h / native_w],
|
| 187 |
+
[0, 0, 1],
|
| 188 |
+
], np.float32)
|
| 189 |
+
intr.append(K)
|
| 190 |
+
|
| 191 |
+
item["da3_images"] = np.stack(imgs, 0) # [V,252,252,3] uint8
|
| 192 |
+
item["camera_extrinsics"] = np.stack(extr, 0) # [V,4,4] robot->cam OpenCV
|
| 193 |
+
item["camera_intrinsics"] = np.stack(intr, 0) # [V,3,3] @ da3_hw
|
| 194 |
+
lf, lm = self._lang_entry(rec["task0"])
|
| 195 |
+
item["lang_feat"] = lf
|
| 196 |
+
item["lang_mask"] = lm
|
| 197 |
+
return item
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class _AttachDA3Fields:
|
| 201 |
+
"""Wraps the TRANSFORMED dataset; merges the raw dataset's DA3 fields into each sample."""
|
| 202 |
+
|
| 203 |
+
def __init__(self, transformed, raw: BehaviorV3DA3Dataset):
|
| 204 |
+
self._transformed = transformed
|
| 205 |
+
self._raw = raw
|
| 206 |
+
|
| 207 |
+
def __len__(self):
|
| 208 |
+
return len(self._transformed)
|
| 209 |
+
|
| 210 |
+
def __getitem__(self, i):
|
| 211 |
+
out = dict(self._transformed[i])
|
| 212 |
+
out.update(self._raw.da3_fields(i))
|
| 213 |
+
return out
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def create_v3_behavior_da3_loader(config, root_2026, activities, task_data_json, *,
|
| 217 |
+
lang_cache, sharding=None, shuffle=True,
|
| 218 |
+
num_workers=None, seed=0, da3_hw=(252, 252)):
|
| 219 |
+
"""v3 loader with DA3 inputs + a per-batch frozen DA3-GIANT extraction hook (GPU)."""
|
| 220 |
+
import jax
|
| 221 |
+
import dataclasses as _dc
|
| 222 |
+
from b1k.policies import b1k_policy
|
| 223 |
+
from b1k.training.data_loader import transform_dataset, DataLoaderImpl
|
| 224 |
+
from openpi.training.data_loader import TorchDataLoader
|
| 225 |
+
from b1k.training import da3_extractor as _ex
|
| 226 |
+
|
| 227 |
+
data_config = config.data.create(config.assets_dirs, config.model)
|
| 228 |
+
new_inputs = tuple(
|
| 229 |
+
B1kInputs2026(model_type=config.model.model_type)
|
| 230 |
+
if isinstance(x, b1k_policy.B1kInputs) else x
|
| 231 |
+
for x in data_config.data_transforms.inputs
|
| 232 |
+
)
|
| 233 |
+
data_config = _dc.replace(
|
| 234 |
+
data_config, data_transforms=_dc.replace(data_config.data_transforms, inputs=new_inputs))
|
| 235 |
+
|
| 236 |
+
ds = BehaviorV3DA3Dataset(
|
| 237 |
+
root_2026, activities=activities, action_horizon=config.model.action_horizon,
|
| 238 |
+
task_data_json=task_data_json, seed=seed,
|
| 239 |
+
da3_hw=da3_hw, lang_cache=lang_cache,
|
| 240 |
+
lang_max_len=config.model.da3.lang_max_len,
|
| 241 |
+
)
|
| 242 |
+
tds = transform_dataset(ds, data_config)
|
| 243 |
+
tds = _AttachDA3Fields(tds, ds)
|
| 244 |
+
|
| 245 |
+
logger.info("Building inline DA3-GIANT extractor (da3_hw=%s) ...", da3_hw)
|
| 246 |
+
# Extraction devices: default single-GPU (cuda:0). Set B1K_EXTRACT_DEVICES to spread the frozen
|
| 247 |
+
# DA3-GIANT forward across GPUs (one replica per device, batch split, run concurrently) so the
|
| 248 |
+
# ~2.7s single-GPU extraction shrinks and better overlaps the JAX train step.
|
| 249 |
+
_dev_env = os.environ.get("B1K_EXTRACT_DEVICES", "").strip()
|
| 250 |
+
_devices = [d.strip() for d in _dev_env.split(",") if d.strip()] or None
|
| 251 |
+
_fchunk = int(os.environ.get("B1K_DA3_FWD_CHUNK", "16"))
|
| 252 |
+
logger.info("DA3 extractor: devices=%s forward_chunk=%d", _devices or ["cuda:0"], _fchunk)
|
| 253 |
+
extractor = _ex.DA3InlineExtractor(da3_hw=da3_hw, forward_chunk=_fchunk, devices=_devices)
|
| 254 |
+
|
| 255 |
+
# DLPack GPU->GPU handoff: skip the ~2.6s/batch host round-trip by moving extractor features
|
| 256 |
+
# straight from the extraction GPUs to the training GPUs over NVLink. Requires CUDA extraction
|
| 257 |
+
# devices whose count matches the training mesh size (contiguous batch split aligns 1:1).
|
| 258 |
+
# Holds the last few batches' torch source shards alive so the async NVLink copies (device_put)
|
| 259 |
+
# can never read freed memory — replaces a blocking block_until_ready that serialized the producer.
|
| 260 |
+
import collections as _collections
|
| 261 |
+
_keepalive = _collections.deque(maxlen=4)
|
| 262 |
+
|
| 263 |
+
def _dlpack_ok():
|
| 264 |
+
try:
|
| 265 |
+
m = getattr(sharding, "mesh", None)
|
| 266 |
+
return (os.environ.get("B1K_DLPACK") == "1" and m is not None
|
| 267 |
+
and len(list(m.devices.flat)) == len(extractor.devices)
|
| 268 |
+
and all(str(d).startswith("cuda") for d in extractor.devices))
|
| 269 |
+
except Exception:
|
| 270 |
+
return False
|
| 271 |
+
|
| 272 |
+
def batch_transform(batch):
|
| 273 |
+
if _dlpack_ok():
|
| 274 |
+
import jax
|
| 275 |
+
parts = extractor.extract_shards_torch(
|
| 276 |
+
batch["da3_images"], batch["camera_extrinsics"], batch["camera_intrinsics"])
|
| 277 |
+
tdevs = list(sharding.mesh.devices.flat) # training devices, batch-chunk k -> tdevs[k]
|
| 278 |
+
|
| 279 |
+
def _asm(fi): # assemble per-shard torch tensors (field fi) into one sharded jax array
|
| 280 |
+
js = [jax.device_put(jax.dlpack.from_dlpack(parts[k][fi]), tdevs[k]) for k in range(len(parts))]
|
| 281 |
+
gshape = (sum(int(s.shape[0]) for s in js),) + tuple(int(d) for d in js[0].shape[1:])
|
| 282 |
+
return jax.make_array_from_single_device_arrays(gshape, sharding, js)
|
| 283 |
+
|
| 284 |
+
fa, ra, da = _asm(0), _asm(1), _asm(2)
|
| 285 |
+
# Do NOT block here: the device_put queues behind the in-flight train step on the target
|
| 286 |
+
# GPUs, so blocking would serialize the producer with training (killing the overlap).
|
| 287 |
+
# Instead keep the torch source shards referenced for a few batches so the async NVLink
|
| 288 |
+
# copy can't read freed memory.
|
| 289 |
+
_keepalive.append(parts)
|
| 290 |
+
batch["da3_features"], batch["da3_ray"], batch["da3_depth"] = fa, ra, da
|
| 291 |
+
else:
|
| 292 |
+
feats, ray, depth = extractor.extract(
|
| 293 |
+
batch["da3_images"], batch["camera_extrinsics"], batch["camera_intrinsics"])
|
| 294 |
+
batch["da3_features"] = feats # uint16 bf16-bits
|
| 295 |
+
batch["da3_ray"] = ray
|
| 296 |
+
batch["da3_depth"] = depth
|
| 297 |
+
batch.pop("da3_images", None)
|
| 298 |
+
batch.pop("camera_intrinsics", None)
|
| 299 |
+
return batch
|
| 300 |
+
|
| 301 |
+
loader = TorchDataLoader(
|
| 302 |
+
tds, local_batch_size=config.batch_size // jax.process_count(),
|
| 303 |
+
sharding=sharding, shuffle=shuffle,
|
| 304 |
+
num_workers=config.num_workers if num_workers is None else num_workers,
|
| 305 |
+
seed=seed, batch_transform=batch_transform,
|
| 306 |
+
)
|
| 307 |
+
return DataLoaderImpl(data_config, loader)
|
pibehavior_da3_clean_up_your_desk_40k/source_code/training/da3_extractor.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inline DA3-GIANT feature extractor (PyTorch, runs in the openpi venv).
|
| 2 |
+
|
| 3 |
+
Produces the SAME feats/ray/depth as the offline cache, but on-the-fly at train time — so
|
| 4 |
+
no precached features are needed (essential for datasets where caching is infeasible, e.g.
|
| 5 |
+
b1k at 30 Hz would be ~3 PB). Frozen, no_grad; the output tensors are handed to the JAX
|
| 6 |
+
model via dlpack.
|
| 7 |
+
|
| 8 |
+
depth_anything_3.api transitively imports rendering/SfM utils (moviepy/gsplat/pycolmap/
|
| 9 |
+
trimesh/evo) that (a) aren't needed for feature extraction and (b) pin numpy<2 (conflicts
|
| 10 |
+
with openpi's numpy 2.x). We stub those modules so nothing gets installed/downgraded.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import concurrent.futures
|
| 14 |
+
import contextlib
|
| 15 |
+
import importlib.util
|
| 16 |
+
import sys
|
| 17 |
+
import types
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
# The frozen extractor is pure GPU work; torch's CPU thread pools only add GIL/scheduler contention
|
| 23 |
+
# with the JAX train loop in the same process (~1000 threads observed). Pin them to 1.
|
| 24 |
+
try:
|
| 25 |
+
torch.set_num_threads(1)
|
| 26 |
+
torch.set_num_interop_threads(1) # only settable before any parallel work; ignore if already set
|
| 27 |
+
except Exception: # noqa: BLE001
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _to_dev(a, device, dtype=None):
|
| 32 |
+
"""Host array/tensor -> device tensor. A torch tensor (pinned by the DataLoader when
|
| 33 |
+
B1K_TORCH_COLLATE=1) is copied with non_blocking=True so the H2D is an async DMA that overlaps
|
| 34 |
+
compute, instead of a synchronous pageable copy. Numpy input keeps the original behavior."""
|
| 35 |
+
if isinstance(a, torch.Tensor):
|
| 36 |
+
t = a.to(device, non_blocking=True)
|
| 37 |
+
return t.to(dtype) if dtype is not None and t.dtype != dtype else t
|
| 38 |
+
return torch.as_tensor(a, device=device, dtype=dtype)
|
| 39 |
+
|
| 40 |
+
_DA3_SRC = "/work/jack/projects/Depth-Anything-3/src"
|
| 41 |
+
_GEOSTACK = "/work/jack/da3xvla_src/DA3-XVLA-cache/models/da3_for_geostack.py"
|
| 42 |
+
_IMAGENET_MEAN = torch.tensor([0.485, 0.456, 0.406]).view(1, 1, 3, 1, 1)
|
| 43 |
+
_IMAGENET_STD = torch.tensor([0.229, 0.224, 0.225]).view(1, 1, 3, 1, 1)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class _AutoStub(types.ModuleType):
|
| 47 |
+
"""Module stub that returns a dummy callable for any non-dunder attribute + acts as a package."""
|
| 48 |
+
|
| 49 |
+
__path__: list = []
|
| 50 |
+
|
| 51 |
+
def __getattr__(self, name):
|
| 52 |
+
if name.startswith("__"):
|
| 53 |
+
raise AttributeError(name)
|
| 54 |
+
return lambda *a, **k: None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _install_stubs():
|
| 58 |
+
for name in ("moviepy", "moviepy.editor", "gsplat", "pycolmap", "trimesh",
|
| 59 |
+
"depth_anything_3.utils.export", "depth_anything_3.utils.pose_align"):
|
| 60 |
+
sys.modules.setdefault(name, _AutoStub(name))
|
| 61 |
+
if hasattr(sys.modules["moviepy"], "__dict__"):
|
| 62 |
+
sys.modules["moviepy"].editor = sys.modules["moviepy.editor"]
|
| 63 |
+
if _DA3_SRC not in sys.path:
|
| 64 |
+
sys.path.insert(0, _DA3_SRC)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _load_da3_class():
|
| 68 |
+
_install_stubs()
|
| 69 |
+
spec = importlib.util.spec_from_file_location("_da3_for_geostack", _GEOSTACK)
|
| 70 |
+
mod = importlib.util.module_from_spec(spec)
|
| 71 |
+
spec.loader.exec_module(mod)
|
| 72 |
+
return mod.DA3LargeForGeoStack
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def rescale_intrinsics(intr: np.ndarray, src_hw, dst_hw) -> np.ndarray:
|
| 76 |
+
"""Rescale pixel K [.,3,3] from src (H,W) to dst (H,W). fx,cx by W-ratio; fy,cy by H-ratio."""
|
| 77 |
+
sh, sw = src_hw
|
| 78 |
+
dh, dw = dst_hw
|
| 79 |
+
rw, rh = dw / sw, dh / sh
|
| 80 |
+
out = np.array(intr, dtype=np.float32, copy=True)
|
| 81 |
+
out[..., 0, 0] *= rw
|
| 82 |
+
out[..., 0, 2] *= rw
|
| 83 |
+
out[..., 1, 1] *= rh
|
| 84 |
+
out[..., 1, 2] *= rh
|
| 85 |
+
return out
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class DA3InlineExtractor:
|
| 89 |
+
"""Frozen DA3-GIANT posed multi-view extractor, REPLICATED one-per-GPU.
|
| 90 |
+
|
| 91 |
+
Each visible GPU holds its own frozen DA3-GIANT copy and runs the forward on ONLY its slice of the
|
| 92 |
+
batch, all GPUs concurrently (one thread per device; CUDA kernels are async per device, and torch
|
| 93 |
+
releases the GIL during them). This removes the single-GPU serial bottleneck of the old design, so
|
| 94 |
+
inline extraction scales with GPU count to match the JAX data-parallel training step.
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
def __init__(
|
| 98 |
+
self,
|
| 99 |
+
model_name: str = "depth-anything/DA3NESTED-GIANT-LARGE-1.1",
|
| 100 |
+
out_layers=(19, 26, 33, 39),
|
| 101 |
+
da3_hw=(252, 336),
|
| 102 |
+
devices=None,
|
| 103 |
+
forward_chunk: int = 16,
|
| 104 |
+
):
|
| 105 |
+
DA3 = _load_da3_class()
|
| 106 |
+
if devices is None:
|
| 107 |
+
# Default SINGLE-GPU (cuda:0): the multi-replica path is correct but thread-based, and the
|
| 108 |
+
# Python GIL serializes the DA3 forward's many kernel launches (~18% concurrency efficiency),
|
| 109 |
+
# so replicating across GPUs doesn't speed it up — it only wastes memory. Pass `devices`
|
| 110 |
+
# explicitly (e.g. for a future multiprocess extractor) to override.
|
| 111 |
+
devices = ["cuda:0" if torch.cuda.is_available() else "cpu"]
|
| 112 |
+
self.devices = list(devices)
|
| 113 |
+
self.da3_hw = tuple(da3_hw)
|
| 114 |
+
self.forward_chunk = int(forward_chunk)
|
| 115 |
+
self.replicas = []
|
| 116 |
+
for dev in self.devices:
|
| 117 |
+
m = (
|
| 118 |
+
DA3(model_name=model_name, out_layers=tuple(out_layers),
|
| 119 |
+
da3_input_h=da3_hw[0], da3_input_w=da3_hw[1], patch_size=14, use_bf16=True)
|
| 120 |
+
.to(dev)
|
| 121 |
+
.eval()
|
| 122 |
+
)
|
| 123 |
+
for p in m.parameters():
|
| 124 |
+
p.requires_grad_(False)
|
| 125 |
+
self.replicas.append(m)
|
| 126 |
+
self._pool = concurrent.futures.ThreadPoolExecutor(max_workers=max(1, len(self.devices)))
|
| 127 |
+
|
| 128 |
+
def _preprocess(self, images: np.ndarray, device) -> torch.Tensor:
|
| 129 |
+
"""images: [B,V,H,W,3] uint8 (or float [0,1]) -> [B,V,3,252,336] ImageNet-normalized on `device`."""
|
| 130 |
+
x = _to_dev(images, device)
|
| 131 |
+
if x.dtype == torch.uint8:
|
| 132 |
+
x = x.float() / 255.0
|
| 133 |
+
elif x.max() > 1.5: # already float but in [0,255]
|
| 134 |
+
x = x.float() / 255.0
|
| 135 |
+
x = x.permute(0, 1, 4, 2, 3) # [B,V,3,H,W]
|
| 136 |
+
x = torch.nn.functional.interpolate(
|
| 137 |
+
x.flatten(0, 1), size=self.da3_hw, mode="bicubic", align_corners=False, antialias=True
|
| 138 |
+
).view(*x.shape[:2], 3, *self.da3_hw)
|
| 139 |
+
x = (x - _IMAGENET_MEAN.to(x)) / _IMAGENET_STD.to(x)
|
| 140 |
+
return x
|
| 141 |
+
|
| 142 |
+
def _run_shard(self, di: int, images: np.ndarray, extrinsics: np.ndarray, intrinsics: np.ndarray,
|
| 143 |
+
return_torch: bool = False):
|
| 144 |
+
"""Run replica `di` over its (pre-sliced) shard, in chunks to bound activation memory.
|
| 145 |
+
return_torch=True keeps the result as torch tensors ON cuda:di (no host round-trip) for the
|
| 146 |
+
DLPack handoff; default returns numpy (host) as before."""
|
| 147 |
+
dev = self.devices[di]
|
| 148 |
+
dev_idx = int(dev.split(":")[1]) if ":" in dev else None
|
| 149 |
+
replica = self.replicas[di]
|
| 150 |
+
chunk = self.forward_chunk
|
| 151 |
+
fc, rc, dc = [], [], []
|
| 152 |
+
ctx = torch.cuda.device(dev_idx) if dev_idx is not None else contextlib.nullcontext()
|
| 153 |
+
with ctx, torch.no_grad(): # set current device so implicit-device tensors land on the right GPU
|
| 154 |
+
for i in range(0, images.shape[0], chunk):
|
| 155 |
+
x = self._preprocess(images[i : i + chunk], dev)
|
| 156 |
+
e = _to_dev(extrinsics[i : i + chunk], dev, torch.float32)
|
| 157 |
+
k = _to_dev(intrinsics[i : i + chunk], dev, torch.float32)
|
| 158 |
+
out = replica.forward_multi_view(x, extrinsics=e, intrinsics=k)
|
| 159 |
+
# Ship feats as bf16 BITS (uint16): the model casts to bf16 anyway (see
|
| 160 |
+
# Pi0._compute_banks), so this is numerically identical to shipping f32 while
|
| 161 |
+
# halving the GPU->CPU->GPU transfer and the collate copies.
|
| 162 |
+
feats = torch.stack(list(out["feats"]), dim=1).to(torch.bfloat16) # [b,4,V,C,h,w]
|
| 163 |
+
ray = out["ray"].float() # [b,V,3,h,w]
|
| 164 |
+
depth = out["depth"]
|
| 165 |
+
if depth is None:
|
| 166 |
+
depth = torch.zeros(ray.shape[0], ray.shape[1], 1, ray.shape[3], ray.shape[4], device=ray.device)
|
| 167 |
+
if return_torch:
|
| 168 |
+
fc.append(feats.view(torch.uint16)); rc.append(ray); dc.append(depth.float())
|
| 169 |
+
else:
|
| 170 |
+
fc.append(feats.view(torch.uint16).cpu().numpy())
|
| 171 |
+
rc.append(ray.cpu().numpy())
|
| 172 |
+
dc.append(depth.float().cpu().numpy())
|
| 173 |
+
if return_torch:
|
| 174 |
+
return torch.cat(fc, 0), torch.cat(rc, 0), torch.cat(dc, 0) # torch tensors on cuda:di
|
| 175 |
+
return np.concatenate(fc, 0), np.concatenate(rc, 0), np.concatenate(dc, 0)
|
| 176 |
+
|
| 177 |
+
def extract_shards_torch(self, images: np.ndarray, extrinsics: np.ndarray, intrinsics: np.ndarray):
|
| 178 |
+
"""Like extract() but returns per-shard torch GPU tensors (feats,ray,depth) each on cuda:di,
|
| 179 |
+
WITHOUT the host round-trip. The caller hands them to JAX via DLPack (GPU->GPU over NVLink).
|
| 180 |
+
Returns a list of len(devices) tuples, shard k on self.devices[k]."""
|
| 181 |
+
b = int(images.shape[0])
|
| 182 |
+
nd = len(self.devices)
|
| 183 |
+
bounds = [round(i * b / nd) for i in range(nd + 1)]
|
| 184 |
+
futs = {}
|
| 185 |
+
for di in range(nd):
|
| 186 |
+
s, e = bounds[di], bounds[di + 1]
|
| 187 |
+
if s >= e:
|
| 188 |
+
continue
|
| 189 |
+
futs[di] = self._pool.submit(self._run_shard, di, images[s:e], extrinsics[s:e], intrinsics[s:e], True)
|
| 190 |
+
return [futs[di].result() for di in sorted(futs)]
|
| 191 |
+
|
| 192 |
+
def extract(self, images: np.ndarray, extrinsics: np.ndarray, intrinsics: np.ndarray):
|
| 193 |
+
"""images [B,V,H,W,3]; extrinsics [B,V,4,4] w2c; intrinsics [B,V,3,3] AT 252x336.
|
| 194 |
+
|
| 195 |
+
Splits the batch across all replicas/GPUs and runs the forwards concurrently.
|
| 196 |
+
Returns numpy: feats [B,4,V,1536,18,24] f32, ray [B,V,3,18,24] f32, depth [B,V,1,18,24] f32.
|
| 197 |
+
"""
|
| 198 |
+
b = int(images.shape[0])
|
| 199 |
+
nd = len(self.devices)
|
| 200 |
+
bounds = [round(i * b / nd) for i in range(nd + 1)]
|
| 201 |
+
futs = {}
|
| 202 |
+
for di in range(nd):
|
| 203 |
+
s, e = bounds[di], bounds[di + 1]
|
| 204 |
+
if s >= e:
|
| 205 |
+
continue
|
| 206 |
+
futs[di] = self._pool.submit(self._run_shard, di, images[s:e], extrinsics[s:e], intrinsics[s:e])
|
| 207 |
+
parts = [futs[di].result() for di in sorted(futs)]
|
| 208 |
+
return (
|
| 209 |
+
np.concatenate([p[0] for p in parts], axis=0),
|
| 210 |
+
np.concatenate([p[1] for p in parts], axis=0),
|
| 211 |
+
np.concatenate([p[2] for p in parts], axis=0),
|
| 212 |
+
)
|
pibehavior_da3_clean_up_your_desk_40k/source_code/training/train.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Training script for BEHAVIOR-1K solution.
|
| 3 |
+
|
| 4 |
+
Based on https://github.com/PhysicalIntelligence/openpi/blob/behavior/openpi/scripts/train.py with custom modifications.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import dataclasses
|
| 8 |
+
import functools
|
| 9 |
+
import logging
|
| 10 |
+
import os
|
| 11 |
+
import platform
|
| 12 |
+
import time
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
import etils.epath as epath
|
| 16 |
+
import flax.nnx as nnx
|
| 17 |
+
from flax.training import common_utils
|
| 18 |
+
import flax.traverse_util as traverse_util
|
| 19 |
+
import jax
|
| 20 |
+
import jax.experimental
|
| 21 |
+
import jax.numpy as jnp
|
| 22 |
+
import numpy as np
|
| 23 |
+
import optax
|
| 24 |
+
import tqdm_loggable.auto as tqdm
|
| 25 |
+
import wandb
|
| 26 |
+
|
| 27 |
+
# Configure JAX memory allocation to prevent OOM errors
|
| 28 |
+
os.environ.setdefault('XLA_PYTHON_CLIENT_MEM_FRACTION', '0.9')
|
| 29 |
+
os.environ.setdefault('XLA_PYTHON_CLIENT_ALLOCATOR', 'platform')
|
| 30 |
+
|
| 31 |
+
# Configure OpenBLAS to prevent thread creation errors
|
| 32 |
+
os.environ.setdefault('OPENBLAS_NUM_THREADS', '16')
|
| 33 |
+
os.environ.setdefault('MKL_NUM_THREADS', '16')
|
| 34 |
+
|
| 35 |
+
import openpi.models.model as _model
|
| 36 |
+
import openpi.shared.array_typing as at
|
| 37 |
+
import openpi.shared.nnx_utils as nnx_utils
|
| 38 |
+
import openpi.training.optimizer as _optimizer
|
| 39 |
+
import openpi.training.sharding as sharding
|
| 40 |
+
import openpi.training.utils as training_utils
|
| 41 |
+
|
| 42 |
+
# Import B1K-specific modules
|
| 43 |
+
from b1k.training import checkpoints as _checkpoints # Use our custom checkpoints (not openpi's!)
|
| 44 |
+
from b1k.training import config as _config
|
| 45 |
+
from b1k.training import data_loader as _data_loader
|
| 46 |
+
from b1k.training import weight_loaders as _weight_loaders
|
| 47 |
+
from b1k.models.pi_behavior import PiBehavior
|
| 48 |
+
from b1k.models.pi_behavior_config import PiBehaviorConfig
|
| 49 |
+
from b1k.models.observation import Observation
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def init_logging():
|
| 53 |
+
"""Custom logging format for better readability."""
|
| 54 |
+
level_mapping = {"DEBUG": "D", "INFO": "I", "WARNING": "W", "ERROR": "E", "CRITICAL": "C"}
|
| 55 |
+
|
| 56 |
+
class CustomFormatter(logging.Formatter):
|
| 57 |
+
def format(self, record):
|
| 58 |
+
record.levelname = level_mapping.get(record.levelname, record.levelname)
|
| 59 |
+
return super().format(record)
|
| 60 |
+
|
| 61 |
+
formatter = CustomFormatter(
|
| 62 |
+
fmt="%(asctime)s.%(msecs)03d [%(levelname)s] %(message)-80s (%(process)d:%(filename)s:%(lineno)s)",
|
| 63 |
+
datefmt="%H:%M:%S",
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
logger = logging.getLogger()
|
| 67 |
+
logger.setLevel(logging.INFO)
|
| 68 |
+
logger.handlers[0].setFormatter(formatter)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def init_wandb(config: _config.TrainConfig, *, resuming: bool, log_code: bool = False, enabled: bool = True):
|
| 72 |
+
if not enabled:
|
| 73 |
+
wandb.init(mode="disabled")
|
| 74 |
+
return
|
| 75 |
+
|
| 76 |
+
ckpt_dir = config.checkpoint_dir
|
| 77 |
+
if not ckpt_dir.exists():
|
| 78 |
+
raise FileNotFoundError(f"Checkpoint directory {ckpt_dir} does not exist.")
|
| 79 |
+
if resuming:
|
| 80 |
+
run_id = (ckpt_dir / "wandb_id.txt").read_text().strip()
|
| 81 |
+
wandb.init(id=run_id, resume="must", project=config.project_name)
|
| 82 |
+
else:
|
| 83 |
+
wandb.init(
|
| 84 |
+
name=config.exp_name,
|
| 85 |
+
config=dataclasses.asdict(config),
|
| 86 |
+
project=config.project_name,
|
| 87 |
+
)
|
| 88 |
+
(ckpt_dir / "wandb_id.txt").write_text(wandb.run.id)
|
| 89 |
+
|
| 90 |
+
if log_code:
|
| 91 |
+
wandb.run.log_code(epath.Path(__file__).parent.parent)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _load_weights_and_validate(loader: _weight_loaders.WeightLoader, params_shape: at.Params) -> at.Params:
|
| 95 |
+
"""Loads and validates the weights. Returns a loaded subset of the weights."""
|
| 96 |
+
loaded_params = loader.load(params_shape)
|
| 97 |
+
|
| 98 |
+
# Filter out nnx.Intermediate fields from both sides (they're not params, excluded from checkpoints)
|
| 99 |
+
# This allows loading old checkpoints that didn't have these fields
|
| 100 |
+
def filter_intermediate_fields(params_dict):
|
| 101 |
+
flat = traverse_util.flatten_dict(params_dict)
|
| 102 |
+
# List of field names that are nnx.Intermediate (excluded from checkpoints)
|
| 103 |
+
intermediate_field_names = [
|
| 104 |
+
'action_correlation_cholesky', # Legacy full correlation matrix
|
| 105 |
+
'L_spatial', # Separable spatial correlation
|
| 106 |
+
'L_temporal', # Separable temporal correlation
|
| 107 |
+
'cached_num_inpaint_actions', # Conditional sampling cache
|
| 108 |
+
'cached_input_action_dim', # Conditional sampling cache
|
| 109 |
+
'cached_Sigma_uo_Sigma_oo_inv', # Conditional sampling cache
|
| 110 |
+
'cached_L_cond_free', # Conditional sampling cache
|
| 111 |
+
'cached_Sigma_ou_Sigma_uu_inv', # Conditional sampling cache
|
| 112 |
+
'cached_L_cond_inp', # Conditional sampling cache
|
| 113 |
+
]
|
| 114 |
+
filtered = {k: v for k, v in flat.items()
|
| 115 |
+
if not any(field in str(k) for field in intermediate_field_names)}
|
| 116 |
+
return traverse_util.unflatten_dict(filtered)
|
| 117 |
+
|
| 118 |
+
# Validate loaded params structure
|
| 119 |
+
params_shape_filtered = filter_intermediate_fields(params_shape)
|
| 120 |
+
loaded_params_filtered = filter_intermediate_fields(loaded_params)
|
| 121 |
+
at.check_pytree_equality(expected=params_shape_filtered, got=loaded_params_filtered, check_shapes=True, check_dtypes=True)
|
| 122 |
+
|
| 123 |
+
# Remove jax.ShapeDtypeStruct and Intermediate fields from the loaded params
|
| 124 |
+
def should_exclude(k, v):
|
| 125 |
+
if isinstance(v, jax.ShapeDtypeStruct):
|
| 126 |
+
return True
|
| 127 |
+
# Exclude all intermediate fields
|
| 128 |
+
intermediate_field_names = [
|
| 129 |
+
'action_correlation_cholesky', 'L_spatial', 'L_temporal',
|
| 130 |
+
'cached_num_inpaint_actions', 'cached_input_action_dim',
|
| 131 |
+
'cached_Sigma_uo_Sigma_oo_inv', 'cached_L_cond_free',
|
| 132 |
+
'cached_Sigma_ou_Sigma_uu_inv', 'cached_L_cond_inp',
|
| 133 |
+
]
|
| 134 |
+
return any(field in str(k) for field in intermediate_field_names)
|
| 135 |
+
|
| 136 |
+
return traverse_util.unflatten_dict(
|
| 137 |
+
{k: v for k, v in traverse_util.flatten_dict(loaded_params).items()
|
| 138 |
+
if not should_exclude(k, v)}
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@at.typecheck
|
| 143 |
+
def init_train_state(
|
| 144 |
+
config: _config.TrainConfig,
|
| 145 |
+
init_rng: at.KeyArrayLike,
|
| 146 |
+
mesh: jax.sharding.Mesh,
|
| 147 |
+
*,
|
| 148 |
+
resume: bool,
|
| 149 |
+
norm_stats: dict | None = None
|
| 150 |
+
) -> tuple[training_utils.TrainState, Any]:
|
| 151 |
+
# DA3 v2 multi-group LR: the fresh spatial modules (bank builder + injection) and the geometry
|
| 152 |
+
# ray-MLP need a much higher LR than the pretrained trunk, or they barely move off init. Keyed on
|
| 153 |
+
# the same env the DA3 entrypoint sets; falls back to the single-group optimizer otherwise.
|
| 154 |
+
if os.environ.get("USE_DA3_FULL") == "1" and os.environ.get("DA3_LR_GROUPS", "1") == "1":
|
| 155 |
+
base = config.lr_schedule
|
| 156 |
+
def _sched(peak, end):
|
| 157 |
+
return dataclasses.replace(base, peak_lr=float(peak), decay_lr=float(end))
|
| 158 |
+
lr_groups = {
|
| 159 |
+
# pretrained SigLIP + gemma trunk + action expert: openpi pi0.5 finetune recipe
|
| 160 |
+
"vlm": _sched(os.environ.get("LR_VLM_PEAK", 2.5e-5), os.environ.get("LR_VLM_END", 2.5e-6)),
|
| 161 |
+
# fresh spatial branch (bank builder + cross-attn injection)
|
| 162 |
+
"core": _sched(os.environ.get("LR_CORE_PEAK", 5e-4), os.environ.get("LR_CORE_END", 5e-5)),
|
| 163 |
+
# fresh geometry ray-MLP
|
| 164 |
+
"geom": _sched(os.environ.get("LR_GEOM_PEAK", 5e-4), os.environ.get("LR_GEOM_END", 1e-4)),
|
| 165 |
+
}
|
| 166 |
+
# Optional delayed/slow phase-in for the geometry group: hold at 0 for LR_GEOM_DELAY steps
|
| 167 |
+
# (while vlm/core do their normal warmup), then ramp 0->peak over LR_GEOM_RAMP steps before
|
| 168 |
+
# the usual cosine decay. Off unless LR_GEOM_RAMP is set, so existing recipes are unchanged.
|
| 169 |
+
_g_ramp = int(os.environ.get("LR_GEOM_RAMP", "0"))
|
| 170 |
+
if _g_ramp > 0:
|
| 171 |
+
lr_groups["geom"] = _optimizer.DelayedRampCosineSchedule(
|
| 172 |
+
delay_steps=int(os.environ.get("LR_GEOM_DELAY", "0")),
|
| 173 |
+
ramp_steps=_g_ramp,
|
| 174 |
+
peak_lr=float(os.environ.get("LR_GEOM_PEAK", 5e-4)),
|
| 175 |
+
decay_steps=base.decay_steps,
|
| 176 |
+
decay_lr=float(os.environ.get("LR_GEOM_END", 1e-4)),
|
| 177 |
+
)
|
| 178 |
+
logging.info("DA3 geom LR: delayed ramp (0 for %d steps, 0->peak over %d, then cosine to %s)",
|
| 179 |
+
int(os.environ.get("LR_GEOM_DELAY", "0")), _g_ramp,
|
| 180 |
+
os.environ.get("LR_GEOM_END", 1e-4))
|
| 181 |
+
logging.info("DA3 multi-group LR: vlm=%s core=%s geom=%s",
|
| 182 |
+
lr_groups["vlm"].peak_lr, lr_groups["core"].peak_lr, lr_groups["geom"].peak_lr)
|
| 183 |
+
tx = _optimizer.create_multi_group_optimizer(
|
| 184 |
+
config.optimizer, lr_groups, _optimizer.spatial_group_labels, weight_decay_mask=None)
|
| 185 |
+
else:
|
| 186 |
+
tx = _optimizer.create_optimizer(config.optimizer, config.lr_schedule, weight_decay_mask=None)
|
| 187 |
+
|
| 188 |
+
def init(rng: at.KeyArrayLike, partial_params: at.Params | None = None) -> training_utils.TrainState:
|
| 189 |
+
rng, model_rng = jax.random.split(rng)
|
| 190 |
+
# initialize the model (and its parameters).
|
| 191 |
+
model = config.model.create(model_rng)
|
| 192 |
+
|
| 193 |
+
# Load correlation matrix into PiBehavior models BEFORE creating graphdef
|
| 194 |
+
if isinstance(model, PiBehavior) and norm_stats is not None:
|
| 195 |
+
model.load_correlation_matrix(norm_stats)
|
| 196 |
+
logging.info("Loaded correlation matrix during model initialization")
|
| 197 |
+
|
| 198 |
+
# Merge the partial params into the model.
|
| 199 |
+
if partial_params is not None:
|
| 200 |
+
graphdef, state = nnx.split(model)
|
| 201 |
+
# This will produce an error if the partial params are not a subset of the state.
|
| 202 |
+
state.replace_by_pure_dict(partial_params)
|
| 203 |
+
model = nnx.merge(graphdef, state)
|
| 204 |
+
|
| 205 |
+
params = nnx.state(model)
|
| 206 |
+
params = nnx_utils.state_map(params, config.freeze_filter, lambda p: p.replace(p.value.astype(jnp.bfloat16)))
|
| 207 |
+
return training_utils.TrainState(
|
| 208 |
+
step=0,
|
| 209 |
+
params=params,
|
| 210 |
+
model_def=nnx.graphdef(model),
|
| 211 |
+
tx=tx,
|
| 212 |
+
opt_state=tx.init(params.filter(config.trainable_filter)),
|
| 213 |
+
ema_decay=config.ema_decay,
|
| 214 |
+
ema_params=None if config.ema_decay is None else params,
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
train_state_shape = jax.eval_shape(init, init_rng)
|
| 218 |
+
state_sharding = sharding.fsdp_sharding(train_state_shape, mesh, log=True)
|
| 219 |
+
|
| 220 |
+
if resume:
|
| 221 |
+
return train_state_shape, state_sharding
|
| 222 |
+
|
| 223 |
+
partial_params = _load_weights_and_validate(config.weight_loader, train_state_shape.params.to_pure_dict())
|
| 224 |
+
replicated_sharding = jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
|
| 225 |
+
|
| 226 |
+
# Initialize the train state and mix in the partial params.
|
| 227 |
+
train_state = jax.jit(
|
| 228 |
+
init,
|
| 229 |
+
donate_argnums=(1,), # donate the partial params buffer.
|
| 230 |
+
in_shardings=replicated_sharding,
|
| 231 |
+
out_shardings=state_sharding,
|
| 232 |
+
)(init_rng, partial_params)
|
| 233 |
+
|
| 234 |
+
# Log KV transform coefficients for PiBehavior models
|
| 235 |
+
model = nnx.merge(train_state.model_def, train_state.params)
|
| 236 |
+
if isinstance(model, PiBehavior) and hasattr(model, 'kv_transform') and model.kv_transform is not None:
|
| 237 |
+
logging.info("KV Transform Coefficients (after loading):")
|
| 238 |
+
logging.info("=" * 80)
|
| 239 |
+
|
| 240 |
+
k_coeffs = model.kv_transform.k_coeffs.value
|
| 241 |
+
v_coeffs = model.kv_transform.v_coeffs.value
|
| 242 |
+
|
| 243 |
+
logging.info("K Coefficients (each layer attends to all VLM layers):")
|
| 244 |
+
for i in range(k_coeffs.shape[0]):
|
| 245 |
+
coeffs_str = ", ".join([f"{float(c):.2f}" for c in k_coeffs[i]])
|
| 246 |
+
logging.info(f" Layer {i:2d}: [{coeffs_str}]")
|
| 247 |
+
|
| 248 |
+
logging.info("")
|
| 249 |
+
logging.info("V Coefficients (each layer attends to all VLM layers):")
|
| 250 |
+
for i in range(v_coeffs.shape[0]):
|
| 251 |
+
coeffs_str = ", ".join([f"{float(c):.2f}" for c in v_coeffs[i]])
|
| 252 |
+
logging.info(f" Layer {i:2d}: [{coeffs_str}]")
|
| 253 |
+
|
| 254 |
+
logging.info("=" * 80)
|
| 255 |
+
|
| 256 |
+
return train_state, state_sharding
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
@at.typecheck
|
| 260 |
+
def train_step(
|
| 261 |
+
config: _config.TrainConfig,
|
| 262 |
+
rng: at.KeyArrayLike,
|
| 263 |
+
state: training_utils.TrainState,
|
| 264 |
+
batch: tuple[Observation, _model.Actions],
|
| 265 |
+
) -> tuple[training_utils.TrainState, dict[str, at.Array]]:
|
| 266 |
+
model = nnx.merge(state.model_def, state.params)
|
| 267 |
+
model.train()
|
| 268 |
+
|
| 269 |
+
@at.typecheck
|
| 270 |
+
def loss_fn(
|
| 271 |
+
model: PiBehavior, rng: at.KeyArrayLike, observation: Observation, actions: _model.Actions
|
| 272 |
+
):
|
| 273 |
+
losses_dict = model.compute_detailed_loss(rng, observation, actions, train=True, num_flow_samples=config.num_flow_samples)
|
| 274 |
+
total_loss = jnp.mean(losses_dict["total_loss"])
|
| 275 |
+
return total_loss, losses_dict
|
| 276 |
+
|
| 277 |
+
train_rng = jax.random.fold_in(rng, state.step)
|
| 278 |
+
observation, actions = batch
|
| 279 |
+
|
| 280 |
+
# Filter out frozen params.
|
| 281 |
+
diff_state = nnx.DiffState(0, config.trainable_filter)
|
| 282 |
+
(loss, losses_dict), grads = nnx.value_and_grad(loss_fn, argnums=diff_state, has_aux=True)(model, train_rng, observation, actions)
|
| 283 |
+
|
| 284 |
+
# Knowledge insulation gradient monitoring
|
| 285 |
+
if config.model.use_knowledge_insulation:
|
| 286 |
+
# Helper functions to identify parameter groups
|
| 287 |
+
def is_action_expert_param(path_str):
|
| 288 |
+
# Action expert parameters:
|
| 289 |
+
# - Second LLM expert (300M params, marked with _1 suffix)
|
| 290 |
+
# - Action projections, time MLPs, kv_transform
|
| 291 |
+
return any(x in path_str for x in [
|
| 292 |
+
"_1", # All second expert parameters
|
| 293 |
+
"action_in_proj",
|
| 294 |
+
"action_out_proj",
|
| 295 |
+
"time_mlp_in",
|
| 296 |
+
"time_mlp_out",
|
| 297 |
+
"kv_transform"
|
| 298 |
+
])
|
| 299 |
+
|
| 300 |
+
def is_vlm_param(path_str):
|
| 301 |
+
# VLM parameters: everything else (first expert, img, FAST, task modules)
|
| 302 |
+
return not is_action_expert_param(path_str)
|
| 303 |
+
|
| 304 |
+
# Compute gradient norms for monitoring only (no scaling applied)
|
| 305 |
+
def compute_group_norm(grads_state, predicate):
|
| 306 |
+
"""Compute norm for gradients matching predicate."""
|
| 307 |
+
flat_grads = []
|
| 308 |
+
for path, value in jax.tree_util.tree_flatten_with_path(grads_state.to_pure_dict())[0]:
|
| 309 |
+
path_str = "/".join(str(k) for k in path)
|
| 310 |
+
if predicate(path_str):
|
| 311 |
+
if hasattr(value, 'value'):
|
| 312 |
+
flat_grads.append(value.value if hasattr(value, 'value') else value)
|
| 313 |
+
else:
|
| 314 |
+
flat_grads.append(value)
|
| 315 |
+
|
| 316 |
+
if flat_grads:
|
| 317 |
+
return jnp.sqrt(sum(jnp.sum(jnp.square(g)) for g in flat_grads))
|
| 318 |
+
return 0.0
|
| 319 |
+
|
| 320 |
+
grad_norm_vlm = compute_group_norm(grads, is_vlm_param)
|
| 321 |
+
grad_norm_action = compute_group_norm(grads, is_action_expert_param)
|
| 322 |
+
else:
|
| 323 |
+
grad_norm_vlm = None
|
| 324 |
+
grad_norm_action = None
|
| 325 |
+
|
| 326 |
+
params = state.params.filter(config.trainable_filter)
|
| 327 |
+
updates, new_opt_state = state.tx.update(grads, state.opt_state, params)
|
| 328 |
+
new_params = optax.apply_updates(params, updates)
|
| 329 |
+
|
| 330 |
+
# Update the model in place and return the new full state.
|
| 331 |
+
nnx.update(model, new_params)
|
| 332 |
+
new_params = nnx.state(model)
|
| 333 |
+
|
| 334 |
+
new_state = dataclasses.replace(state, step=state.step + 1, params=new_params, opt_state=new_opt_state)
|
| 335 |
+
if state.ema_decay is not None:
|
| 336 |
+
new_state = dataclasses.replace(
|
| 337 |
+
new_state,
|
| 338 |
+
ema_params=jax.tree.map(
|
| 339 |
+
lambda old, new: state.ema_decay * old + (1 - state.ema_decay) * new, state.ema_params, new_params
|
| 340 |
+
),
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# Filter out params that aren't kernels.
|
| 344 |
+
kernel_params = nnx.state(
|
| 345 |
+
model,
|
| 346 |
+
nnx.All(
|
| 347 |
+
nnx.Param,
|
| 348 |
+
nnx.Not(nnx_utils.PathRegex(".*/(bias|scale|pos_embedding|input_embedding)")),
|
| 349 |
+
lambda _, x: x.value.ndim > 1,
|
| 350 |
+
),
|
| 351 |
+
)
|
| 352 |
+
info = {
|
| 353 |
+
"loss": loss,
|
| 354 |
+
"grad_norm": optax.global_norm(grads),
|
| 355 |
+
"param_norm": optax.global_norm(kernel_params),
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
# Add gradient norm breakdown for knowledge insulation monitoring
|
| 359 |
+
if grad_norm_vlm is not None:
|
| 360 |
+
info["grad_norm_vlm"] = grad_norm_vlm
|
| 361 |
+
info["grad_norm_action_expert"] = grad_norm_action
|
| 362 |
+
|
| 363 |
+
# Add detailed loss components to info
|
| 364 |
+
for key, value in losses_dict.items():
|
| 365 |
+
if isinstance(value, (float, int)) or (hasattr(value, 'ndim') and value.ndim == 0):
|
| 366 |
+
info[key] = value
|
| 367 |
+
else:
|
| 368 |
+
info[key] = jnp.mean(value)
|
| 369 |
+
return new_state, info
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def main(config: _config.TrainConfig):
|
| 373 |
+
init_logging()
|
| 374 |
+
logging.info(f"Running on: {platform.node()}")
|
| 375 |
+
|
| 376 |
+
if config.batch_size % jax.device_count() != 0:
|
| 377 |
+
raise ValueError(
|
| 378 |
+
f"Batch size {config.batch_size} must be divisible by the number of devices {jax.device_count()}."
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
jax.config.update("jax_compilation_cache_dir", str(epath.Path("~/.cache/jax").expanduser()))
|
| 382 |
+
|
| 383 |
+
# Generate random seed if not provided
|
| 384 |
+
seed = config.seed
|
| 385 |
+
if seed is None:
|
| 386 |
+
seed = int(time.time() * 1000) % (2**32)
|
| 387 |
+
logging.info(f"Using random seed for JAX RNG: {seed}")
|
| 388 |
+
|
| 389 |
+
rng = jax.random.key(seed)
|
| 390 |
+
train_rng, init_rng = jax.random.split(rng)
|
| 391 |
+
|
| 392 |
+
mesh = sharding.make_mesh(config.fsdp_devices)
|
| 393 |
+
data_sharding = jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec(sharding.DATA_AXIS))
|
| 394 |
+
replicated_sharding = jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
|
| 395 |
+
|
| 396 |
+
checkpoint_manager, resuming = _checkpoints.initialize_checkpoint_dir(
|
| 397 |
+
config.checkpoint_dir,
|
| 398 |
+
keep_period=config.keep_period,
|
| 399 |
+
overwrite=config.overwrite,
|
| 400 |
+
resume=config.resume,
|
| 401 |
+
)
|
| 402 |
+
init_wandb(config, resuming=resuming, enabled=config.wandb_enabled)
|
| 403 |
+
|
| 404 |
+
data_loader = _data_loader.create_behavior_data_loader(
|
| 405 |
+
config,
|
| 406 |
+
sharding=data_sharding,
|
| 407 |
+
shuffle=True,
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
data_iter = iter(data_loader)
|
| 411 |
+
batch = next(data_iter)
|
| 412 |
+
logging.info(f"Initialized data loader:\n{training_utils.array_tree_to_info(batch)}")
|
| 413 |
+
|
| 414 |
+
if os.environ.get("SKIP_IMAGE_LOG", "0") != "1":
|
| 415 |
+
# Log images from first batch to sanity check.
|
| 416 |
+
images_to_log = [
|
| 417 |
+
wandb.Image(np.concatenate([np.array(img[i]) for img in batch[0].images.values()], axis=1))
|
| 418 |
+
for i in range(min(5, len(next(iter(batch[0].images.values())))))
|
| 419 |
+
]
|
| 420 |
+
wandb.log({"camera_views": images_to_log}, step=0)
|
| 421 |
+
|
| 422 |
+
# Get norm_stats for correlation matrix loading
|
| 423 |
+
data_config = data_loader.data_config()
|
| 424 |
+
if data_config.norm_stats is None:
|
| 425 |
+
raise ValueError(
|
| 426 |
+
"norm_stats not found. Run compute_norm_stats.py to generate normalization statistics."
|
| 427 |
+
)
|
| 428 |
+
norm_stats = data_config.norm_stats
|
| 429 |
+
|
| 430 |
+
train_state, train_state_sharding = init_train_state(
|
| 431 |
+
config, init_rng, mesh, resume=resuming, norm_stats=norm_stats
|
| 432 |
+
)
|
| 433 |
+
jax.block_until_ready(train_state)
|
| 434 |
+
logging.info(f"Initialized train state:\n{training_utils.array_tree_to_info(train_state.params)}")
|
| 435 |
+
|
| 436 |
+
if resuming:
|
| 437 |
+
train_state = _checkpoints.restore_state(checkpoint_manager, train_state, data_loader)
|
| 438 |
+
|
| 439 |
+
# Reload correlation matrix after restore
|
| 440 |
+
model = nnx.merge(train_state.model_def, train_state.params)
|
| 441 |
+
model.load_correlation_matrix(norm_stats)
|
| 442 |
+
logging.info("Reloaded correlation matrix after checkpoint restore")
|
| 443 |
+
train_state = dataclasses.replace(train_state, model_def=nnx.graphdef(model))
|
| 444 |
+
|
| 445 |
+
ptrain_step = jax.jit(
|
| 446 |
+
functools.partial(train_step, config),
|
| 447 |
+
in_shardings=(replicated_sharding, train_state_sharding, data_sharding),
|
| 448 |
+
out_shardings=(train_state_sharding, replicated_sharding),
|
| 449 |
+
donate_argnums=(1,),
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
start_step = int(train_state.step)
|
| 453 |
+
pbar = tqdm.tqdm(
|
| 454 |
+
range(start_step, config.num_train_steps),
|
| 455 |
+
initial=start_step,
|
| 456 |
+
total=config.num_train_steps,
|
| 457 |
+
dynamic_ncols=True,
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
infos = []
|
| 461 |
+
for step in pbar:
|
| 462 |
+
with sharding.set_mesh(mesh):
|
| 463 |
+
train_state, info = ptrain_step(train_rng, train_state, batch)
|
| 464 |
+
infos.append(info)
|
| 465 |
+
if step % config.log_interval == 0:
|
| 466 |
+
stacked_infos = common_utils.stack_forest(infos)
|
| 467 |
+
reduced_info = jax.device_get(jax.tree.map(jnp.mean, stacked_infos))
|
| 468 |
+
|
| 469 |
+
# Create a concise console log with main metrics
|
| 470 |
+
main_metrics = {k: v for k, v in reduced_info.items()
|
| 471 |
+
if "loss" in k or "accuracy" in k or k in ["grad_norm", "param_norm", "grad_norm_vlm", "grad_norm_action_expert"]}
|
| 472 |
+
info_str = ", ".join(f"{k}={v:.4f}" for k, v in main_metrics.items())
|
| 473 |
+
pbar.write(f"Step {step}: {info_str}")
|
| 474 |
+
wandb.log(reduced_info, step=step)
|
| 475 |
+
infos = []
|
| 476 |
+
batch = next(data_iter)
|
| 477 |
+
|
| 478 |
+
skip_final_save = os.environ.get("SKIP_FINAL_SAVE", "0") == "1"
|
| 479 |
+
should_save_interval = step % config.save_interval == 0 and step > start_step
|
| 480 |
+
should_save_final = step == config.num_train_steps - 1 and not skip_final_save
|
| 481 |
+
if should_save_interval or should_save_final:
|
| 482 |
+
_checkpoints.save_state(checkpoint_manager, train_state, data_loader, step)
|
| 483 |
+
|
| 484 |
+
logging.info("Waiting for checkpoint manager to finish")
|
| 485 |
+
checkpoint_manager.wait_until_finished()
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
if __name__ == "__main__":
|
| 489 |
+
main(_config.cli())
|
pibehavior_da3_clean_up_your_desk_40k/source_code/training/train_2026.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train / correctness-gate the 2025 PiBehavior model on the 2026 v3 subset.
|
| 2 |
+
|
| 3 |
+
Reuses scripts/train.py's main() but swaps the data loader for the v3 reader
|
| 4 |
+
(b1k.training.b1k_2026) and initializes weights from a 2025 checkpoint.
|
| 5 |
+
|
| 6 |
+
Env knobs:
|
| 7 |
+
B1K_2026_ROOT dataset root (default: checkpoint_3's 13-task subset)
|
| 8 |
+
B1K_INIT_PARAMS 2025 checkpoint params dir to init from (default: checkpoint_3)
|
| 9 |
+
B1K_ACTIVITIES comma-separated activity ids to train/evaluate (default: ck3 subset)
|
| 10 |
+
B1K_BASE_CONFIG training config to clone (default: pi_behavior_b1k_fast)
|
| 11 |
+
USE_DA3_SPATIAL Enable precomputed DA3 spatial-token adapter (default 0)
|
| 12 |
+
DA3_SPATIAL_TOKENS Number of precomputed DA3 tokens per sample (default 320)
|
| 13 |
+
DA3_SPATIAL_DIM Feature width of each DA3 token (default 1024)
|
| 14 |
+
DA3_SPATIAL_HEADS Cross-attention heads (default 8)
|
| 15 |
+
DA3_SPATIAL_SCALE Residual scale for the DA3 adapter (default 1.0)
|
| 16 |
+
BS global batch size (must be divisible by #devices; default 16)
|
| 17 |
+
FSDP_DEVICES number of devices for FSDP sharding (default: repo config)
|
| 18 |
+
NW dataloader workers (default 24)
|
| 19 |
+
STEPS num_train_steps (default 40 — a gate, not a full run)
|
| 20 |
+
FLOW num_flow_samples (default 4; paper uses 15)
|
| 21 |
+
LR_WARMUP cosine LR warmup steps (default: repo config)
|
| 22 |
+
LR_PEAK cosine peak LR (default: repo config)
|
| 23 |
+
LR_DECAY_STEPS cosine decay steps (default: repo config)
|
| 24 |
+
LR_DECAY cosine final LR (default: repo config)
|
| 25 |
+
SAVE_INTERVAL checkpoint interval (default: disabled during gates)
|
| 26 |
+
KEEP_PERIOD checkpoint keep period (default: repo config)
|
| 27 |
+
RESUME resume existing checkpoint directory (default 0)
|
| 28 |
+
OVERWRITE overwrite checkpoint directory (default 1 unless RESUME=1)
|
| 29 |
+
LOG_INTERVAL metric logging interval (default 10)
|
| 30 |
+
SKIP_IMAGE_LOG skip first-batch image logging (default 1)
|
| 31 |
+
"""
|
| 32 |
+
import os
|
| 33 |
+
import sys
|
| 34 |
+
import json
|
| 35 |
+
import dataclasses
|
| 36 |
+
|
| 37 |
+
# JAX-friendly + headless defaults
|
| 38 |
+
os.environ.setdefault("WANDB_MODE", "disabled")
|
| 39 |
+
os.environ.setdefault("XLA_PYTHON_CLIENT_MEM_FRACTION", "0.9")
|
| 40 |
+
os.environ.setdefault("SKIP_IMAGE_LOG", "1")
|
| 41 |
+
|
| 42 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # so `import train` (scripts/train.py) works
|
| 43 |
+
|
| 44 |
+
from b1k.training import config as _config
|
| 45 |
+
from b1k.training import data_loader as _data_loader
|
| 46 |
+
from b1k.training import weight_loaders
|
| 47 |
+
from b1k.training.b1k_2026 import create_v3_behavior_data_loader
|
| 48 |
+
from b1k.training.b1k_da3 import create_v3_behavior_da3_loader
|
| 49 |
+
|
| 50 |
+
ROOT = os.environ.get("B1K_2026_ROOT", "/work/jack/behavior1k/data/behavior_2026_ck3")
|
| 51 |
+
INIT_PARAMS = os.environ.get("B1K_INIT_PARAMS", "/work/jack/behavior1k/checkpoints/checkpoint_3/params")
|
| 52 |
+
BASE_CONFIG = os.environ.get("B1K_BASE_CONFIG", "pi_behavior_b1k_fast")
|
| 53 |
+
TASK_DATA_JSON = os.environ.get("B1K_TASK_DATA_JSON", "/work/jack/behavior1k/task_data.json")
|
| 54 |
+
if os.environ.get("B1K_ACTIVITIES"):
|
| 55 |
+
ACTIVITIES = [x.strip() for x in os.environ["B1K_ACTIVITIES"].split(",") if x.strip()]
|
| 56 |
+
else:
|
| 57 |
+
ACTIVITIES = json.load(open("/work/jack/behavior1k/subset_plan.json"))["ck3_names"]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _v3_loader(config, *, sharding=None, shuffle=False, num_batches=None, skip_norm_stats=False):
|
| 61 |
+
if "SHUFFLE" in os.environ:
|
| 62 |
+
shuffle = bool(int(os.environ["SHUFFLE"]))
|
| 63 |
+
if getattr(config.model, "da3", None) is not None and config.model.da3.enabled:
|
| 64 |
+
return create_v3_behavior_da3_loader(
|
| 65 |
+
config, ROOT, ACTIVITIES, TASK_DATA_JSON,
|
| 66 |
+
lang_cache=os.environ.get("DA3_LANG_CACHE", "/work/jack/behavior1k/modernbert_b1k_tasks.pkl"),
|
| 67 |
+
sharding=sharding, shuffle=shuffle, num_workers=config.num_workers, seed=config.seed or 0,
|
| 68 |
+
)
|
| 69 |
+
return create_v3_behavior_data_loader(
|
| 70 |
+
config, ROOT, ACTIVITIES, TASK_DATA_JSON,
|
| 71 |
+
sharding=sharding, shuffle=shuffle, num_workers=config.num_workers, seed=config.seed or 0,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# Swap the loader everywhere main() reaches it.
|
| 76 |
+
_data_loader.create_behavior_data_loader = _v3_loader
|
| 77 |
+
import train # scripts/train.py — defines main()
|
| 78 |
+
train._data_loader.create_behavior_data_loader = _v3_loader
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def build_config() -> _config.TrainConfig:
|
| 82 |
+
c = _config.get_config(BASE_CONFIG)
|
| 83 |
+
model = c.model
|
| 84 |
+
lr_schedule = c.lr_schedule
|
| 85 |
+
if any(k in os.environ for k in ("LR_WARMUP", "LR_PEAK", "LR_DECAY_STEPS", "LR_DECAY")):
|
| 86 |
+
lr_schedule = _config._optimizer.CosineDecaySchedule(
|
| 87 |
+
warmup_steps=int(os.environ.get("LR_WARMUP", str(lr_schedule.warmup_steps))),
|
| 88 |
+
peak_lr=float(os.environ.get("LR_PEAK", str(lr_schedule.peak_lr))),
|
| 89 |
+
decay_steps=int(os.environ.get("LR_DECAY_STEPS", str(lr_schedule.decay_steps))),
|
| 90 |
+
decay_lr=float(os.environ.get("LR_DECAY", str(lr_schedule.decay_lr))),
|
| 91 |
+
)
|
| 92 |
+
if bool(int(os.environ.get("USE_DA3_FULL", "0"))):
|
| 93 |
+
from b1k.models.pi_behavior_config import B1KDA3Config
|
| 94 |
+
model = dataclasses.replace(
|
| 95 |
+
model,
|
| 96 |
+
da3=B1KDA3Config(
|
| 97 |
+
spatial_scale=float(os.environ.get("DA3_SCALE", "2.0")),
|
| 98 |
+
spatial_init_std=float(os.environ.get("DA3_INIT_STD", "0.01")),
|
| 99 |
+
attn_logit_gain=bool(int(os.environ.get("DA3_LOGIT_GAIN", "1"))),
|
| 100 |
+
),
|
| 101 |
+
)
|
| 102 |
+
if bool(int(os.environ.get("USE_DA3_SPATIAL", "0"))):
|
| 103 |
+
model = dataclasses.replace(
|
| 104 |
+
model,
|
| 105 |
+
use_spatial_action_cross_attention=True,
|
| 106 |
+
spatial_num_tokens=int(os.environ.get("DA3_SPATIAL_TOKENS", "320")),
|
| 107 |
+
spatial_token_dim=int(os.environ.get("DA3_SPATIAL_DIM", "1024")),
|
| 108 |
+
spatial_num_heads=int(os.environ.get("DA3_SPATIAL_HEADS", "8")),
|
| 109 |
+
spatial_residual_scale=float(os.environ.get("DA3_SPATIAL_SCALE", "1.0")),
|
| 110 |
+
)
|
| 111 |
+
return dataclasses.replace(
|
| 112 |
+
c,
|
| 113 |
+
exp_name=os.environ.get("EXP", "v3_ck3_gate"),
|
| 114 |
+
model=model,
|
| 115 |
+
lr_schedule=lr_schedule,
|
| 116 |
+
weight_loader=weight_loaders.PiBehaviorWeightLoader(INIT_PARAMS),
|
| 117 |
+
wandb_enabled=False,
|
| 118 |
+
overwrite=bool(int(os.environ.get("OVERWRITE", "0" if os.environ.get("RESUME", "0") == "1" else "1"))),
|
| 119 |
+
resume=bool(int(os.environ.get("RESUME", "0"))),
|
| 120 |
+
batch_size=int(os.environ.get("BS", "16")),
|
| 121 |
+
fsdp_devices=int(os.environ.get("FSDP_DEVICES", str(c.fsdp_devices))),
|
| 122 |
+
num_workers=int(os.environ.get("NW", "24")),
|
| 123 |
+
num_train_steps=int(os.environ.get("STEPS", "40")),
|
| 124 |
+
num_flow_samples=int(os.environ.get("FLOW", "4")),
|
| 125 |
+
log_interval=int(os.environ.get("LOG_INTERVAL", "10")),
|
| 126 |
+
save_interval=int(os.environ.get("SAVE_INTERVAL", "10000000")), # disabled during gates by default
|
| 127 |
+
keep_period=int(os.environ.get("KEEP_PERIOD", str(c.keep_period))),
|
| 128 |
+
seed=0,
|
| 129 |
+
assets_base_dir="./outputs/assets",
|
| 130 |
+
checkpoint_base_dir="./outputs/checkpoints",
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
train.main(build_config())
|