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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
raise ValueError(
|
| 99 |
-
"selection_mode must be 'policy', 'field', '
|
|
|
|
| 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
|
| 138 |
if all_groups:
|
| 139 |
-
raise ValueError("
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
| 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"
|