lfhe commited on
Commit
37c05a8
·
verified ·
1 Parent(s): b5c3367
Files changed (3) hide show
  1. flock_robotics_adapter.py +185 -107
  2. model.pt +3 -0
  3. vla_config.json +96 -0
flock_robotics_adapter.py CHANGED
@@ -1,117 +1,195 @@
1
  from __future__ import annotations
2
 
 
3
  import json
 
4
  from pathlib import Path
5
 
6
  import numpy as np
7
  import torch
8
  import torch.nn as nn
9
- from PIL import Image
10
- from safetensors.torch import load_file
11
- from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
12
-
13
- ACTION_DIM = 7
14
-
15
-
16
- def format_user_text(obs: dict) -> str:
17
- """Build the text the VLM sees — must match training."""
18
- instruction = str(obs.get("instruction", "")).strip()
19
- task = str(obs.get("task", "")).strip()
20
- difficulty = str(obs.get("difficulty", "")).strip()
21
- step = obs.get("step", obs.get("step_index", ""))
22
- horizon = obs.get("horizon", "")
23
- parts = [instruction] if instruction else []
24
- meta = []
25
- if task:
26
- meta.append(f"task={task}")
27
- if difficulty:
28
- meta.append(f"difficulty={difficulty}")
29
- if step != "" and step is not None:
30
- meta.append(f"step={step}" + (f"/{horizon}" if horizon not in ("", None) else ""))
31
- if meta:
32
- parts.append(" ".join(meta))
33
- proprio = obs.get("proprio")
34
- if proprio is not None:
35
- arr = np.asarray(proprio, dtype=np.float32).reshape(-1)
36
- # Compact fixed-width floats keep tokenizer length stable
37
- props = " ".join(f"{x:.3f}" for x in arr.tolist())
38
- parts.append(f"proprio: {props}")
39
- return "\n".join(parts) if parts else "act"
40
-
41
-
42
- def build_action_head(hidden_size, head_hidden):
43
- return nn.Sequential(
44
- nn.Linear(hidden_size, head_hidden),
45
- nn.GELU(),
46
- nn.Dropout(0.05),
47
- nn.Linear(head_hidden, head_hidden),
48
- nn.GELU(),
49
- nn.Linear(head_hidden, ACTION_DIM),
50
- )
51
-
52
-
53
- def last_token_hidden(hidden, attention_mask):
54
- # hidden: [B, T, H], mask: [B, T]
55
- idx = attention_mask.long().sum(dim=1).clamp(min=1) - 1
56
- b = torch.arange(hidden.size(0), device=hidden.device)
57
- return hidden[b, idx]
58
-
59
-
60
- class QwenVLAPolicy:
61
- def __init__(self, model, processor, head, device):
62
- self.model = model
63
- self.processor = processor
64
- self.head = head
65
- self.device = device
66
- self.model.eval()
67
- self.head.eval()
68
-
69
- @torch.no_grad()
70
- def act(self, obs):
71
- image = np.asarray(obs["image"], dtype=np.uint8)
72
- pil = Image.fromarray(image)
73
- user_text = format_user_text(obs)
74
- messages = [
75
- {
76
- "role": "user",
77
- "content": [
78
- {"type": "image"},
79
- {"type": "text", "text": user_text},
80
- ],
81
- }
82
- ]
83
- text = self.processor.apply_chat_template(
84
- messages, tokenize=False, add_generation_prompt=True
85
  )
86
- inputs = self.processor(
87
- text=[text], images=[pil], return_tensors="pt", padding=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  )
89
- inputs = {k: v.to(self.device) for k, v in inputs.items()}
90
- out = self.model(**inputs, output_hidden_states=True, use_cache=False)
91
- hidden = out.hidden_states[-1]
92
- pooled = last_token_hidden(hidden, inputs["attention_mask"])
93
- action = torch.tanh(self.head(pooled.float())).squeeze(0).cpu().numpy()
94
- return np.clip(action, -1.0, 1.0).astype(np.float32)
95
-
96
-
97
- def load_policy(model_dir, device, dtype):
98
- model_root = Path(model_dir)
99
- cfg = json.loads((model_root / "model_config.json").read_text())
100
- use_cuda = device == "cuda" and torch.cuda.is_available()
101
- torch_device = torch.device("cuda" if use_cuda else "cpu")
102
- torch_dtype = (
103
- torch.bfloat16
104
- if (use_cuda and str(dtype) == "bfloat16")
105
- else torch.float32
106
- )
107
- model = Qwen3VLForConditionalGeneration.from_pretrained(
108
- model_root, dtype=torch_dtype
109
- ).to(torch_device)
110
- processor = AutoProcessor.from_pretrained(model_root)
111
- head = build_action_head(int(cfg["hidden_size"]), int(cfg["head_hidden"])).to(
112
- torch_device
113
- )
114
- head.load_state_dict(
115
- load_file(str(model_root / "action_head.safetensors"), device=str(torch_device))
116
- )
117
- return QwenVLAPolicy(model, processor, head, torch_device)
 
1
  from __future__ import annotations
2
 
3
+ import hashlib
4
  import json
5
+ import re
6
  from pathlib import Path
7
 
8
  import numpy as np
9
  import torch
10
  import torch.nn as nn
11
+ import torch.nn.functional as F
12
+
13
+
14
+ IMAGENET_MEAN = (0.485, 0.456, 0.406)
15
+ IMAGENET_STD = (0.229, 0.224, 0.225)
16
+
17
+
18
+ class EncoderVLAPolicyNet(nn.Module):
19
+ def __init__(
20
+ self,
21
+ encoder,
22
+ text_dim: int = 256,
23
+ proprio_dim: int = 32,
24
+ action_dim: int = 7,
25
+ image_size: int = 224,
26
+ vision_dim: int = 384,
27
+ modality_emb_dim: int = 256,
28
+ freeze_encoder: bool = True,
29
+ ):
30
+ super().__init__()
31
+ self.encoder = encoder
32
+ self.image_size = int(image_size)
33
+ self.freeze_encoder = bool(freeze_encoder)
34
+ if self.freeze_encoder:
35
+ for p in self.encoder.parameters():
36
+ p.requires_grad = False
37
+ self.vision_proj = nn.Sequential(
38
+ nn.Linear(vision_dim, 512),
39
+ nn.SiLU(),
40
+ nn.LayerNorm(512),
41
+ nn.Linear(512, 512),
42
+ nn.SiLU(),
43
+ )
44
+ self.proprio_encoder = nn.Sequential(
45
+ nn.Linear(proprio_dim, modality_emb_dim),
46
+ nn.SiLU(),
47
+ nn.LayerNorm(modality_emb_dim),
48
+ nn.Linear(modality_emb_dim, modality_emb_dim),
49
+ nn.SiLU(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  )
51
+ self.text_encoder = nn.Sequential(
52
+ nn.Linear(text_dim, modality_emb_dim),
53
+ nn.SiLU(),
54
+ nn.LayerNorm(modality_emb_dim),
55
+ nn.Linear(modality_emb_dim, modality_emb_dim),
56
+ nn.SiLU(),
57
+ )
58
+ fused = 512 + 2 * modality_emb_dim
59
+ self.action_head = nn.Sequential(
60
+ nn.Linear(fused, 1024),
61
+ nn.SiLU(),
62
+ nn.LayerNorm(1024),
63
+ nn.Dropout(0.05),
64
+ nn.Linear(1024, 512),
65
+ nn.SiLU(),
66
+ nn.LayerNorm(512),
67
+ nn.Linear(512, 256),
68
+ nn.SiLU(),
69
+ nn.Linear(256, action_dim),
70
+ nn.Tanh(),
71
+ )
72
+ self.register_buffer(
73
+ "img_mean",
74
+ torch.tensor(IMAGENET_MEAN, dtype=torch.float32).view(1, 3, 1, 1),
75
+ persistent=False,
76
+ )
77
+ self.register_buffer(
78
+ "img_std",
79
+ torch.tensor(IMAGENET_STD, dtype=torch.float32).view(1, 3, 1, 1),
80
+ persistent=False,
81
+ )
82
+
83
+ def _encode_images(self, images: torch.Tensor) -> torch.Tensor:
84
+ if images.ndim != 4:
85
+ raise ValueError(f"expected image batch [B,H,W,3] or [B,3,H,W], got {tuple(images.shape)}")
86
+ if images.shape[-1] == 3:
87
+ images = images.permute(0, 3, 1, 2)
88
+ images = images.float()
89
+ if images.max() > 2.0:
90
+ images = images / 255.0
91
+ target = (self.image_size, self.image_size)
92
+ if images.shape[-2:] != target:
93
+ images = F.interpolate(images, size=target, mode="bilinear", align_corners=False)
94
+ images = (images - self.img_mean) / self.img_std
95
+ if self.freeze_encoder:
96
+ with torch.no_grad():
97
+ out = self.encoder(pixel_values=images)
98
+ else:
99
+ out = self.encoder(pixel_values=images)
100
+ # CLS token
101
+ return out.last_hidden_state[:, 0]
102
+
103
+ def forward(self, images, proprio, text_features):
104
+ vision = self.vision_proj(self._encode_images(images))
105
+ proprio_emb = self.proprio_encoder(proprio.float())
106
+ text_emb = self.text_encoder(text_features.float())
107
+ return self.action_head(torch.cat([vision, proprio_emb, text_emb], dim=-1))
108
+
109
+
110
+ def _text_vector(text: str, dim: int) -> np.ndarray:
111
+ vec = np.zeros(dim, dtype=np.float32)
112
+ tokens = re.findall(r"[a-z0-9_]+", text.lower())
113
+ for token in tokens:
114
+ digest = hashlib.blake2b(token.encode("utf-8"), digest_size=8).digest()
115
+ value = int.from_bytes(digest, byteorder="little", signed=False)
116
+ vec[value % dim] += 1.0 if value & 1 else -1.0
117
+ norm = float(np.linalg.norm(vec))
118
+ if norm > 0:
119
+ vec /= norm
120
+ return vec
121
+
122
+
123
+ def _proprio_vector(obs: dict, dim: int) -> np.ndarray:
124
+ raw = np.asarray(obs.get("proprio", np.zeros(25, dtype=np.float32)), dtype=np.float32).reshape(-1)
125
+ step = float(obs.get("step", 0))
126
+ horizon = float(obs.get("horizon", 320) or 320)
127
+ step_feature = np.asarray([step / max(horizon, 1.0)], dtype=np.float32)
128
+ combined = np.concatenate([raw, step_feature], axis=0)
129
+ if combined.size < dim:
130
+ combined = np.pad(combined, (0, dim - combined.size))
131
+ return combined[:dim].astype(np.float32)
132
+
133
+
134
+ class EncoderVLAPolicy:
135
+ def __init__(self, model_dir: str, device: str, dtype: str):
136
+ from transformers import Dinov2Config, Dinov2Model
137
+
138
+ self.model_dir = Path(model_dir)
139
+ self.config = json.loads((self.model_dir / "vla_config.json").read_text())
140
+ if device == "cuda" and torch.cuda.is_available():
141
+ self.device = torch.device("cuda")
142
+ elif device == "mps" and hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
143
+ self.device = torch.device("mps")
144
+ else:
145
+ self.device = torch.device("cpu")
146
+
147
+ enc_cfg = Dinov2Config.from_dict(self.config["encoder_config"])
148
+ encoder = Dinov2Model(enc_cfg)
149
+ image_size = self.config.get("image_size", [224, 224])
150
+ if isinstance(image_size, list):
151
+ image_size = int(image_size[0])
152
+ self.model = EncoderVLAPolicyNet(
153
+ encoder=encoder,
154
+ text_dim=int(self.config.get("text_dim", 256)),
155
+ proprio_dim=int(self.config.get("proprio_dim", 32)),
156
+ action_dim=int(self.config.get("action_dim", 7)),
157
+ image_size=int(image_size),
158
+ vision_dim=int(self.config.get("vision_dim", enc_cfg.hidden_size)),
159
+ modality_emb_dim=int(self.config.get("modality_emb_dim", 256)),
160
+ freeze_encoder=True,
161
+ ).to(self.device)
162
+ checkpoint = torch.load(self.model_dir / "model.pt", map_location=self.device, weights_only=True)
163
+ state_dict = checkpoint.get("state_dict", checkpoint)
164
+ self.model.load_state_dict(state_dict, strict=True)
165
+ self.model.eval()
166
+ for p in self.model.parameters():
167
+ p.requires_grad = False
168
+
169
+ def act(self, obs: dict) -> np.ndarray:
170
+ image_size = int(self.config.get("image_size", [224, 224])[0])
171
+ image = np.asarray(
172
+ obs.get("image", np.zeros((image_size, image_size, 3), dtype=np.uint8)),
173
+ dtype=np.uint8,
174
  )
175
+ if image.ndim == 2:
176
+ image = np.repeat(image[..., None], 3, axis=-1)
177
+ if image.shape[-1] > 3:
178
+ image = image[..., :3]
179
+ task = str(obs.get("task", ""))
180
+ difficulty = str(obs.get("difficulty", ""))
181
+ instruction = str(obs.get("instruction", ""))
182
+ text = f"task {task} difficulty {difficulty} instruction {instruction}"
183
+ text_features = _text_vector(text, int(self.config.get("text_dim", 256)))
184
+ proprio = _proprio_vector(obs, int(self.config.get("proprio_dim", 32)))
185
+ with torch.no_grad():
186
+ action = self.model(
187
+ torch.from_numpy(image).unsqueeze(0).to(self.device),
188
+ torch.from_numpy(proprio).unsqueeze(0).to(self.device),
189
+ torch.from_numpy(text_features).unsqueeze(0).to(self.device),
190
+ )
191
+ return np.clip(action.squeeze(0).detach().cpu().numpy(), -1.0, 1.0).astype(np.float32)
192
+
193
+
194
+ def load_policy(model_dir: str, device: str, dtype: str):
195
+ return EncoderVLAPolicy(model_dir=model_dir, device=device, dtype=dtype)
 
 
 
 
 
 
 
 
model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b69ff022f57ed09d6745740c9b2a183ed0a6f9b3a04cf84f8db1cff8a84f8fd4
3
+ size 97821951
vla_config.json ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": "robotics_vla_encoder_policy_v1",
3
+ "model_type": "dinov2_small_text_proprio_bc",
4
+ "encoder_id": "facebook/dinov2-small",
5
+ "encoder_config": {
6
+ "return_dict": true,
7
+ "output_hidden_states": false,
8
+ "dtype": "float32",
9
+ "chunk_size_feed_forward": 0,
10
+ "is_encoder_decoder": false,
11
+ "architectures": [
12
+ "Dinov2Model"
13
+ ],
14
+ "id2label": {
15
+ "0": "LABEL_0",
16
+ "1": "LABEL_1"
17
+ },
18
+ "label2id": {
19
+ "LABEL_0": 0,
20
+ "LABEL_1": 1
21
+ },
22
+ "problem_type": null,
23
+ "_name_or_path": "",
24
+ "transformers_version": "5.3.0",
25
+ "model_type": "dinov2",
26
+ "hidden_size": 384,
27
+ "num_hidden_layers": 12,
28
+ "num_attention_heads": 6,
29
+ "mlp_ratio": 4,
30
+ "hidden_act": "gelu",
31
+ "hidden_dropout_prob": 0.0,
32
+ "attention_probs_dropout_prob": 0.0,
33
+ "initializer_range": 0.02,
34
+ "layer_norm_eps": 1e-06,
35
+ "image_size": 518,
36
+ "patch_size": 14,
37
+ "num_channels": 3,
38
+ "qkv_bias": true,
39
+ "layerscale_value": 1.0,
40
+ "drop_path_rate": 0.0,
41
+ "use_swiglu_ffn": false,
42
+ "stage_names": [
43
+ "stem",
44
+ "stage1",
45
+ "stage2",
46
+ "stage3",
47
+ "stage4",
48
+ "stage5",
49
+ "stage6",
50
+ "stage7",
51
+ "stage8",
52
+ "stage9",
53
+ "stage10",
54
+ "stage11",
55
+ "stage12"
56
+ ],
57
+ "apply_layernorm": true,
58
+ "reshape_hidden_states": true,
59
+ "use_mask_token": true,
60
+ "output_attentions": false,
61
+ "out_features": [
62
+ "stage12"
63
+ ],
64
+ "out_indices": [
65
+ 12
66
+ ]
67
+ },
68
+ "text_dim": 256,
69
+ "proprio_dim": 32,
70
+ "action_dim": 7,
71
+ "image_size": [
72
+ 224,
73
+ 224
74
+ ],
75
+ "vision_dim": 384,
76
+ "modality_emb_dim": 256,
77
+ "parameter_count": 24434823,
78
+ "freeze_encoder": true,
79
+ "adapter_inputs": [
80
+ "image",
81
+ "proprio",
82
+ "instruction",
83
+ "task",
84
+ "difficulty",
85
+ "step",
86
+ "horizon"
87
+ ],
88
+ "forbidden_adapter_inputs": [
89
+ "raw_obs",
90
+ "object_state",
91
+ "sim_state",
92
+ "mujoco_data"
93
+ ],
94
+ "training_data": "hf:random-sequence/flock-robotics-vla-training-v2",
95
+ "run_tag": "dino_v10_baseline"
96
+ }