zhicao commited on
Commit
2b33aae
·
verified ·
1 Parent(s): 146c2fa

Scheduled Commit

Browse files
Files changed (1) hide show
  1. scripts/eval/trex_ablation_eval.py +593 -0
scripts/eval/trex_ablation_eval.py ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Controlled ablation evaluation for the T-Rex Track-Force cascade.
2
+
3
+ For each named checkpoint, on identical data:
4
+
5
+ * fixed-tau forward losses (action / dynamics / track / force flow MSE);
6
+ * open-loop chunk reconstruction: normalized 62-D action MSE of ``sample()``
7
+ against the ground-truth delta-base chunk (cascade and coarse-only);
8
+ * Stage-1 self-attention mass of action/obs queries over key token groups;
9
+ * Stage-2 force-transformer attention mass of action queries over
10
+ F6 / VQ-history / deform / coarse-memory tokens;
11
+ * tactile sensitivity: |refine(real tactile) - refine(tactile masked)|.
12
+
13
+ Usage:
14
+ python scripts/eval/trex_ablation_eval.py \
15
+ --dataset-root data/trex_mini_force \
16
+ --run full=checkpoints/ablate_full_3k/checkpoint-3000 \
17
+ --out ablation_eval.json
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import gc
24
+ import json
25
+ from pathlib import Path
26
+
27
+ import numpy as np
28
+ import torch
29
+ from hydra.utils import instantiate
30
+ from omegaconf import OmegaConf
31
+
32
+ from groot.vla.data.schema import DatasetMetadata, EmbodimentTag
33
+ from groot.vla.experiment.trex_eval_utils import TrexEpisode
34
+ from groot.vla.model.trex_track_force.attention import TokenType
35
+ from groot.vla.model.trex_track_force.dataset import (
36
+ DEFORM_VIDEO_KEYS,
37
+ eef62_delta_base,
38
+ nearest_timestamp_indices,
39
+ uniform_target_times,
40
+ )
41
+ from groot.vla.model.trex_track_force.force import (
42
+ ACTION_HORIZON,
43
+ FORCE_HISTORY_FRAMES,
44
+ FORCE_OFFSETS,
45
+ euler_flow_step,
46
+ pad_action_62_to_64,
47
+ )
48
+ from groot.vla.model.trex_track_force.runtime import TrexRuntimeStatistics
49
+ from groot.vla.model.trex_track_force.track import TRACK_HORIZON
50
+ from groot.vla.model.trex_track_force.vla import TrexTrackForceVLA
51
+
52
+ ACTION_RATE_HZ = 20.0
53
+ TACTILE_RATE_HZ = 5.0
54
+ VIDEO_RATE_HZ = 10.0
55
+ AR_BLOCKS = 4
56
+ VIDEO_FRAMES_PER_BLOCK = 8
57
+ VIDEO_KEYS = ("video.head_left", "video.left_wrist", "video.right_wrist")
58
+
59
+
60
+ def _column(episode: TrexEpisode, name: str, dtype=np.float32) -> np.ndarray:
61
+ values = episode.table.column(name).to_numpy(zero_copy_only=False)
62
+ return np.stack([np.asarray(row, dtype=dtype) for row in values])
63
+
64
+
65
+ def _sample(timestamps, anchor, offsets, rate):
66
+ return nearest_timestamp_indices(
67
+ timestamps, uniform_target_times(anchor, offsets, rate)
68
+ )
69
+
70
+
71
+ def _read_frames(episode: TrexEpisode, key: str, indices: np.ndarray) -> np.ndarray:
72
+ import decord
73
+
74
+ reader = decord.VideoReader(episode.video_dirs[key], num_threads=1)
75
+ return reader.get_batch([int(i) for i in indices]).asnumpy().astype(np.uint8)
76
+
77
+
78
+ class ChunkBuilder:
79
+ """Builds training-format (K-block) raw samples from one episode."""
80
+
81
+ def __init__(self, dataset_root: str, episode_index: int = 0) -> None:
82
+ self.root = dataset_root
83
+ self.episode = TrexEpisode(dataset_root, episode_index)
84
+ self.stats = TrexRuntimeStatistics.from_dataset(dataset_root)
85
+ self.timestamps = _column(self.episode, "timestamp", np.float64).reshape(-1)
86
+ self.state = _column(self.episode, "observation.state_eef62")
87
+ self.action_abs = _column(self.episode, "action.eef62_absolute")
88
+ self.track_xy = _column(self.episode, "observation.track_xy")
89
+ self.track_vis = _column(self.episode, "observation.track_visibility")
90
+ self.force = _column(self.episode, "observation.tactile_force").reshape(
91
+ -1, 10, 6
92
+ )
93
+ self._deform: np.ndarray | None = None
94
+
95
+ def valid_anchor_times(self, blocks: int) -> list[float]:
96
+ future = max(
97
+ blocks * ACTION_HORIZON / ACTION_RATE_HZ,
98
+ blocks * VIDEO_FRAMES_PER_BLOCK / VIDEO_RATE_HZ,
99
+ )
100
+ grid = np.arange(
101
+ self.timestamps[0], self.timestamps[-1] + 1e-9, 1.0 / ACTION_RATE_HZ
102
+ )
103
+ return [float(t) for t in grid if t + future <= self.timestamps[-1]]
104
+
105
+ def deform_frames(self, size: int = 96) -> np.ndarray:
106
+ if self._deform is None:
107
+ import cv2
108
+ import decord
109
+
110
+ streams = []
111
+ for key in DEFORM_VIDEO_KEYS:
112
+ path = (
113
+ Path(self.root) / "videos" / "chunk-000"
114
+ / f"observation.images.{key}"
115
+ / f"episode_{self.episode.episode_index:06d}.mp4"
116
+ )
117
+ reader = decord.VideoReader(str(path), num_threads=1)
118
+ frames = reader.get_batch(range(len(reader))).asnumpy()
119
+ frames = np.stack(
120
+ [cv2.resize(f, (size, size), interpolation=cv2.INTER_AREA)
121
+ for f in frames]
122
+ )
123
+ streams.append(frames.astype(np.uint8))
124
+ self._deform = np.stack(streams, axis=1)
125
+ return self._deform
126
+
127
+ def _norm_force(self, selection) -> np.ndarray:
128
+ values = self.stats.normalize_force(self.force[selection.indices])
129
+ values[selection.padding_mask] = 0.0
130
+ return values
131
+
132
+ def build(self, anchor: float, *, blocks: int, prompt: str,
133
+ with_deform: bool, history_only_video: bool = False) -> dict:
134
+ ts = self.timestamps
135
+ block_anchors = [
136
+ anchor + b * ACTION_HORIZON / ACTION_RATE_HZ for b in range(blocks)
137
+ ]
138
+ action_sel = _sample(ts, anchor, range(blocks * ACTION_HORIZON), ACTION_RATE_HZ)
139
+ state_sel = _sample(
140
+ ts, anchor, range(0, blocks * ACTION_HORIZON, ACTION_HORIZON),
141
+ ACTION_RATE_HZ,
142
+ )
143
+ reference = self.state[state_sel.indices]
144
+ absolute = self.action_abs[action_sel.indices].reshape(
145
+ blocks, ACTION_HORIZON, 62
146
+ )
147
+ delta = np.stack(
148
+ [eef62_delta_base(reference[b], absolute[b]) for b in range(blocks)]
149
+ ).reshape(blocks * ACTION_HORIZON, 62)
150
+
151
+ past_sels = [
152
+ _sample(ts, b, range(-(FORCE_HISTORY_FRAMES - 1), 1), ACTION_RATE_HZ)
153
+ for b in block_anchors
154
+ ]
155
+ future_sels = [
156
+ _sample(ts, b, range(TRACK_HORIZON), ACTION_RATE_HZ)
157
+ for b in block_anchors
158
+ ]
159
+ force_sels = [
160
+ [
161
+ _sample(ts, b + off / ACTION_RATE_HZ,
162
+ range(-(FORCE_HISTORY_FRAMES - 1), 1), TACTILE_RATE_HZ)
163
+ for off in FORCE_OFFSETS
164
+ ]
165
+ for b in block_anchors
166
+ ]
167
+ force_history = np.stack(
168
+ [[self._norm_force(sel) for sel in block_sel] for block_sel in force_sels]
169
+ )
170
+
171
+ video_hist = _sample(ts, anchor, range(1), VIDEO_RATE_HZ)
172
+ if history_only_video:
173
+ video_indices = video_hist.indices
174
+ else:
175
+ video_future = _sample(
176
+ ts, anchor, range(1, blocks * VIDEO_FRAMES_PER_BLOCK + 1),
177
+ VIDEO_RATE_HZ,
178
+ )
179
+ video_indices = np.concatenate(
180
+ (video_hist.indices, video_future.indices)
181
+ )
182
+
183
+ raw: dict[str, object] = {
184
+ key: _read_frames(self.episode, key, video_indices)
185
+ for key in VIDEO_KEYS
186
+ }
187
+ raw.update(
188
+ {
189
+ "state.eef62": reference.astype(np.float32),
190
+ "action.eef62": delta.astype(np.float32),
191
+ "track_past_xy": np.stack(
192
+ [self.track_xy[s.indices] for s in past_sels]
193
+ ),
194
+ "track_past_visibility": np.stack(
195
+ [self.track_vis[s.indices] * (~s.padding_mask[:, None])
196
+ for s in past_sels]
197
+ ),
198
+ "track_future_xy": np.stack(
199
+ [self.track_xy[s.indices] for s in future_sels]
200
+ ),
201
+ "track_future_visibility": np.stack(
202
+ [self.track_vis[s.indices] for s in future_sels]
203
+ ),
204
+ "current_force": force_history[:, :, -1],
205
+ "force_history": force_history,
206
+ "force_history_padding_mask": np.stack(
207
+ [[sel.padding_mask for sel in block_sel]
208
+ for block_sel in force_sels]
209
+ ),
210
+ "annotation.task": prompt,
211
+ }
212
+ )
213
+ if with_deform:
214
+ deform = self.deform_frames()
215
+ refresh = np.stack(
216
+ [[int(sel.indices[-1]) for sel in block_sel]
217
+ for block_sel in force_sels]
218
+ )
219
+ raw["deform_current"] = deform[refresh]
220
+ return raw
221
+
222
+
223
+ def load_pipeline(checkpoint: Path, *, training: bool):
224
+ cfg_dir = checkpoint / "experiment_cfg"
225
+ if not cfg_dir.exists():
226
+ cfg_dir = checkpoint.parent / "experiment_cfg"
227
+ cfg = OmegaConf.load(cfg_dir / "conf.yaml")
228
+ with open(cfg_dir / "metadata.json", "r", encoding="utf-8") as handle:
229
+ metadata = DatasetMetadata.model_validate(
230
+ json.load(handle)[EmbodimentTag.TREX.value]
231
+ )
232
+ transform = instantiate(cfg.transforms["trex"])
233
+ transform.set_metadata(metadata)
234
+ transform.train() if training else transform.eval()
235
+ collator = instantiate(cfg.data_collator)
236
+ return transform, collator
237
+
238
+
239
+ def to_batch(sample: dict, collator, device, dtype) -> dict:
240
+ batch = collator([sample])
241
+ out = {}
242
+ for key, value in batch.items():
243
+ if torch.is_tensor(value):
244
+ value = (
245
+ value.to(device=device, dtype=dtype)
246
+ if value.is_floating_point()
247
+ else value.to(device=device)
248
+ )
249
+ out[key] = value
250
+ return out
251
+
252
+
253
+ def stage1_attention_masses(policy, batch, *, layers, tau_value):
254
+ from groot.vla.model.trex_track_force import blocks as block_module
255
+
256
+ records: dict[int, dict] = {}
257
+
258
+ def make_patched(layer_index, module):
259
+ def patched(x, *, layout, rope_frequencies, allow_matrix=None, **_):
260
+ batch_size, length = x.shape[:2]
261
+ heads, head_dim = module.num_heads, module.head_dim
262
+ query = module.norm_q(module.q(x)).view(batch_size, length, heads, head_dim)
263
+ key = module.norm_k(module.k(x)).view(batch_size, length, heads, head_dim)
264
+ value = module.v(x).view(batch_size, length, heads, head_dim)
265
+ query = block_module.apply_multimodal_rope(
266
+ query, rope_frequencies
267
+ ).type_as(value)
268
+ key = block_module.apply_multimodal_rope(
269
+ key, rope_frequencies
270
+ ).type_as(value)
271
+ scores = torch.einsum("blhd,bmhd->bhlm", query, key) * head_dim**-0.5
272
+ scores = scores.float().masked_fill(
273
+ ~allow_matrix.view(1, 1, length, length), float("-inf")
274
+ )
275
+ attention = scores.softmax(dim=-1)
276
+ token_type, _ = layout.token_metadata(device=x.device)
277
+ layer_record = {}
278
+ for query_group, query_type in (
279
+ ("action", TokenType.ACTION), ("obs", TokenType.OBS),
280
+ ):
281
+ rows = (token_type == int(query_type)).nonzero(as_tuple=True)[0]
282
+ row_attention = attention[:, :, rows]
283
+ masses = {}
284
+ for name, key_type in (
285
+ ("cond_obs", TokenType.CONDITIONING_OBS),
286
+ ("obs", TokenType.OBS),
287
+ ("action", TokenType.ACTION),
288
+ ("state", TokenType.STATE),
289
+ ("track_past", TokenType.TRACK_PAST),
290
+ ("track_future", TokenType.TRACK_FUTURE),
291
+ ):
292
+ cols = (token_type == int(key_type)).nonzero(as_tuple=True)[0]
293
+ masses[name] = (
294
+ float(row_attention[..., cols].sum(dim=-1).mean().item())
295
+ if cols.numel()
296
+ else 0.0
297
+ )
298
+ layer_record[query_group] = masses
299
+ records[layer_index] = layer_record
300
+ output = torch.einsum(
301
+ "bhlm,bmhd->blhd", attention.to(value.dtype), value
302
+ ).reshape(batch_size, length, module.dim)
303
+ return module.o(output), None
304
+
305
+ return patched
306
+
307
+ originals = {}
308
+ for layer_index in layers:
309
+ module = policy.model.blocks[layer_index].self_attn
310
+ originals[layer_index] = module.forward
311
+ module.forward = make_patched(layer_index, module)
312
+ try:
313
+ tau = torch.full(
314
+ (1, AR_BLOCKS), tau_value, device=policy.device, dtype=policy.dtype
315
+ )
316
+ with torch.inference_mode():
317
+ policy.forward_core(batch, tau=tau)
318
+ finally:
319
+ for layer_index, forward in originals.items():
320
+ policy.model.blocks[layer_index].self_attn.forward = forward
321
+ return records
322
+
323
+
324
+ class Stage2Recorder:
325
+ """Wrap each force-transformer layer to record action-query attention."""
326
+
327
+ def __init__(self, force):
328
+ self.force = force
329
+ self.groups = {"action": force.action_horizon,
330
+ "f6": force.force_sensor_count,
331
+ "vq": force.force_sensor_count}
332
+ if force.use_deform_tactile:
333
+ self.groups["deform"] = force.force_sensor_count
334
+ self.records: list[dict[str, float]] = []
335
+ self._originals = []
336
+
337
+ def __enter__(self):
338
+ for layer in self.force.transformer.layers:
339
+ self._originals.append((layer, layer.forward))
340
+ layer.forward = self._make(layer)
341
+ return self
342
+
343
+ def __exit__(self, *exc):
344
+ for layer, forward in self._originals:
345
+ layer.forward = forward
346
+
347
+ def _make(self, layer):
348
+ groups = self.groups
349
+ records = self.records
350
+
351
+ def forward(src, src_mask=None, src_key_padding_mask=None, is_causal=False):
352
+ x = src
353
+ normed = layer.norm1(x)
354
+ attn_out, weights = layer.self_attn(
355
+ normed, normed, normed,
356
+ attn_mask=src_mask,
357
+ key_padding_mask=src_key_padding_mask,
358
+ need_weights=True,
359
+ average_attn_weights=True,
360
+ )
361
+ action_rows = weights[:, : groups["action"]]
362
+ cursor, masses = 0, {}
363
+ for name, size in groups.items():
364
+ masses[name] = float(
365
+ action_rows[..., cursor:cursor + size].sum(-1).mean().item()
366
+ )
367
+ cursor += size
368
+ masses["memory"] = float(action_rows[..., cursor:].sum(-1).mean().item())
369
+ records.append(masses)
370
+ x = x + layer.dropout1(attn_out)
371
+ x = x + layer._ff_block(layer.norm2(x))
372
+ return x
373
+
374
+ return forward
375
+
376
+
377
+ def refine_with_keep(policy, state_batch, refinement, *, keep: bool,
378
+ deform_images=None):
379
+ """Mirror refine_action_suffix but force the tactile keep mask."""
380
+
381
+ force = policy.force_transformer
382
+ action = pad_action_62_to_64(refinement["coarse_action"]).clone()
383
+ action[..., policy.config.physical_action_dim:] = 0
384
+ keep_mask = torch.full(
385
+ (action.shape[0],), 1.0 if keep else 0.0,
386
+ device=policy.device, dtype=policy.dtype,
387
+ )
388
+ for step in policy.schedule.iter_steps("force"):
389
+ flow = force(
390
+ action,
391
+ step.tau,
392
+ refinement["current_force"],
393
+ refinement["history"],
394
+ coarse_memory=refinement["memory"],
395
+ update_offset=0,
396
+ tactile_keep_mask=keep_mask,
397
+ tactile_history_valid_mask=refinement["history_valid"],
398
+ deform_images=deform_images,
399
+ )
400
+ action = euler_flow_step(action, flow, step.tau, step.tau_next)
401
+ action[..., policy.config.physical_action_dim:] = 0
402
+ return action
403
+
404
+
405
+ def main() -> None:
406
+ parser = argparse.ArgumentParser()
407
+ parser.add_argument("--dataset-root", required=True)
408
+ parser.add_argument("--run", action="append", required=True)
409
+ parser.add_argument("--out", required=True)
410
+ parser.add_argument("--forward-anchors", type=int, default=6)
411
+ parser.add_argument("--openloop-anchors", type=int, default=16)
412
+ parser.add_argument("--tau-grid", default="0.8,0.6,0.4,0.2,0.05")
413
+ parser.add_argument("--tau-repeats", type=int, default=4)
414
+ parser.add_argument("--attn-layers", default="0,14,29")
415
+ parser.add_argument("--prompt", default=None)
416
+ args = parser.parse_args()
417
+
418
+ device = torch.device("cuda")
419
+ builder = ChunkBuilder(args.dataset_root)
420
+ prompt = args.prompt
421
+ if prompt is None:
422
+ with open(Path(args.dataset_root) / "meta" / "tasks.jsonl") as handle:
423
+ prompt = json.loads(handle.readline())["task"]
424
+
425
+ anchors_k4 = builder.valid_anchor_times(AR_BLOCKS)
426
+ forward_anchor_times = [
427
+ anchors_k4[int(i)]
428
+ for i in np.linspace(0, len(anchors_k4) - 1, args.forward_anchors)
429
+ ]
430
+ anchors_k1 = builder.valid_anchor_times(1)
431
+ openloop_anchor_times = [
432
+ anchors_k1[int(i)]
433
+ for i in np.linspace(0, len(anchors_k1) - 1, args.openloop_anchors)
434
+ ]
435
+ tau_grid = [float(v) for v in args.tau_grid.split(",")]
436
+ attn_layers = [int(v) for v in args.attn_layers.split(",")]
437
+
438
+ results: dict[str, dict] = {}
439
+ for spec in args.run:
440
+ name, _, checkpoint = spec.partition("=")
441
+ checkpoint_path = Path(checkpoint)
442
+ print(f"=== {name}: {checkpoint_path} ===", flush=True)
443
+ model = TrexTrackForceVLA.load_lora(str(checkpoint_path))
444
+ model = model.to(device=device, dtype=torch.bfloat16)
445
+ model.eval()
446
+ policy = model.action_head
447
+ use_force = policy.config.use_force
448
+ use_deform = policy.config.use_deform_tactile
449
+ transform, collator = load_pipeline(checkpoint_path, training=True)
450
+ transform_eval, _ = load_pipeline(checkpoint_path, training=False)
451
+
452
+ record: dict[str, object] = {
453
+ "checkpoint": str(checkpoint_path),
454
+ "use_track": policy.config.use_track,
455
+ "use_force": use_force,
456
+ "use_deform_tactile": use_deform,
457
+ }
458
+
459
+ # ---------------- controlled forward losses ----------------------
460
+ forward_batches = []
461
+ for anchor in forward_anchor_times:
462
+ raw = builder.build(anchor, blocks=AR_BLOCKS, prompt=prompt,
463
+ with_deform=use_deform)
464
+ forward_batches.append(
465
+ to_batch(transform(dict(raw)), collator, device, torch.bfloat16)
466
+ )
467
+ tau_table = {}
468
+ for tau_value in tau_grid:
469
+ metrics: dict[str, list[float]] = {}
470
+ for repeat in range(args.tau_repeats):
471
+ for batch_index, batch in enumerate(forward_batches):
472
+ torch.manual_seed(100000 + repeat * 1000 + batch_index * 10)
473
+ tau = torch.full((1, AR_BLOCKS), tau_value,
474
+ device=device, dtype=torch.bfloat16)
475
+ with torch.inference_mode():
476
+ out = policy.forward_core(batch, tau=tau)
477
+ for key in ("action_loss", "dynamics_loss",
478
+ "track_flow_loss", "force_loss"):
479
+ metrics.setdefault(key, []).append(float(out[key]))
480
+ tau_table[f"{tau_value:g}"] = {
481
+ key: [float(np.mean(v)), float(np.std(v))]
482
+ for key, v in metrics.items()
483
+ }
484
+ record["forward_tau_losses"] = tau_table
485
+ print(f" forward losses done", flush=True)
486
+
487
+ # ---------------- open-loop chunk reconstruction -----------------
488
+ arm_dims = list(range(0, 9)) + list(range(31, 40))
489
+ hand_dims = list(range(9, 31)) + list(range(40, 62))
490
+ openloop: dict[str, list[float]] = {}
491
+ split_metrics: dict[str, list[float]] = {}
492
+ refinement_state = None
493
+ for anchor_index, anchor in enumerate(openloop_anchor_times):
494
+ raw = builder.build(anchor, blocks=1, prompt=prompt,
495
+ with_deform=use_deform, history_only_video=True)
496
+ sample = transform_eval(dict(raw))
497
+ gt = np.asarray(sample["action"], dtype=np.float32)[..., :62]
498
+ gt = torch.as_tensor(gt.reshape(-1, 62)[:ACTION_HORIZON])
499
+ batch = to_batch(sample, collator, device, torch.bfloat16)
500
+ if use_deform and "deform_current" in batch:
501
+ batch["deform_current"] = batch["deform_current"][:, :1, :1]
502
+ modes = [("cascade", True)] if use_force else []
503
+ modes.append(("coarse_only", False))
504
+ for mode_name, refine in modes:
505
+ with torch.inference_mode():
506
+ result = policy.sample(
507
+ batch, seed=500 + anchor_index,
508
+ run_force_refinement=refine,
509
+ return_refinement_state=refine and anchor_index == 0,
510
+ )
511
+ if refine and anchor_index == 0:
512
+ current, history, valid = policy._extract_force_inputs(batch, 1)
513
+ refinement_state = {
514
+ "coarse_action": result["coarse_action_at_split"],
515
+ "memory": result["coarse_memory"],
516
+ "current_force": current[:, 0, 0].to(policy.device,
517
+ policy.dtype),
518
+ "history": history[:, 0, 0].to(policy.device),
519
+ "history_valid": (
520
+ None if valid is None
521
+ else valid[:, 0, 0].to(policy.device)
522
+ ),
523
+ "deform": (
524
+ policy._extract_deform_images(batch, 1, 1)
525
+ if use_deform else None
526
+ ),
527
+ }
528
+ pred = result["action_pred"][0, :, :62].float().cpu()
529
+ openloop.setdefault(mode_name, []).append(
530
+ float(((pred - gt) ** 2).mean())
531
+ )
532
+ prefix = "cascade" if mode_name == "cascade" else "coarse"
533
+ split_metrics.setdefault(f"{prefix}_arm", []).append(
534
+ float(((pred[:, arm_dims] - gt[:, arm_dims]) ** 2).mean())
535
+ )
536
+ split_metrics.setdefault(f"{prefix}_hand", []).append(
537
+ float(((pred[:, hand_dims] - gt[:, hand_dims]) ** 2).mean())
538
+ )
539
+ record["openloop_action_mse"] = {
540
+ key: [float(np.mean(v)), float(np.std(v)), len(v)]
541
+ for key, v in openloop.items()
542
+ }
543
+ record["openloop_action_mse_split"] = {
544
+ key: float(np.mean(v)) for key, v in split_metrics.items()
545
+ }
546
+ print(f" open-loop done", flush=True)
547
+
548
+ # ---------------- attention masses --------------------------------
549
+ record["stage1_attention"] = {
550
+ str(layer): masses
551
+ for layer, masses in stage1_attention_masses(
552
+ policy, forward_batches[0], layers=attn_layers, tau_value=0.4
553
+ ).items()
554
+ }
555
+ if use_force and refinement_state is not None:
556
+ with Stage2Recorder(policy.force_transformer) as recorder:
557
+ refine_with_keep(policy, None, refinement_state, keep=True,
558
+ deform_images=refinement_state["deform"])
559
+ per_layer = recorder.records[: len(
560
+ policy.force_transformer.transformer.layers
561
+ )]
562
+ record["stage2_attention"] = per_layer
563
+
564
+ refined_real = refine_with_keep(
565
+ policy, None, refinement_state, keep=True,
566
+ deform_images=refinement_state["deform"],
567
+ )
568
+ refined_masked = refine_with_keep(
569
+ policy, None, refinement_state, keep=False,
570
+ deform_images=refinement_state["deform"],
571
+ )
572
+ coarse = pad_action_62_to_64(refinement_state["coarse_action"])
573
+ delta_tactile = (refined_real - refined_masked)[..., :62].float()
574
+ delta_refine = (refined_real - coarse)[..., :62].float()
575
+ record["tactile_sensitivity"] = {
576
+ "mean_abs_delta_vs_masked": float(delta_tactile.abs().mean()),
577
+ "max_abs_delta_vs_masked": float(delta_tactile.abs().max()),
578
+ "mean_abs_refinement": float(delta_refine.abs().mean()),
579
+ }
580
+ print(f" attention/sensitivity done", flush=True)
581
+
582
+ results[name] = record
583
+ del model, policy, forward_batches
584
+ gc.collect()
585
+ torch.cuda.empty_cache()
586
+
587
+ with open(args.out, "w", encoding="utf-8") as handle:
588
+ json.dump(results, handle, indent=1)
589
+ print(f"wrote {args.out}")
590
+
591
+
592
+ if __name__ == "__main__":
593
+ main()