mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-23 09:46:00 +00:00
feat(molmoact2): add RECAP advantage conditioning via recipe system for MolmoAct2
- Add recipe_path, advantage_prefix, cfg_beta to MolmoAct2Config - Place advantage clause in the assistant section of _build_robot_text - Add MolmoAct2NormalizeTaskStep for consistent task normalization - Parse recipe-rendered advantage in MolmoAct2PackInputsProcessorStep - Insert RenderMessagesStep pipeline when recipe_path is configured - Add recap_advantage_molmoact2.yaml recipe
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# RECAP advantage recipe for MolmoAct2.
|
||||||
|
#
|
||||||
|
# Renders task + advantage into the task field as "<task> Advantage: <value>".
|
||||||
|
# MolmoAct2PackInputsProcessorStep parses this, extracts the advantage value,
|
||||||
|
# and places it AFTER the full user prompt but BEFORE action tokens — matching
|
||||||
|
# the RECAP paper (Section V-B): "The advantage indicator appears in the training
|
||||||
|
# sequence after ˆℓ but before the actions, such that only the action
|
||||||
|
# log-likelihoods are affected."
|
||||||
|
#
|
||||||
|
# Final prompt layout:
|
||||||
|
# <images><|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\nAdvantage: positive. <action_output>...
|
||||||
|
#
|
||||||
|
# When advantage is absent (CFG dropout), if_present guard skips this message
|
||||||
|
# and RenderedMessagesToTaskStep leaves the task unchanged — no advantage clause.
|
||||||
|
|
||||||
|
bindings:
|
||||||
|
advantage: "active_at(t, style=advantage)"
|
||||||
|
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "${task} Advantage: ${advantage}"
|
||||||
|
stream: high_level
|
||||||
|
if_present: advantage
|
||||||
|
|
||||||
|
- role: assistant
|
||||||
|
content: ""
|
||||||
|
stream: low_level
|
||||||
|
target: true
|
||||||
@@ -73,6 +73,19 @@ class MolmoAct2Config(PreTrainedConfig):
|
|||||||
num_inference_steps: int | None = None
|
num_inference_steps: int | None = None
|
||||||
mask_action_dim_padding: bool = True
|
mask_action_dim_padding: bool = True
|
||||||
enable_inference_cuda_graph: bool = True
|
enable_inference_cuda_graph: bool = True
|
||||||
|
|
||||||
|
# Language conditioning (e.g. RECAP advantage). When set, RenderMessagesStep
|
||||||
|
# resolves language_persistent rows via the recipe YAML. Same mechanism as PI05.
|
||||||
|
recipe_path: str | None = None
|
||||||
|
|
||||||
|
# Inference-time advantage indicator (e.g. "Advantage: positive. ").
|
||||||
|
# Used during rollout when no language_persistent data is available.
|
||||||
|
# Placed after the user prompt, before action tokens.
|
||||||
|
advantage_prefix: str = ""
|
||||||
|
|
||||||
|
# Classifier-Free Guidance (CFG) scale for inference (RECAP Eq. 13).
|
||||||
|
# 1.0 = no guidance. >1.0 = dual-path: v = v_uncond + beta * (v_cond - v_uncond)
|
||||||
|
cfg_beta: float = 1.0
|
||||||
# MolmoAct2-local eval option. When enabled, stochastic continuous action
|
# MolmoAct2-local eval option. When enabled, stochastic continuous action
|
||||||
# generation uses a rollout-local generator derived from eval_seed.
|
# generation uses a rollout-local generator derived from eval_seed.
|
||||||
per_episode_seed: bool = False
|
per_episode_seed: bool = False
|
||||||
|
|||||||
@@ -359,6 +359,7 @@ def _build_robot_text(
|
|||||||
add_setup_tokens: bool,
|
add_setup_tokens: bool,
|
||||||
add_control_tokens: bool,
|
add_control_tokens: bool,
|
||||||
num_images: int,
|
num_images: int,
|
||||||
|
advantage: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
setup_text = _wrap_setup_text(setup_type, add_setup_tokens=add_setup_tokens)
|
setup_text = _wrap_setup_text(setup_type, add_setup_tokens=add_setup_tokens)
|
||||||
control_text = _wrap_control_text(control_mode, add_control_tokens=add_control_tokens)
|
control_text = _wrap_control_text(control_mode, add_control_tokens=add_control_tokens)
|
||||||
@@ -375,7 +376,10 @@ def _build_robot_text(
|
|||||||
image_prefix = "<|image|>"
|
image_prefix = "<|image|>"
|
||||||
else:
|
else:
|
||||||
image_prefix = "".join(f"Image {idx + 1}<|image|>" for idx in range(num_images))
|
image_prefix = "".join(f"Image {idx + 1}<|image|>" for idx in range(num_images))
|
||||||
return f"{image_prefix}<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n{ACTION_OUTPUT_TOKEN}"
|
# Per RECAP paper (Section V-B): advantage indicator goes after context,
|
||||||
|
# before actions, so only action log-likelihoods are affected.
|
||||||
|
advantage_clause = f"Advantage: {advantage}. " if advantage else ""
|
||||||
|
return f"{image_prefix}<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n{advantage_clause}{ACTION_OUTPUT_TOKEN}"
|
||||||
|
|
||||||
|
|
||||||
def _as_text_list(value: Any, batch_size: int) -> list[str]:
|
def _as_text_list(value: Any, batch_size: int) -> list[str]:
|
||||||
@@ -695,6 +699,39 @@ class MolmoAct2ClampNormalizedProcessorStep(ProcessorStep):
|
|||||||
return features
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorStepRegistry.register(name="molmoact2_normalize_task")
|
||||||
|
@dataclass
|
||||||
|
class MolmoAct2NormalizeTaskStep(ProcessorStep):
|
||||||
|
"""Normalize the task text in complementary_data before recipe rendering.
|
||||||
|
|
||||||
|
Ensures ${task} in recipe templates gets the same normalized form that
|
||||||
|
MolmoAct2PackInputsProcessorStep would produce, so training prompts
|
||||||
|
match inference prompts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
||||||
|
complementary = transition.get(TransitionKey.COMPLEMENTARY_DATA)
|
||||||
|
if not isinstance(complementary, dict):
|
||||||
|
return transition
|
||||||
|
task = complementary.get("task")
|
||||||
|
if task is None:
|
||||||
|
return transition
|
||||||
|
|
||||||
|
transition = transition.copy()
|
||||||
|
complementary = dict(complementary)
|
||||||
|
if isinstance(task, str):
|
||||||
|
complementary["task"] = _normalize_question_text(task)
|
||||||
|
elif isinstance(task, list):
|
||||||
|
complementary["task"] = [_normalize_question_text(t) for t in task]
|
||||||
|
transition[TransitionKey.COMPLEMENTARY_DATA] = complementary
|
||||||
|
return transition
|
||||||
|
|
||||||
|
def transform_features(
|
||||||
|
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
|
||||||
|
) -> dict[PipelineFeatureType, dict[str, PolicyFeature]]:
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
@ProcessorStepRegistry.register(name="molmoact2_pack_inputs")
|
@ProcessorStepRegistry.register(name="molmoact2_pack_inputs")
|
||||||
@dataclass
|
@dataclass
|
||||||
class MolmoAct2PackInputsProcessorStep(ProcessorStep):
|
class MolmoAct2PackInputsProcessorStep(ProcessorStep):
|
||||||
@@ -715,6 +752,8 @@ class MolmoAct2PackInputsProcessorStep(ProcessorStep):
|
|||||||
chunk_size: int = 30
|
chunk_size: int = 30
|
||||||
max_action_dim: int = 32
|
max_action_dim: int = 32
|
||||||
env_action_dim: int | None = None
|
env_action_dim: int | None = None
|
||||||
|
# RECAP: advantage indicator for inference (e.g. "Advantage: positive. ")
|
||||||
|
advantage_prefix: str = ""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
require_package("transformers", extra="molmoact2")
|
require_package("transformers", extra="molmoact2")
|
||||||
@@ -757,6 +796,7 @@ class MolmoAct2PackInputsProcessorStep(ProcessorStep):
|
|||||||
"chunk_size": self.chunk_size,
|
"chunk_size": self.chunk_size,
|
||||||
"max_action_dim": self.max_action_dim,
|
"max_action_dim": self.max_action_dim,
|
||||||
"env_action_dim": self.env_action_dim,
|
"env_action_dim": self.env_action_dim,
|
||||||
|
"advantage_prefix": self.advantage_prefix,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _resolve_max_sequence_length(
|
def _resolve_max_sequence_length(
|
||||||
@@ -919,8 +959,40 @@ class MolmoAct2PackInputsProcessorStep(ProcessorStep):
|
|||||||
if task_source is None:
|
if task_source is None:
|
||||||
task_source = complementary.get("language_instruction")
|
task_source = complementary.get("language_instruction")
|
||||||
tasks = _as_text_list(task_source, batch_size)
|
tasks = _as_text_list(task_source, batch_size)
|
||||||
if self.normalize_language:
|
|
||||||
tasks = [_normalize_question_text(task) for task in tasks]
|
# Resolve the advantage indicator. Per RECAP paper (Section V-B), it goes
|
||||||
|
# after all context but before actions — handled by _build_robot_text.
|
||||||
|
# Source priority: recipe-rendered "advantage" key > config advantage_prefix.
|
||||||
|
advantages: list[str] = []
|
||||||
|
recipe_rendered = "base_task" in complementary
|
||||||
|
if recipe_rendered:
|
||||||
|
# Recipe rendered the task as "<task> Advantage: <value>".
|
||||||
|
# Extract the advantage value and restore the clean task.
|
||||||
|
clean_tasks: list[str] = []
|
||||||
|
for t in tasks:
|
||||||
|
if " Advantage: " in t:
|
||||||
|
split_idx = t.rindex(" Advantage: ")
|
||||||
|
clean_task = t[:split_idx]
|
||||||
|
adv = t[split_idx + len(" Advantage: ") :]
|
||||||
|
advantages.append(adv)
|
||||||
|
clean_tasks.append(clean_task)
|
||||||
|
else:
|
||||||
|
advantages.append("")
|
||||||
|
clean_tasks.append(t)
|
||||||
|
tasks = clean_tasks
|
||||||
|
else:
|
||||||
|
if self.normalize_language:
|
||||||
|
tasks = [_normalize_question_text(task) for task in tasks]
|
||||||
|
if self.advantage_prefix:
|
||||||
|
# Extract just the value from prefix like "Advantage: positive. "
|
||||||
|
prefix = self.advantage_prefix.strip()
|
||||||
|
if prefix.startswith("Advantage:"):
|
||||||
|
adv_val = prefix[len("Advantage:") :].strip().rstrip(".")
|
||||||
|
else:
|
||||||
|
adv_val = prefix
|
||||||
|
advantages = [adv_val] * batch_size
|
||||||
|
else:
|
||||||
|
advantages = [""] * batch_size
|
||||||
complementary["task"] = tasks
|
complementary["task"] = tasks
|
||||||
|
|
||||||
action_padded = None
|
action_padded = None
|
||||||
@@ -953,6 +1025,7 @@ class MolmoAct2PackInputsProcessorStep(ProcessorStep):
|
|||||||
add_setup_tokens=self.add_setup_tokens,
|
add_setup_tokens=self.add_setup_tokens,
|
||||||
add_control_tokens=self.add_control_tokens,
|
add_control_tokens=self.add_control_tokens,
|
||||||
num_images=len(images),
|
num_images=len(images),
|
||||||
|
advantage=advantages[batch_idx],
|
||||||
)
|
)
|
||||||
prompt_texts.append(prompt)
|
prompt_texts.append(prompt)
|
||||||
if build_action_labels:
|
if build_action_labels:
|
||||||
@@ -1164,28 +1237,48 @@ def make_molmoact2_pre_post_processors(
|
|||||||
stats=masked_dataset_stats,
|
stats=masked_dataset_stats,
|
||||||
),
|
),
|
||||||
MolmoAct2ClampNormalizedProcessorStep(normalization_masks=normalization_masks),
|
MolmoAct2ClampNormalizedProcessorStep(normalization_masks=normalization_masks),
|
||||||
MolmoAct2PackInputsProcessorStep(
|
|
||||||
checkpoint_path=config.checkpoint_path,
|
|
||||||
checkpoint_revision=config.checkpoint_revision,
|
|
||||||
checkpoint_force_download=config.checkpoint_force_download,
|
|
||||||
action_mode=config.action_mode,
|
|
||||||
discrete_action_tokenizer=config.discrete_action_tokenizer,
|
|
||||||
image_keys=image_keys,
|
|
||||||
allow_image_key_fallback=not bool(config.image_keys),
|
|
||||||
setup_type=setup_type,
|
|
||||||
control_mode=control_mode,
|
|
||||||
normalize_language=config.normalize_language,
|
|
||||||
add_setup_tokens=config.add_setup_tokens,
|
|
||||||
add_control_tokens=config.add_control_tokens,
|
|
||||||
num_state_tokens=config.num_state_tokens,
|
|
||||||
max_sequence_length=config.max_sequence_length,
|
|
||||||
chunk_size=chunk_size,
|
|
||||||
max_action_dim=config.expected_max_action_dim,
|
|
||||||
env_action_dim=env_action_dim,
|
|
||||||
),
|
|
||||||
DeviceProcessorStep(device=config.device),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Insert language rendering steps when a recipe is configured (e.g. RECAP advantage)
|
||||||
|
if config.recipe_path is not None:
|
||||||
|
from lerobot.configs.recipe import load_recipe
|
||||||
|
from lerobot.processor.render_messages_processor import RenderMessagesStep
|
||||||
|
from lerobot.processor.rendered_messages_to_task import RenderedMessagesToTaskStep
|
||||||
|
|
||||||
|
recipe = load_recipe(config.recipe_path)
|
||||||
|
# Normalize task text before recipe uses ${task}, ensuring consistency
|
||||||
|
# between training (recipe-rendered) and inference (advantage_prefix).
|
||||||
|
if config.normalize_language:
|
||||||
|
input_steps.append(MolmoAct2NormalizeTaskStep())
|
||||||
|
input_steps.append(RenderMessagesStep(recipe=recipe))
|
||||||
|
input_steps.append(RenderedMessagesToTaskStep())
|
||||||
|
|
||||||
|
input_steps.extend(
|
||||||
|
[
|
||||||
|
MolmoAct2PackInputsProcessorStep(
|
||||||
|
checkpoint_path=config.checkpoint_path,
|
||||||
|
checkpoint_revision=config.checkpoint_revision,
|
||||||
|
checkpoint_force_download=config.checkpoint_force_download,
|
||||||
|
action_mode=config.action_mode,
|
||||||
|
discrete_action_tokenizer=config.discrete_action_tokenizer,
|
||||||
|
image_keys=image_keys,
|
||||||
|
allow_image_key_fallback=not bool(config.image_keys),
|
||||||
|
setup_type=setup_type,
|
||||||
|
control_mode=control_mode,
|
||||||
|
normalize_language=config.normalize_language,
|
||||||
|
add_setup_tokens=config.add_setup_tokens,
|
||||||
|
add_control_tokens=config.add_control_tokens,
|
||||||
|
num_state_tokens=config.num_state_tokens,
|
||||||
|
max_sequence_length=config.max_sequence_length,
|
||||||
|
chunk_size=chunk_size,
|
||||||
|
max_action_dim=config.expected_max_action_dim,
|
||||||
|
env_action_dim=env_action_dim,
|
||||||
|
advantage_prefix=config.advantage_prefix,
|
||||||
|
),
|
||||||
|
DeviceProcessorStep(device=config.device),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
output_steps: list[ProcessorStep] = [
|
output_steps: list[ProcessorStep] = [
|
||||||
MolmoAct2ClampActionProcessorStep(),
|
MolmoAct2ClampActionProcessorStep(),
|
||||||
MolmoAct2MaskedUnnormalizerProcessorStep(
|
MolmoAct2MaskedUnnormalizerProcessorStep(
|
||||||
|
|||||||
Reference in New Issue
Block a user