mirror of
https://github.com/huggingface/lerobot.git
synced 2026-05-21 11:39:50 +00:00
fix(datasets): render flow-only low_level recipes instead of dropping them
A recipe whose only supervision is the action-expert flow loss (e.g.
`low_level_execution`: `user(${subtask})` with `stream: low_level` and no
`target` turn) was rejected at render time by `_render_message_recipe` and
`_validate_rendered`, both of which required at least one target turn.
The result: every blend draw of the flow-only recipe rendered to `None`,
`predict_actions` was never set, `run_flow` never fired, and the action
expert received no flow loss — leaving it at random init. Both gates now
also accept a `low_level`-stream turn as valid supervision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -376,7 +376,15 @@ def _render_message_recipe(
|
|||||||
if turn.target:
|
if turn.target:
|
||||||
target_indices.append(message_idx)
|
target_indices.append(message_idx)
|
||||||
|
|
||||||
if not target_indices:
|
# A render is meaningful if it supervises *something*: either a
|
||||||
|
# text-CE target turn, or a ``low_level`` stream turn (flow / action
|
||||||
|
# supervision — e.g. the flow-only ``low_level_execution`` recipe,
|
||||||
|
# ``user(${subtask})`` with ``stream: low_level`` and no target).
|
||||||
|
# Without this, a flow-only recipe renders to ``None`` every time
|
||||||
|
# the blend draws it → ``predict_actions`` is never True → the
|
||||||
|
# action expert never receives a flow loss.
|
||||||
|
has_low_level = any(stream == "low_level" for stream in streams)
|
||||||
|
if not target_indices and not has_low_level:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
rendered = {
|
rendered = {
|
||||||
@@ -433,8 +441,13 @@ def _validate_rendered(rendered: RenderedMessages) -> None:
|
|||||||
|
|
||||||
if len(streams) != len(messages):
|
if len(streams) != len(messages):
|
||||||
raise ValueError("message_streams must be aligned with messages.")
|
raise ValueError("message_streams must be aligned with messages.")
|
||||||
if not target_indices:
|
# Valid iff it supervises something: a text-CE target turn OR a
|
||||||
raise ValueError("Rendered samples must contain at least one target message.")
|
# ``low_level`` stream turn (flow / action supervision).
|
||||||
|
if not target_indices and not any(s == "low_level" for s in streams):
|
||||||
|
raise ValueError(
|
||||||
|
"Rendered samples must contain a target message or a "
|
||||||
|
"low_level-stream message."
|
||||||
|
)
|
||||||
for idx in target_indices:
|
for idx in target_indices:
|
||||||
if idx < 0 or idx >= len(messages):
|
if idx < 0 or idx >= len(messages):
|
||||||
raise ValueError(f"Target message index {idx} is out of bounds.")
|
raise ValueError(f"Target message index {idx} is out of bounds.")
|
||||||
|
|||||||
@@ -370,6 +370,38 @@ def test_resolve_task_explicit_override_beats_rephrasings():
|
|||||||
assert rendered["messages"][0]["content"] == "explicit override wins"
|
assert rendered["messages"][0]["content"] == "explicit override wins"
|
||||||
|
|
||||||
|
|
||||||
|
def test_flow_only_low_level_recipe_renders_without_target():
|
||||||
|
"""Regression: a flow-only ``low_level`` recipe has no ``target`` turn —
|
||||||
|
its supervision is the action-expert flow loss, not text-CE. It must
|
||||||
|
still render (not ``None``), otherwise every blend draw of it is dropped
|
||||||
|
and the action expert never receives a flow loss."""
|
||||||
|
recipe = TrainingRecipe(
|
||||||
|
messages=[
|
||||||
|
MessageTurn(
|
||||||
|
role="user",
|
||||||
|
content="${subtask}",
|
||||||
|
stream="low_level",
|
||||||
|
if_present="subtask",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bindings={"subtask": "active_at(t, style=subtask)"},
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered = render_sample(
|
||||||
|
recipe=recipe,
|
||||||
|
persistent=PERSISTENT,
|
||||||
|
events=[],
|
||||||
|
t=0.5,
|
||||||
|
sample_idx=0,
|
||||||
|
task="clean kitchen",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rendered is not None
|
||||||
|
assert rendered["messages"] == [{"role": "user", "content": "subtask 0"}]
|
||||||
|
assert rendered["message_streams"] == ["low_level"]
|
||||||
|
assert rendered["target_message_indices"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_canonical_recipe_can_render_low_level_branch():
|
def test_canonical_recipe_can_render_low_level_branch():
|
||||||
recipe = TrainingRecipe.from_yaml(Path("src/lerobot/configs/recipes/pi05_hirobot.yaml"))
|
recipe = TrainingRecipe.from_yaml(Path("src/lerobot/configs/recipes/pi05_hirobot.yaml"))
|
||||||
low_level = TrainingRecipe(blend={"low": recipe.blend["low_level_execution"]})
|
low_level = TrainingRecipe(blend={"low": recipe.blend["low_level_execution"]})
|
||||||
|
|||||||
Reference in New Issue
Block a user