181 lines
6.0 KiB
Python
181 lines
6.0 KiB
Python
import torch
|
|
|
|
from pi07mini.model import (
|
|
HighLevelPolicy,
|
|
Pi07Config,
|
|
Pi07Mini,
|
|
PrefixLayout,
|
|
TinyGoalGenerator,
|
|
)
|
|
from pi07mini.tokenizer import EpisodeMetadata, MiniTokenizer
|
|
|
|
|
|
def tiny_model() -> Pi07Mini:
|
|
tokenizer = MiniTokenizer()
|
|
return Pi07Mini(
|
|
Pi07Config(
|
|
vocab_size=tokenizer.vocab_size,
|
|
image_size=16,
|
|
patch_size=16,
|
|
d_model=32,
|
|
n_heads=4,
|
|
n_layers=1,
|
|
memory_layers=1,
|
|
max_history_frames=3,
|
|
max_text_tokens=64,
|
|
max_action_tokens=4,
|
|
action_start_token_id=tokenizer.ACTION_START,
|
|
)
|
|
)
|
|
|
|
|
|
def sample_sequence():
|
|
tokenizer = MiniTokenizer()
|
|
metadata = EpisodeMetadata(2000, 5, False, "ee", "demonstration")
|
|
return tokenizer, tokenizer.build_policy_sequence(
|
|
"drive", "steer_left", torch.zeros(2, 2), metadata
|
|
)
|
|
|
|
|
|
def common_inputs(sequence):
|
|
return dict(
|
|
pixel_values=torch.randn(1, 3, 16, 16),
|
|
history_pixel_values=torch.randn(1, 2, 3, 16, 16),
|
|
history_states=torch.randn(1, 2, 2),
|
|
current_state=torch.randn(1, 2),
|
|
subgoal_pixel_values=torch.randn(1, 3, 16, 16),
|
|
input_ids=sequence.input_ids[None],
|
|
noisy_actions=torch.randn(1, 2, 2),
|
|
timesteps=torch.rand(1),
|
|
action_prefix_mask=sequence.action_prefix_mask[None],
|
|
)
|
|
|
|
|
|
def test_block_causal_attention_matches_pi07_modal_order() -> None:
|
|
layout = PrefixLayout(observation_tokens=2, goal_tokens=2, text_tokens=3)
|
|
valid = torch.ones(1, layout.total_tokens, dtype=torch.bool)
|
|
edges = Pi07Mini.build_prefix_attention_mask(layout, valid)[0]
|
|
assert edges[0, :2].all()
|
|
assert not edges[0, 2:].any() # observation cannot read goal or text
|
|
assert edges[2, :4].all()
|
|
assert not edges[2, 4:].any() # goal reads observation + goal, not text
|
|
assert edges[4, :5].all()
|
|
assert not edges[4, 5:].any() # first text token is causal
|
|
assert edges[6].all()
|
|
|
|
|
|
def test_prefix_has_fixed_mem_tokens_and_one_token_per_state() -> None:
|
|
_, sequence = sample_sequence()
|
|
model = tiny_model()
|
|
inputs = common_inputs(sequence)
|
|
prefix, _, layout = model.encode_prefix(
|
|
inputs["pixel_values"],
|
|
inputs["history_pixel_values"],
|
|
inputs["history_states"],
|
|
inputs["current_state"],
|
|
inputs["input_ids"],
|
|
inputs["subgoal_pixel_values"],
|
|
)
|
|
assert layout.observation_tokens == 1 + 2 + 1 # MEM + history states + current
|
|
assert layout.goal_tokens == 1
|
|
assert prefix.shape[1] == layout.total_tokens
|
|
|
|
|
|
def test_flow_cannot_read_teacher_forced_fast_tokens() -> None:
|
|
torch.manual_seed(12)
|
|
tokenizer, sequence = sample_sequence()
|
|
model = tiny_model().eval()
|
|
changed_ids = sequence.input_ids.clone()
|
|
changed_ids[sequence.action_start_index + 1 : -1] = tokenizer.ACTION_TOKEN_START
|
|
inputs = common_inputs(sequence)
|
|
with torch.no_grad():
|
|
first = model(**inputs).vector_field
|
|
inputs["input_ids"] = changed_ids[None]
|
|
second = model(**inputs).vector_field
|
|
torch.testing.assert_close(first, second)
|
|
|
|
|
|
def test_flow_loss_is_knowledge_insulated_from_all_vlm_parameters() -> None:
|
|
torch.manual_seed(13)
|
|
_, sequence = sample_sequence()
|
|
model = tiny_model()
|
|
output = model(
|
|
**common_inputs(sequence), target_velocity=torch.randn(1, 2, 2)
|
|
)
|
|
assert output.flow_loss is not None
|
|
output.flow_loss.backward()
|
|
assert all(
|
|
parameter.grad is None or torch.count_nonzero(parameter.grad) == 0
|
|
for parameter in model.vlm_parameters()
|
|
)
|
|
assert any(
|
|
parameter.grad is not None and torch.count_nonzero(parameter.grad) > 0
|
|
for parameter in model.action_parameters()
|
|
)
|
|
|
|
|
|
def test_fast_cross_entropy_still_trains_vlm() -> None:
|
|
_, sequence = sample_sequence()
|
|
model = tiny_model()
|
|
output = model(
|
|
**common_inputs(sequence), text_labels=sequence.labels[None]
|
|
)
|
|
assert output.text_loss is not None
|
|
output.text_loss.backward()
|
|
assert model.token_embedding.weight.grad is not None
|
|
assert torch.count_nonzero(model.token_embedding.weight.grad) > 0
|
|
|
|
|
|
def test_visual_subgoal_conditions_flow_expert() -> None:
|
|
torch.manual_seed(14)
|
|
_, sequence = sample_sequence()
|
|
model = tiny_model().eval()
|
|
inputs = common_inputs(sequence)
|
|
with torch.no_grad():
|
|
first = model(**inputs).vector_field
|
|
inputs["subgoal_pixel_values"] = inputs["subgoal_pixel_values"] + 3.0
|
|
second = model(**inputs).vector_field
|
|
assert not torch.allclose(first, second)
|
|
|
|
|
|
def test_metadata_cfg_uses_paper_extrapolation_formula() -> None:
|
|
conditioned = torch.tensor([1.0, 2.0])
|
|
unconditional = torch.tensor([0.5, 1.5])
|
|
torch.testing.assert_close(
|
|
Pi07Mini.combine_metadata_cfg(conditioned, unconditional, 0.0), conditioned
|
|
)
|
|
torch.testing.assert_close(
|
|
Pi07Mini.combine_metadata_cfg(conditioned, unconditional, 1.0),
|
|
torch.tensor([1.5, 2.5]),
|
|
)
|
|
|
|
|
|
def test_high_level_policy_predicts_next_subtask_token() -> None:
|
|
model = HighLevelPolicy(tiny_model().config)
|
|
output = model(
|
|
pixel_values=torch.randn(2, 3, 16, 16),
|
|
history_pixel_values=torch.randn(2, 2, 3, 16, 16),
|
|
history_states=torch.randn(2, 2, 2),
|
|
current_state=torch.randn(2, 2),
|
|
input_ids=torch.tensor([[256, 257, 264], [256, 257, 264]]),
|
|
target_token=torch.tensor([271, 272]),
|
|
)
|
|
assert output.logits.shape == (2, tiny_model().config.vocab_size)
|
|
assert output.loss is not None
|
|
|
|
|
|
def test_tiny_goal_generator_predicts_image_flow() -> None:
|
|
tokenizer = MiniTokenizer()
|
|
generator = TinyGoalGenerator(tokenizer.vocab_size)
|
|
current = torch.randn(2, 3, 16, 16)
|
|
noisy = torch.randn_like(current)
|
|
output = generator(
|
|
current,
|
|
torch.tensor([[256, 257, 264], [256, 257, 264]]),
|
|
noisy,
|
|
torch.rand(2),
|
|
target_velocity=torch.randn_like(current),
|
|
)
|
|
assert output.vector_field.shape == current.shape
|
|
assert output.loss is not None
|