diff --git a/src/lerobot/policies/smolvla2/inference/runtime.py b/src/lerobot/policies/smolvla2/inference/runtime.py index f888b8bc3..3d76015ab 100644 --- a/src/lerobot/policies/smolvla2/inference/runtime.py +++ b/src/lerobot/policies/smolvla2/inference/runtime.py @@ -69,16 +69,18 @@ class SmolVLA2Runtime: _stop: bool = field(default=False, init=False) def __post_init__(self) -> None: + # Pipeline order matters. Both ``HighLevelSubtaskFwd`` and + # ``LowLevelForward`` are gated on "action queue is empty" so + # the slow LLM call (select_message) doesn't starve dispatch. + # If LowLevelForward runs first, it refills the queue and the + # high-level step never sees ``queue == 0`` afterwards. + # + # Order is therefore: high-level steps that read state (subtask, + # memory, interjection, vqa) → low-level chunk refresh → action + # dispatch → tool dispatch. So on an empty-queue tick the + # subtask refreshes first, the new subtask string flows into + # the next chunk's prompt, and DispatchAction drains. self.pipeline = [ - LowLevelForward( - trigger=HzTrigger(self.chunk_hz), - policy=self.policy, - observation_provider=self.observation_provider, - ), - DispatchAction( - trigger=HzTrigger(self.ctrl_hz), - robot_executor=self.robot_executor, - ), HighLevelSubtaskFwd( trigger=HzTrigger(self.high_level_hz), policy=self.policy, @@ -96,6 +98,15 @@ class SmolVLA2Runtime: policy=self.policy, observation_provider=self.observation_provider, ), + LowLevelForward( + trigger=HzTrigger(self.chunk_hz), + policy=self.policy, + observation_provider=self.observation_provider, + ), + DispatchAction( + trigger=HzTrigger(self.ctrl_hz), + robot_executor=self.robot_executor, + ), DispatchToolCalls(tools=self.tools), ] self.state = initial_runtime_state()