Compare commits

...

2 Commits

Author SHA1 Message Date
Steven Palma fdd9d92349 feat(policies): early stopping + termination 2026-07-31 16:53:37 +02:00
Thomas Landeg 50192947fd perf(pi0fast): stop FAST decoding at the end-of-action marker
The decode loop always runs all max_decoding_steps (256) tokens, but
detokenize_actions() cuts the output at the first "|", so most of them get
generated and then thrown away. On LIBERO the marker lands around token 25-35.

Stopping there gives the same actions ~4x faster end to end. Checked on
libero_10: 50/50 episodes came out identical to the old path, same success
and same state trajectories.
2026-07-31 16:49:34 +02:00
@@ -604,6 +604,12 @@ class PI0FastPytorch(nn.Module): # see openpi `PI0Pytorch`
) -> torch.Tensor:
"""
Optimized autoregressive decoding for FAST tokens using KV Caching.
Greedy decoding stops once every sequence emits the end-of-action marker. The
returned tensor keeps its fixed shape, with positions not generated after the
batch-wide stop left zero-filled. Stochastic decoding always runs to
``max_decoding_steps`` so early stopping does not change the RNG state used by
subsequent calls.
"""
if max_decoding_steps is None:
max_decoding_steps = self.config.max_action_tokens
@@ -612,6 +618,12 @@ class PI0FastPytorch(nn.Module): # see openpi `PI0Pytorch`
device = tokens.device
lm_head = self.paligemma_with_expert.paligemma.lm_head
# detokenize_actions() cuts at the first "|", so greedy decoding can stop once
# every sequence has emitted it. Keep stochastic decoding unchanged because
# skipping multinomial calls would shift the RNG state for subsequent calls.
end_of_action_token_id = self._paligemma_tokenizer.convert_tokens_to_ids("|")
finished = torch.zeros(bsize, dtype=torch.bool, device=device) if temperature == 0 else None
# --- 1. PREFILL PHASE ---
# Process Images + Text Prompt + BOS token once to populate the KV cache.
@@ -663,6 +675,10 @@ class PI0FastPytorch(nn.Module): # see openpi `PI0Pytorch`
# Initialize storage for generated tokens
generated_action_tokens = torch.zeros((bsize, max_decoding_steps), dtype=torch.long, device=device)
generated_action_tokens[:, 0] = next_token.squeeze(-1)
if finished is not None:
finished |= next_token.squeeze(-1) == end_of_action_token_id
if bool(finished.all()):
return generated_action_tokens
# Track valid tokens mask (0 for pad, 1 for valid)
# We need this to tell the new token what it can attend to (images + text + past actions)
@@ -713,6 +729,11 @@ class PI0FastPytorch(nn.Module): # see openpi `PI0Pytorch`
generated_action_tokens[:, t] = next_token.squeeze(-1)
if finished is not None:
finished |= next_token.squeeze(-1) == end_of_action_token_id
if bool(finished.all()):
break
return generated_action_tokens