mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-24 18:26:11 +00:00
9d00293f49
Package fitted tokenizer artifacts with processor pipelines so pretrained PI052 checkpoints restore their saved recipe and normalization state without refitting. Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
5.7 KiB
Python
153 lines
5.7 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import json
|
|
import shutil
|
|
from dataclasses import asdict
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
from lerobot.configs import FeatureType, NormalizationMode, PolicyFeature
|
|
from lerobot.configs.recipe import MessageTurn, TrainingRecipe
|
|
from lerobot.policies import make_pre_post_processors
|
|
from lerobot.processor import ActionTokenizerProcessorStep, DataProcessorPipeline, NormalizerProcessorStep
|
|
from lerobot.processor.converters import identity_transition
|
|
from lerobot.processor.render_messages_processor import RenderMessagesStep
|
|
from lerobot.utils.constants import ACTION
|
|
|
|
|
|
class _ActionTokenizer:
|
|
def __call__(self, actions):
|
|
return np.asarray(actions).round().astype(np.int64)
|
|
|
|
def save_pretrained(self, path):
|
|
path.mkdir(parents=True)
|
|
(path / "processor_config.json").write_text('{"processor_class": "_ActionTokenizer"}\n')
|
|
|
|
|
|
class _PaligemmaTokenizer:
|
|
vocab_size = 4096
|
|
bos_token_id = 2
|
|
|
|
def encode(self, text, **kwargs):
|
|
return [10, 11] if text == "Action: " else [12]
|
|
|
|
|
|
def _make_pipeline(action_tokenizer_path):
|
|
recipe = TrainingRecipe(
|
|
messages=[
|
|
MessageTurn(role="user", content="${task}", stream="high_level"),
|
|
MessageTurn(role="assistant", content="${subtask}", stream="low_level", target=True),
|
|
]
|
|
)
|
|
stats = {ACTION: {"min": torch.tensor([-1.0, -2.0]), "max": torch.tensor([1.0, 2.0])}}
|
|
normalizer = NormalizerProcessorStep(
|
|
features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(2,))},
|
|
norm_map={FeatureType.ACTION: NormalizationMode.MIN_MAX},
|
|
stats=stats,
|
|
)
|
|
action_tokenizer = ActionTokenizerProcessorStep(
|
|
action_tokenizer_name=str(action_tokenizer_path),
|
|
max_action_tokens=16,
|
|
fast_skip_tokens=128,
|
|
)
|
|
return DataProcessorPipeline(
|
|
[normalizer, RenderMessagesStep(recipe), action_tokenizer],
|
|
name="policy_preprocessor",
|
|
to_transition=identity_transition,
|
|
to_output=identity_transition,
|
|
)
|
|
|
|
|
|
def test_pi052_pipeline_embeds_and_loads_fitted_action_tokenizer(tmp_path, monkeypatch):
|
|
original_cache = tmp_path / "original_fast_cache"
|
|
original_cache.mkdir()
|
|
tokenizer = _ActionTokenizer()
|
|
monkeypatch.setattr(
|
|
"lerobot.processor.tokenizer_processor.AutoProcessor.from_pretrained",
|
|
lambda path, **kwargs: tokenizer,
|
|
)
|
|
monkeypatch.setattr(
|
|
"lerobot.processor.tokenizer_processor.AutoTokenizer.from_pretrained",
|
|
lambda *args, **kwargs: _PaligemmaTokenizer(),
|
|
)
|
|
monkeypatch.setattr(
|
|
"lerobot.policies.pi052.fit_fast_tokenizer.fit_fast_tokenizer",
|
|
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("FAST fitting must not run")),
|
|
)
|
|
|
|
pipeline = _make_pipeline(original_cache)
|
|
expected_tokens = pipeline.steps[-1]._tokenize_action(torch.tensor([[[0.2, 0.8]]]))[0]
|
|
expected_recipe = asdict(pipeline.steps[1].recipe)
|
|
expected_state = pipeline.steps[0].state_dict()
|
|
checkpoint = tmp_path / "checkpoint"
|
|
pipeline.save_pretrained(checkpoint)
|
|
DataProcessorPipeline(
|
|
[],
|
|
name="policy_postprocessor",
|
|
to_transition=identity_transition,
|
|
to_output=identity_transition,
|
|
).save_pretrained(checkpoint)
|
|
|
|
saved_config = json.loads((checkpoint / "policy_preprocessor.json").read_text())
|
|
tokenizer_step = saved_config["steps"][2]
|
|
assert tokenizer_step["config"]["action_tokenizer_name"] == "action_tokenizer"
|
|
assert tokenizer_step["artifacts"] == {"action_tokenizer_name": "action_tokenizer"}
|
|
assert (checkpoint / "action_tokenizer" / "processor_config.json").is_file()
|
|
|
|
shutil.rmtree(original_cache)
|
|
loaded, _ = make_pre_post_processors(
|
|
SimpleNamespace(type="pi052", auto_fit_fast_tokenizer=True),
|
|
pretrained_path=str(checkpoint),
|
|
dataset_repo_id="org/dataset-that-must-not-be-read",
|
|
)
|
|
|
|
assert asdict(loaded.steps[1].recipe) == expected_recipe
|
|
for key, tensor in expected_state.items():
|
|
torch.testing.assert_close(loaded.steps[0].state_dict()[key], tensor)
|
|
torch.testing.assert_close(
|
|
loaded.steps[-1]._tokenize_action(torch.tensor([[[0.2, 0.8]]]))[0],
|
|
expected_tokens,
|
|
)
|
|
|
|
|
|
def test_pi052_pipeline_rejects_missing_fitted_action_tokenizer(tmp_path, monkeypatch):
|
|
tokenizer = _ActionTokenizer()
|
|
monkeypatch.setattr(
|
|
"lerobot.processor.tokenizer_processor.AutoProcessor.from_pretrained",
|
|
lambda path, **kwargs: tokenizer,
|
|
)
|
|
monkeypatch.setattr(
|
|
"lerobot.processor.tokenizer_processor.AutoTokenizer.from_pretrained",
|
|
lambda *args, **kwargs: _PaligemmaTokenizer(),
|
|
)
|
|
|
|
pipeline = _make_pipeline(tmp_path / "original_fast_cache")
|
|
checkpoint = tmp_path / "checkpoint"
|
|
pipeline.save_pretrained(checkpoint)
|
|
shutil.rmtree(checkpoint / "action_tokenizer")
|
|
|
|
with pytest.raises(FileNotFoundError, match="Checkpoint artifacts are incomplete"):
|
|
DataProcessorPipeline.from_pretrained(
|
|
checkpoint,
|
|
config_filename="policy_preprocessor.json",
|
|
to_transition=identity_transition,
|
|
to_output=identity_transition,
|
|
)
|