anhtld commited on
Commit
52ed8f7
·
verified ·
1 Parent(s): a25c3f8

Auto-sync: 2026-06-27 15:04:36

Browse files
dovla_cil/eval/maniskill_policy_rollout.py CHANGED
@@ -55,6 +55,11 @@ def evaluate_maniskill_policy_rollout(
55
  num_candidates: int = 1,
56
  candidate_sigma: float = 0.2,
57
  selection_seed: int = 0,
 
 
 
 
 
58
  lattice_exclude_types: tuple[str, ...] = (),
59
  ) -> dict[str, Any]:
60
  """Execute a checkpoint policy from restored ManiSkill CIL states.
@@ -80,6 +85,17 @@ def evaluate_maniskill_policy_rollout(
80
  training-split state with the same task rather than the evaluated state's own lattice. This
81
  tests whether the field can use reusable intervention proposals without same-state proposal
82
  leakage.
 
 
 
 
 
 
 
 
 
 
 
83
  """
84
 
85
  try:
@@ -94,12 +110,30 @@ def evaluate_maniskill_policy_rollout(
94
 
95
  if group_batch_size <= 0:
96
  raise ValueError("group_batch_size must be positive")
97
- if selection_mode not in {"policy", "field", "lattice", "retrieval_lattice"}:
 
 
 
 
 
 
 
98
  raise ValueError(
99
- "selection_mode must be 'policy', 'field', 'lattice', or 'retrieval_lattice'"
 
100
  )
101
  if num_candidates <= 0:
102
  raise ValueError("num_candidates must be positive")
 
 
 
 
 
 
 
 
 
 
103
  if selection_mode == "policy":
104
  num_candidates = 1
105
  checkpoint = torch.load(
@@ -112,6 +146,8 @@ def evaluate_maniskill_policy_rollout(
112
  model = DoVLAModel(model_config).to(resolved_device)
113
  load_model_state(model, checkpoint)
114
  model.eval()
 
 
115
 
116
  trainer_config = checkpoint.get("trainer_config", {})
117
  dataset = CILDataset(dataset_dir)
@@ -134,16 +170,26 @@ def evaluate_maniskill_policy_rollout(
134
  group_ids,
135
  observation_mode=model_config.observation_mode,
136
  )
137
- if selection_mode == "retrieval_lattice":
138
  if all_groups:
139
- raise ValueError("retrieval_lattice requires a held-out validation split")
140
- cases = _attach_retrieved_lattice_candidates(
141
- dataset,
142
- cases,
143
- heldout_group_ids=split_group_ids,
144
- obs_dim=model_config.obs_dim,
145
- observation_mode=model_config.observation_mode,
146
- )
 
 
 
 
 
 
 
 
 
 
147
  by_task: dict[str, list[_RolloutCase]] = defaultdict(list)
148
  for case in cases:
149
  by_task[case.task_id].append(case)
@@ -167,13 +213,17 @@ def evaluate_maniskill_policy_rollout(
167
  num_candidates=num_candidates,
168
  candidate_sigma=candidate_sigma,
169
  selection_seed=selection_seed,
 
 
 
 
170
  lattice_exclude_types=lattice_exclude_types,
171
  )
172
  rows.extend(task_rows)
173
  task_summaries[task_id] = _summarize_rows(task_rows)
174
 
175
  effective_num_candidates = num_candidates
176
- if selection_mode in {"lattice", "retrieval_lattice"}:
177
  effective_num_candidates = max(
178
  [int(row.get("lattice_candidate_count", 0)) for row in rows],
179
  default=0,
@@ -191,7 +241,24 @@ def evaluate_maniskill_policy_rollout(
191
  "group_batch_size": group_batch_size,
192
  "selection_mode": selection_mode,
193
  "num_candidates": effective_num_candidates,
194
- "candidate_sigma": candidate_sigma if selection_mode == "field" else 0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  "lattice_exclude_types": list(lattice_exclude_types),
196
  "policy_rollout_success_rate": _mean([row["success"] for row in rows]),
197
  "policy_rollout_progress": _mean([row["progress"] for row in rows]),
@@ -332,6 +399,86 @@ def _attach_retrieved_lattice_candidates(
332
  return output
333
 
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  def _evaluate_task_cases(
336
  task_id: str,
337
  cases: list[_RolloutCase],
@@ -348,6 +495,10 @@ def _evaluate_task_cases(
348
  num_candidates: int = 1,
349
  candidate_sigma: float = 0.2,
350
  selection_seed: int = 0,
 
 
 
 
351
  lattice_exclude_types: tuple[str, ...] = (),
352
  ) -> list[dict[str, Any]]:
353
  rows: list[dict[str, Any]] = []
@@ -406,6 +557,10 @@ def _evaluate_task_cases(
406
  num_candidates=num_candidates,
407
  candidate_sigma=candidate_sigma,
408
  selection_seed=selection_seed + start,
 
 
 
 
409
  action_low=action_low,
410
  action_high=action_high,
411
  action_candidates=(
@@ -416,6 +571,7 @@ def _evaluate_task_cases(
416
  )
417
  if selection_mode == "lattice"
418
  or selection_mode == "retrieval_lattice"
 
419
  else None
420
  ),
421
  candidate_mask=(
@@ -425,7 +581,7 @@ def _evaluate_task_cases(
425
  device=device,
426
  exclude_types=lattice_exclude_types,
427
  )
428
- if selection_mode in {"lattice", "retrieval_lattice"}
429
  and lattice_exclude_types
430
  else None
431
  ),
@@ -494,6 +650,10 @@ def _select_action_chunk(
494
  num_candidates: int,
495
  candidate_sigma: float,
496
  selection_seed: int,
 
 
 
 
497
  action_low: Any | None = None,
498
  action_high: Any | None = None,
499
  action_candidates: Any | None = None,
@@ -503,8 +663,11 @@ def _select_action_chunk(
503
 
504
  In ``policy`` mode this is just the deterministic policy mean. In ``field`` mode we draw
505
  ``num_candidates`` proposals (mean + Gaussian noise) and keep, per state, the chunk whose
506
- learned interventional-field potential is largest. Only the model's own generations are
507
- scored, so no dataset candidate ever leaks into the deployed action.
 
 
 
508
  """
509
 
510
  if selection_mode in {"lattice", "retrieval_lattice"}:
@@ -523,9 +686,59 @@ def _select_action_chunk(
523
 
524
  policy_mean = model.forward_policy(observations, instructions)
525
  batch_size = policy_mean.shape[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  if selection_mode == "policy" or num_candidates <= 1:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  return policy_mean, np.zeros(batch_size, dtype=np.int64)
528
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  generator = torch.Generator(device=policy_mean.device)
530
  generator.manual_seed(int(selection_seed))
531
  candidates = [_clamp_action_tensor(policy_mean, action_low=action_low, action_high=action_high)]
@@ -566,6 +779,155 @@ def _select_action_chunk(
566
  return best_actions, best_index.detach().cpu().numpy()
567
 
568
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  def _select_lattice_action_chunk(
570
  model: DoVLAModel,
571
  observations: Any,
@@ -606,6 +968,44 @@ def _select_lattice_action_chunk(
606
  return best_actions, best_index.detach().cpu().numpy()
607
 
608
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  def _lattice_candidate_mask(
610
  batch: list[_RolloutCase],
611
  *,
@@ -633,6 +1033,12 @@ def _selected_candidate_type(
633
  return "policy_continuous"
634
  if selection_mode == "field":
635
  return "field_selected"
 
 
 
 
 
 
636
  if 0 <= selected_index < len(case.candidate_types):
637
  return f"lattice_{case.candidate_types[selected_index]}"
638
  return "lattice_unknown"
 
55
  num_candidates: int = 1,
56
  candidate_sigma: float = 0.2,
57
  selection_seed: int = 0,
58
+ field_optim_steps: int = 0,
59
+ field_optim_step_size: float = 0.05,
60
+ field_optim_trust_radius: float = 0.5,
61
+ field_optim_l2_penalty: float = 0.0,
62
+ retrieval_neighbors: int = 1,
63
  lattice_exclude_types: tuple[str, ...] = (),
64
  ) -> dict[str, Any]:
65
  """Execute a checkpoint policy from restored ManiSkill CIL states.
 
85
  training-split state with the same task rather than the evaluated state's own lattice. This
86
  tests whether the field can use reusable intervention proposals without same-state proposal
87
  leakage.
88
+
89
+ When ``selection_mode == 'retrieval_residual'`` the evaluator retrieves counterfactual
90
+ action residuals (candidate minus expert action) from the nearest training-split state(s),
91
+ adds those residuals around the current policy mean, scores the resulting local proposal
92
+ lattice with the learned field, and executes the best chunk. This keeps the proposal
93
+ geometry counterfactual while avoiding same-state validation candidates.
94
+
95
+ When ``selection_mode == 'field_optim'`` the evaluator starts from the policy mean plus
96
+ optional Gaussian multi-start proposals, performs projected gradient ascent on the learned
97
+ field potential in action space, and executes the best optimized chunk. This is still
98
+ deployment-clean: no dataset action candidates or rewards are consulted.
99
  """
100
 
101
  try:
 
110
 
111
  if group_batch_size <= 0:
112
  raise ValueError("group_batch_size must be positive")
113
+ if selection_mode not in {
114
+ "policy",
115
+ "field",
116
+ "field_optim",
117
+ "lattice",
118
+ "retrieval_lattice",
119
+ "retrieval_residual",
120
+ }:
121
  raise ValueError(
122
+ "selection_mode must be 'policy', 'field', 'field_optim', 'lattice', "
123
+ "'retrieval_lattice', or 'retrieval_residual'"
124
  )
125
  if num_candidates <= 0:
126
  raise ValueError("num_candidates must be positive")
127
+ if field_optim_steps < 0:
128
+ raise ValueError("field_optim_steps must be non-negative")
129
+ if field_optim_step_size < 0:
130
+ raise ValueError("field_optim_step_size must be non-negative")
131
+ if field_optim_trust_radius < 0:
132
+ raise ValueError("field_optim_trust_radius must be non-negative")
133
+ if field_optim_l2_penalty < 0:
134
+ raise ValueError("field_optim_l2_penalty must be non-negative")
135
+ if retrieval_neighbors <= 0:
136
+ raise ValueError("retrieval_neighbors must be positive")
137
  if selection_mode == "policy":
138
  num_candidates = 1
139
  checkpoint = torch.load(
 
146
  model = DoVLAModel(model_config).to(resolved_device)
147
  load_model_state(model, checkpoint)
148
  model.eval()
149
+ for parameter in model.parameters():
150
+ parameter.requires_grad_(False)
151
 
152
  trainer_config = checkpoint.get("trainer_config", {})
153
  dataset = CILDataset(dataset_dir)
 
170
  group_ids,
171
  observation_mode=model_config.observation_mode,
172
  )
173
+ if selection_mode in {"retrieval_lattice", "retrieval_residual"}:
174
  if all_groups:
175
+ raise ValueError(f"{selection_mode} requires a held-out validation split")
176
+ if selection_mode == "retrieval_lattice":
177
+ cases = _attach_retrieved_lattice_candidates(
178
+ dataset,
179
+ cases,
180
+ heldout_group_ids=split_group_ids,
181
+ obs_dim=model_config.obs_dim,
182
+ observation_mode=model_config.observation_mode,
183
+ retrieval_neighbors=retrieval_neighbors,
184
+ )
185
+ else:
186
+ cases = _attach_retrieved_residual_candidates(
187
+ dataset,
188
+ cases,
189
+ heldout_group_ids=split_group_ids,
190
+ obs_dim=model_config.obs_dim,
191
+ observation_mode=model_config.observation_mode,
192
+ )
193
  by_task: dict[str, list[_RolloutCase]] = defaultdict(list)
194
  for case in cases:
195
  by_task[case.task_id].append(case)
 
213
  num_candidates=num_candidates,
214
  candidate_sigma=candidate_sigma,
215
  selection_seed=selection_seed,
216
+ field_optim_steps=field_optim_steps,
217
+ field_optim_step_size=field_optim_step_size,
218
+ field_optim_trust_radius=field_optim_trust_radius,
219
+ field_optim_l2_penalty=field_optim_l2_penalty,
220
  lattice_exclude_types=lattice_exclude_types,
221
  )
222
  rows.extend(task_rows)
223
  task_summaries[task_id] = _summarize_rows(task_rows)
224
 
225
  effective_num_candidates = num_candidates
226
+ if selection_mode in {"lattice", "retrieval_lattice", "retrieval_residual"}:
227
  effective_num_candidates = max(
228
  [int(row.get("lattice_candidate_count", 0)) for row in rows],
229
  default=0,
 
241
  "group_batch_size": group_batch_size,
242
  "selection_mode": selection_mode,
243
  "num_candidates": effective_num_candidates,
244
+ "candidate_sigma": candidate_sigma
245
+ if selection_mode in {"field", "field_optim"}
246
+ else 0.0,
247
+ "field_optim_steps": field_optim_steps
248
+ if selection_mode == "field_optim"
249
+ else 0,
250
+ "field_optim_step_size": field_optim_step_size
251
+ if selection_mode == "field_optim"
252
+ else 0.0,
253
+ "field_optim_trust_radius": field_optim_trust_radius
254
+ if selection_mode == "field_optim"
255
+ else 0.0,
256
+ "field_optim_l2_penalty": field_optim_l2_penalty
257
+ if selection_mode == "field_optim"
258
+ else 0.0,
259
+ "retrieval_neighbors": retrieval_neighbors
260
+ if selection_mode in {"retrieval_lattice", "retrieval_residual"}
261
+ else 0,
262
  "lattice_exclude_types": list(lattice_exclude_types),
263
  "policy_rollout_success_rate": _mean([row["success"] for row in rows]),
264
  "policy_rollout_progress": _mean([row["progress"] for row in rows]),
 
399
  return output
400
 
401
 
402
+ def _attach_retrieved_residual_candidates(
403
+ dataset: CILDataset,
404
+ cases: list[_RolloutCase],
405
+ *,
406
+ heldout_group_ids: list[str],
407
+ obs_dim: int,
408
+ observation_mode: str,
409
+ retrieval_neighbors: int,
410
+ ) -> list[_RolloutCase]:
411
+ if observation_mode != "state":
412
+ raise ValueError("retrieval_residual currently supports state observations only")
413
+ heldout = set(heldout_group_ids)
414
+ bank: dict[str, list[tuple[str, np.ndarray, list[list[list[float]]], list[str]]]] = (
415
+ defaultdict(list)
416
+ )
417
+ for group_id in dataset.group_ids:
418
+ if group_id in heldout:
419
+ continue
420
+ records = dataset.get_group(group_id)
421
+ if not records:
422
+ continue
423
+ task_ids = {record.task_id for record in records}
424
+ if len(task_ids) != 1:
425
+ continue
426
+ anchor = next((record for record in records if record.candidate_type == "expert"), records[0])
427
+ anchor_action = np.asarray(_numeric_action_values(anchor), dtype=np.float32)
428
+ residuals: list[list[list[float]]] = [np.zeros_like(anchor_action).tolist()]
429
+ candidate_types = ["policy_residual"]
430
+ for record in records:
431
+ if record.record_id == anchor.record_id:
432
+ continue
433
+ residual = np.asarray(_numeric_action_values(record), dtype=np.float32) - anchor_action
434
+ residuals.append(residual.tolist())
435
+ candidate_types.append(f"residual_{record.candidate_type}")
436
+ feature = np.asarray(
437
+ vectorize_toy_observation(records[0].observation_inline or {}, obs_dim=obs_dim),
438
+ dtype=np.float32,
439
+ )
440
+ bank[next(iter(task_ids))].append((group_id, feature, residuals, candidate_types))
441
+
442
+ output: list[_RolloutCase] = []
443
+ for case in cases:
444
+ candidates = bank.get(case.task_id, [])
445
+ if not candidates:
446
+ zero = np.zeros_like(np.asarray(case.best_action_values, dtype=np.float32)).tolist()
447
+ output.append(
448
+ replace(
449
+ case,
450
+ candidate_action_values=[zero],
451
+ candidate_types=["policy_residual"],
452
+ candidate_source_group_id=None,
453
+ )
454
+ )
455
+ continue
456
+ query = np.asarray(
457
+ vectorize_toy_observation(case.observation, obs_dim=obs_dim),
458
+ dtype=np.float32,
459
+ )
460
+ nearest = sorted(
461
+ candidates,
462
+ key=lambda item: float(np.mean((item[1] - query) ** 2)),
463
+ )[:retrieval_neighbors]
464
+ source_group_ids: list[str] = []
465
+ residuals: list[list[list[float]]] = []
466
+ candidate_types: list[str] = []
467
+ for source_group_id, _feature, source_residuals, source_candidate_types in nearest:
468
+ source_group_ids.append(source_group_id)
469
+ residuals.extend(source_residuals)
470
+ candidate_types.extend(source_candidate_types)
471
+ output.append(
472
+ replace(
473
+ case,
474
+ candidate_action_values=residuals,
475
+ candidate_types=candidate_types,
476
+ candidate_source_group_id=";".join(source_group_ids),
477
+ )
478
+ )
479
+ return output
480
+
481
+
482
  def _evaluate_task_cases(
483
  task_id: str,
484
  cases: list[_RolloutCase],
 
495
  num_candidates: int = 1,
496
  candidate_sigma: float = 0.2,
497
  selection_seed: int = 0,
498
+ field_optim_steps: int = 0,
499
+ field_optim_step_size: float = 0.05,
500
+ field_optim_trust_radius: float = 0.5,
501
+ field_optim_l2_penalty: float = 0.0,
502
  lattice_exclude_types: tuple[str, ...] = (),
503
  ) -> list[dict[str, Any]]:
504
  rows: list[dict[str, Any]] = []
 
557
  num_candidates=num_candidates,
558
  candidate_sigma=candidate_sigma,
559
  selection_seed=selection_seed + start,
560
+ field_optim_steps=field_optim_steps,
561
+ field_optim_step_size=field_optim_step_size,
562
+ field_optim_trust_radius=field_optim_trust_radius,
563
+ field_optim_l2_penalty=field_optim_l2_penalty,
564
  action_low=action_low,
565
  action_high=action_high,
566
  action_candidates=(
 
571
  )
572
  if selection_mode == "lattice"
573
  or selection_mode == "retrieval_lattice"
574
+ or selection_mode == "retrieval_residual"
575
  else None
576
  ),
577
  candidate_mask=(
 
581
  device=device,
582
  exclude_types=lattice_exclude_types,
583
  )
584
+ if selection_mode in {"lattice", "retrieval_lattice", "retrieval_residual"}
585
  and lattice_exclude_types
586
  else None
587
  ),
 
650
  num_candidates: int,
651
  candidate_sigma: float,
652
  selection_seed: int,
653
+ field_optim_steps: int = 0,
654
+ field_optim_step_size: float = 0.05,
655
+ field_optim_trust_radius: float = 0.5,
656
+ field_optim_l2_penalty: float = 0.0,
657
  action_low: Any | None = None,
658
  action_high: Any | None = None,
659
  action_candidates: Any | None = None,
 
663
 
664
  In ``policy`` mode this is just the deterministic policy mean. In ``field`` mode we draw
665
  ``num_candidates`` proposals (mean + Gaussian noise) and keep, per state, the chunk whose
666
+ learned interventional-field potential is largest. In ``field_optim`` mode those proposals
667
+ are optimized by projected action-space gradient ascent before scoring. Only the model's
668
+ own generations are scored, so no dataset candidate ever leaks into the deployed action.
669
+ In ``retrieval_residual`` mode, train-split counterfactual residuals are translated around
670
+ the current policy mean before field scoring.
671
  """
672
 
673
  if selection_mode in {"lattice", "retrieval_lattice"}:
 
686
 
687
  policy_mean = model.forward_policy(observations, instructions)
688
  batch_size = policy_mean.shape[0]
689
+ if selection_mode == "retrieval_residual":
690
+ if action_candidates is None:
691
+ raise ValueError("retrieval_residual selection requires action_candidates")
692
+ return _select_residual_lattice_action_chunk(
693
+ model,
694
+ observations,
695
+ instructions,
696
+ policy_mean,
697
+ action_candidates,
698
+ torch=torch,
699
+ action_low=action_low,
700
+ action_high=action_high,
701
+ candidate_mask=candidate_mask,
702
+ )
703
+
704
  if selection_mode == "policy" or num_candidates <= 1:
705
+ if selection_mode == "field_optim":
706
+ return _select_field_optim_action_chunk(
707
+ model,
708
+ observations,
709
+ instructions,
710
+ policy_mean,
711
+ torch=torch,
712
+ num_candidates=1,
713
+ candidate_sigma=candidate_sigma,
714
+ selection_seed=selection_seed,
715
+ action_low=action_low,
716
+ action_high=action_high,
717
+ optim_steps=field_optim_steps,
718
+ optim_step_size=field_optim_step_size,
719
+ optim_trust_radius=field_optim_trust_radius,
720
+ optim_l2_penalty=field_optim_l2_penalty,
721
+ )
722
  return policy_mean, np.zeros(batch_size, dtype=np.int64)
723
 
724
+ if selection_mode == "field_optim":
725
+ return _select_field_optim_action_chunk(
726
+ model,
727
+ observations,
728
+ instructions,
729
+ policy_mean,
730
+ torch=torch,
731
+ num_candidates=num_candidates,
732
+ candidate_sigma=candidate_sigma,
733
+ selection_seed=selection_seed,
734
+ action_low=action_low,
735
+ action_high=action_high,
736
+ optim_steps=field_optim_steps,
737
+ optim_step_size=field_optim_step_size,
738
+ optim_trust_radius=field_optim_trust_radius,
739
+ optim_l2_penalty=field_optim_l2_penalty,
740
+ )
741
+
742
  generator = torch.Generator(device=policy_mean.device)
743
  generator.manual_seed(int(selection_seed))
744
  candidates = [_clamp_action_tensor(policy_mean, action_low=action_low, action_high=action_high)]
 
779
  return best_actions, best_index.detach().cpu().numpy()
780
 
781
 
782
+ def _select_field_optim_action_chunk(
783
+ model: DoVLAModel,
784
+ observations: Any,
785
+ instructions: list[str],
786
+ policy_mean: Any,
787
+ *,
788
+ torch: Any,
789
+ num_candidates: int,
790
+ candidate_sigma: float,
791
+ selection_seed: int,
792
+ action_low: Any | None,
793
+ action_high: Any | None,
794
+ optim_steps: int,
795
+ optim_step_size: float,
796
+ optim_trust_radius: float,
797
+ optim_l2_penalty: float,
798
+ ) -> tuple[Any, np.ndarray]:
799
+ batch_size = policy_mean.shape[0]
800
+ base = _clamp_action_tensor(
801
+ policy_mean.detach(),
802
+ action_low=action_low,
803
+ action_high=action_high,
804
+ )
805
+ generator = torch.Generator(device=base.device)
806
+ generator.manual_seed(int(selection_seed))
807
+
808
+ candidates = base.unsqueeze(1).repeat(1, num_candidates, 1, 1)
809
+ if num_candidates > 1 and candidate_sigma > 0:
810
+ noise = torch.randn(
811
+ candidates.shape,
812
+ generator=generator,
813
+ device=base.device,
814
+ dtype=base.dtype,
815
+ )
816
+ noise[:, 0].zero_()
817
+ candidates = candidates + candidate_sigma * noise
818
+ candidates = _project_action_candidates(
819
+ candidates,
820
+ base,
821
+ action_low=action_low,
822
+ action_high=action_high,
823
+ trust_radius=optim_trust_radius,
824
+ )
825
+
826
+ for _ in range(optim_steps):
827
+ candidates = candidates.detach().requires_grad_(True)
828
+ with torch.enable_grad():
829
+ objective = _field_candidate_objective(
830
+ model,
831
+ observations,
832
+ instructions,
833
+ candidates,
834
+ base,
835
+ torch=torch,
836
+ l2_penalty=optim_l2_penalty,
837
+ )
838
+ grad = torch.autograd.grad(objective.sum(), candidates, only_inputs=True)[0]
839
+ with torch.no_grad():
840
+ step = _normalized_action_gradient(grad, torch=torch) * optim_step_size
841
+ candidates = _project_action_candidates(
842
+ candidates + step,
843
+ base,
844
+ action_low=action_low,
845
+ action_high=action_high,
846
+ trust_radius=optim_trust_radius,
847
+ )
848
+
849
+ with torch.no_grad():
850
+ objective = _field_candidate_objective(
851
+ model,
852
+ observations,
853
+ instructions,
854
+ candidates,
855
+ base,
856
+ torch=torch,
857
+ l2_penalty=optim_l2_penalty,
858
+ )
859
+ best_index = torch.argmax(objective, dim=1)
860
+ batch_index = torch.arange(batch_size, device=candidates.device)
861
+ best_actions = candidates[batch_index, best_index]
862
+ return best_actions, best_index.detach().cpu().numpy()
863
+
864
+
865
+ def _field_candidate_objective(
866
+ model: DoVLAModel,
867
+ observations: Any,
868
+ instructions: list[str],
869
+ candidates: Any,
870
+ base: Any,
871
+ *,
872
+ torch: Any,
873
+ l2_penalty: float,
874
+ ) -> Any:
875
+ if candidates.ndim != 4:
876
+ raise ValueError("candidates must have shape [B,K,H,D]")
877
+ batch_size, candidate_count = candidates.shape[:2]
878
+ flat_candidates = candidates.reshape(
879
+ batch_size * candidate_count,
880
+ candidates.shape[-2],
881
+ candidates.shape[-1],
882
+ )
883
+ flat_observations = observations.repeat_interleave(candidate_count, dim=0)
884
+ flat_instructions = [
885
+ instruction
886
+ for instruction in instructions
887
+ for _ in range(candidate_count)
888
+ ]
889
+ field = model.forward_field(flat_observations, flat_instructions, flat_candidates)
890
+ potential = field["potential"].reshape(batch_size, candidate_count)
891
+ if l2_penalty <= 0:
892
+ return potential
893
+ delta = candidates - base.unsqueeze(1)
894
+ penalty = delta.reshape(batch_size, candidate_count, -1).pow(2).mean(dim=2)
895
+ return potential - float(l2_penalty) * penalty
896
+
897
+
898
+ def _normalized_action_gradient(grad: Any, *, torch: Any) -> Any:
899
+ flat = grad.reshape(*grad.shape[:2], -1)
900
+ scale = flat.abs().amax(dim=2).clamp_min(1e-6).reshape(*grad.shape[:2], 1, 1)
901
+ return torch.nan_to_num(grad / scale)
902
+
903
+
904
+ def _project_action_candidates(
905
+ candidates: Any,
906
+ base: Any,
907
+ *,
908
+ action_low: Any | None,
909
+ action_high: Any | None,
910
+ trust_radius: float,
911
+ ) -> Any:
912
+ if trust_radius > 0:
913
+ low = base.unsqueeze(1) - float(trust_radius)
914
+ high = base.unsqueeze(1) + float(trust_radius)
915
+ candidates = candidates.clamp(min=low, max=high)
916
+ return _clamp_action_tensor(
917
+ candidates,
918
+ action_low=(
919
+ action_low.unsqueeze(1)
920
+ if action_low is not None and action_low.ndim == 3
921
+ else action_low
922
+ ),
923
+ action_high=(
924
+ action_high.unsqueeze(1)
925
+ if action_high is not None and action_high.ndim == 3
926
+ else action_high
927
+ ),
928
+ )
929
+
930
+
931
  def _select_lattice_action_chunk(
932
  model: DoVLAModel,
933
  observations: Any,
 
968
  return best_actions, best_index.detach().cpu().numpy()
969
 
970
 
971
+ def _select_residual_lattice_action_chunk(
972
+ model: DoVLAModel,
973
+ observations: Any,
974
+ instructions: list[str],
975
+ policy_mean: Any,
976
+ action_residuals: Any,
977
+ *,
978
+ torch: Any,
979
+ action_low: Any | None,
980
+ action_high: Any | None,
981
+ candidate_mask: Any | None,
982
+ ) -> tuple[Any, np.ndarray]:
983
+ if action_residuals.ndim != 4:
984
+ raise ValueError("action_residuals must have shape [B,K,H,D]")
985
+ candidates = policy_mean.unsqueeze(1) + action_residuals.to(
986
+ device=policy_mean.device,
987
+ dtype=policy_mean.dtype,
988
+ )
989
+ return _select_lattice_action_chunk(
990
+ model,
991
+ observations,
992
+ instructions,
993
+ candidates,
994
+ torch=torch,
995
+ action_low=(
996
+ action_low.unsqueeze(1)
997
+ if action_low is not None and action_low.ndim == 3
998
+ else action_low
999
+ ),
1000
+ action_high=(
1001
+ action_high.unsqueeze(1)
1002
+ if action_high is not None and action_high.ndim == 3
1003
+ else action_high
1004
+ ),
1005
+ candidate_mask=candidate_mask,
1006
+ )
1007
+
1008
+
1009
  def _lattice_candidate_mask(
1010
  batch: list[_RolloutCase],
1011
  *,
 
1033
  return "policy_continuous"
1034
  if selection_mode == "field":
1035
  return "field_selected"
1036
+ if selection_mode == "field_optim":
1037
+ return "field_optim_selected"
1038
+ if selection_mode == "retrieval_residual":
1039
+ if 0 <= selected_index < len(case.candidate_types):
1040
+ return f"retrieval_residual_{case.candidate_types[selected_index]}"
1041
+ return "retrieval_residual_unknown"
1042
  if 0 <= selected_index < len(case.candidate_types):
1043
  return f"lattice_{case.candidate_types[selected_index]}"
1044
  return "lattice_unknown"