mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-07 18:11:50 +00:00
Compare commits
19 Commits
fix/ci-tv5
...
v0.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fff0fde7c | |||
| 04de496547 | |||
| baf9b50365 | |||
| a0fdbf037a | |||
| c085531b17 | |||
| c7c6205332 | |||
| 4e54be1334 | |||
| fde9d08281 | |||
| 46044fed75 | |||
| 975dcad918 | |||
| d0b58190da | |||
| 9a5ab8ffab | |||
| 7541d72130 | |||
| 0317a15bf1 | |||
| f138e5948a | |||
| 8fef4ddab8 | |||
| 18d9cb5ac4 | |||
| 5095ab0845 | |||
| dac1efd13d |
@@ -173,6 +173,8 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
working-directory: /lerobot
|
working-directory: /lerobot
|
||||||
steps:
|
steps:
|
||||||
|
- name: Fix ptxas permissions
|
||||||
|
run: chmod +x /lerobot/.venv/lib/python3.10/site-packages/triton/backends/nvidia/bin/ptxas
|
||||||
- name: Run pytest on GPU
|
- name: Run pytest on GPU
|
||||||
run: pytest tests -vv --maxfail=10
|
run: pytest tests -vv --maxfail=10
|
||||||
- name: Run end-to-end tests
|
- name: Run end-to-end tests
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
include src/lerobot/templates/lerobot_modelcard_template.md
|
include src/lerobot/templates/lerobot_modelcard_template.md
|
||||||
include src/lerobot/datasets/card_template.md
|
include src/lerobot/datasets/card_template.md
|
||||||
|
include src/lerobot/envs/metaworld_config.json
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ RUN if [ "$UNBOUND_DEPS" = "true" ]; then \
|
|||||||
|
|
||||||
RUN uv pip install --no-cache ".[all]"
|
RUN uv pip install --no-cache ".[all]"
|
||||||
|
|
||||||
|
RUN chmod +x /lerobot/.venv/lib/python${PYTHON_VERSION}/site-packages/triton/backends/nvidia/bin/ptxas
|
||||||
|
|
||||||
# Copy the rest of the application source code
|
# Copy the rest of the application source code
|
||||||
# Make sure to have the git-LFS files for testing
|
# Make sure to have the git-LFS files for testing
|
||||||
COPY --chown=user_lerobot:user_lerobot . .
|
COPY --chown=user_lerobot:user_lerobot . .
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class DatasetReplayConfig:
|
|||||||
repo_id: str
|
repo_id: str
|
||||||
# Episode to replay.
|
# Episode to replay.
|
||||||
episode: int
|
episode: int
|
||||||
# Root directory where the dataset will be stored (e.g. 'dataset/path').
|
# Root directory where the dataset will be stored (e.g. 'dataset/path'). If None, defaults to $HF_LEROBOT_HOME/repo_id.
|
||||||
root: str | Path | None = None
|
root: str | Path | None = None
|
||||||
# Limit the frames per second. By default, uses the policy fps.
|
# Limit the frames per second. By default, uses the policy fps.
|
||||||
fps: int = 30
|
fps: int = 30
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ lerobot-edit-dataset="lerobot.scripts.lerobot_edit_dataset:main"
|
|||||||
lerobot-setup-can="lerobot.scripts.lerobot_setup_can:main"
|
lerobot-setup-can="lerobot.scripts.lerobot_setup_can:main"
|
||||||
|
|
||||||
# ---------------- Tool Configurations ----------------
|
# ---------------- Tool Configurations ----------------
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
lerobot = ["envs/*.json"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
|
|||||||
@@ -49,23 +49,18 @@ import torch
|
|||||||
|
|
||||||
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig # noqa: F401
|
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig # noqa: F401
|
||||||
from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraConfig # noqa: F401
|
from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraConfig # noqa: F401
|
||||||
from lerobot.robots import ( # noqa: F401
|
from lerobot.robots import (
|
||||||
Robot,
|
RobotConfig, # noqa: F401
|
||||||
RobotConfig,
|
|
||||||
bi_so_follower,
|
|
||||||
koch_follower,
|
|
||||||
make_robot_from_config,
|
make_robot_from_config,
|
||||||
omx_follower,
|
|
||||||
so_follower,
|
|
||||||
)
|
)
|
||||||
from lerobot.transport import (
|
from lerobot.transport import (
|
||||||
services_pb2, # type: ignore
|
services_pb2, # type: ignore
|
||||||
services_pb2_grpc, # type: ignore
|
services_pb2_grpc, # type: ignore
|
||||||
)
|
)
|
||||||
from lerobot.transport.utils import grpc_channel_options, send_bytes_in_chunks
|
from lerobot.transport.utils import grpc_channel_options, send_bytes_in_chunks
|
||||||
|
from lerobot.utils.import_utils import register_third_party_plugins
|
||||||
|
|
||||||
from .configs import RobotClientConfig
|
from .configs import RobotClientConfig
|
||||||
from .constants import SUPPORTED_ROBOTS
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
Action,
|
Action,
|
||||||
FPSTracker,
|
FPSTracker,
|
||||||
@@ -485,8 +480,9 @@ class RobotClient:
|
|||||||
def async_client(cfg: RobotClientConfig):
|
def async_client(cfg: RobotClientConfig):
|
||||||
logging.info(pformat(asdict(cfg)))
|
logging.info(pformat(asdict(cfg)))
|
||||||
|
|
||||||
if cfg.robot.type not in SUPPORTED_ROBOTS:
|
# TODO: Assert if checking robot support is still needed with the plugin system
|
||||||
raise ValueError(f"Robot {cfg.robot.type} not yet supported!")
|
# if cfg.robot.type not in SUPPORTED_ROBOTS:
|
||||||
|
# raise ValueError(f"Robot {cfg.robot.type} not yet supported!")
|
||||||
|
|
||||||
client = RobotClient(cfg)
|
client = RobotClient(cfg)
|
||||||
|
|
||||||
@@ -512,4 +508,5 @@ def async_client(cfg: RobotClientConfig):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
register_third_party_plugins()
|
||||||
async_client() # run the client
|
async_client() # run the client
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class DatasetConfig:
|
|||||||
# "dataset_index" into the returned item. The index mapping is made according to the order in which the
|
# "dataset_index" into the returned item. The index mapping is made according to the order in which the
|
||||||
# datasets are provided.
|
# datasets are provided.
|
||||||
repo_id: str
|
repo_id: str
|
||||||
# Root directory where the dataset will be stored (e.g. 'dataset/path').
|
# Root directory where the dataset will be stored (e.g. 'dataset/path'). If None, defaults to $HF_LEROBOT_HOME/repo_id.
|
||||||
root: str | None = None
|
root: str | None = None
|
||||||
episodes: list[int] | None = None
|
episodes: list[int] | None = None
|
||||||
image_transforms: ImageTransformsConfig = field(default_factory=ImageTransformsConfig)
|
image_transforms: ImageTransformsConfig = field(default_factory=ImageTransformsConfig)
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
|
|
||||||
This dataset was created using [LeRobot](https://github.com/huggingface/lerobot).
|
This dataset was created using [LeRobot](https://github.com/huggingface/lerobot).
|
||||||
|
|
||||||
|
{% if repo_id is defined and repo_id %}
|
||||||
|
<a class="flex" href="https://huggingface.co/spaces/lerobot/visualize_dataset?path={{ repo_id }}">
|
||||||
|
<img class="block dark:hidden" src="https://huggingface.co/datasets/huggingface/badges/resolve/main/visualize-this-dataset-xl.svg"/>
|
||||||
|
<img class="hidden dark:block" src="https://huggingface.co/datasets/huggingface/badges/resolve/main/visualize-this-dataset-xl-dark.svg"/>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
## Dataset Description
|
## Dataset Description
|
||||||
|
|
||||||
{{ dataset_description | default("", true) }}
|
{{ dataset_description | default("", true) }}
|
||||||
|
|||||||
@@ -567,20 +567,22 @@ def _copy_and_reindex_data(
|
|||||||
def _keep_episodes_from_video_with_av(
|
def _keep_episodes_from_video_with_av(
|
||||||
input_path: Path,
|
input_path: Path,
|
||||||
output_path: Path,
|
output_path: Path,
|
||||||
episodes_to_keep: list[tuple[float, float]],
|
episodes_to_keep: list[tuple[int, int]],
|
||||||
fps: float,
|
fps: float,
|
||||||
vcodec: str = "libsvtav1",
|
vcodec: str = "libsvtav1",
|
||||||
pix_fmt: str = "yuv420p",
|
pix_fmt: str = "yuv420p",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Keep only specified episodes from a video file using PyAV.
|
"""Keep only specified episodes from a video file using PyAV.
|
||||||
|
|
||||||
This function decodes frames from specified time ranges and re-encodes them with
|
This function decodes frames from specified frame ranges and re-encodes them with
|
||||||
properly reset timestamps to ensure monotonic progression.
|
properly reset timestamps to ensure monotonic progression.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_path: Source video file path.
|
input_path: Source video file path.
|
||||||
output_path: Destination video file path.
|
output_path: Destination video file path.
|
||||||
episodes_to_keep: List of (start_time, end_time) tuples for episodes to keep.
|
episodes_to_keep: List of (start_frame, end_frame) tuples for episodes to keep.
|
||||||
|
Ranges are half-open intervals: [start_frame, end_frame), where start_frame
|
||||||
|
is inclusive and end_frame is exclusive.
|
||||||
fps: Frame rate of the video.
|
fps: Frame rate of the video.
|
||||||
vcodec: Video codec to use for encoding.
|
vcodec: Video codec to use for encoding.
|
||||||
pix_fmt: Pixel format for output video.
|
pix_fmt: Pixel format for output video.
|
||||||
@@ -622,9 +624,10 @@ def _keep_episodes_from_video_with_av(
|
|||||||
|
|
||||||
# Create set of (start, end) ranges for fast lookup.
|
# Create set of (start, end) ranges for fast lookup.
|
||||||
# Convert to a sorted list for efficient checking.
|
# Convert to a sorted list for efficient checking.
|
||||||
time_ranges = sorted(episodes_to_keep)
|
frame_ranges = sorted(episodes_to_keep)
|
||||||
|
|
||||||
# Track frame index for setting PTS and current range being processed.
|
# Track frame index for setting PTS and current range being processed.
|
||||||
|
src_frame_count = 0
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
range_idx = 0
|
range_idx = 0
|
||||||
|
|
||||||
@@ -634,21 +637,20 @@ def _keep_episodes_from_video_with_av(
|
|||||||
if frame is None:
|
if frame is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get frame timestamp.
|
# Check if frame is in any of our desired frame ranges.
|
||||||
frame_time = float(frame.pts * frame.time_base) if frame.pts is not None else 0.0
|
|
||||||
|
|
||||||
# Check if frame is in any of our desired time ranges.
|
|
||||||
# Skip ranges that have already passed.
|
# Skip ranges that have already passed.
|
||||||
while range_idx < len(time_ranges) and frame_time >= time_ranges[range_idx][1]:
|
while range_idx < len(frame_ranges) and src_frame_count >= frame_ranges[range_idx][1]:
|
||||||
range_idx += 1
|
range_idx += 1
|
||||||
|
|
||||||
# If we've passed all ranges, stop processing.
|
# If we've passed all ranges, stop processing.
|
||||||
if range_idx >= len(time_ranges):
|
if range_idx >= len(frame_ranges):
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check if frame is in current range.
|
# Check if frame is in current range.
|
||||||
start_ts, end_ts = time_ranges[range_idx]
|
start_frame = frame_ranges[range_idx][0]
|
||||||
if frame_time < start_ts:
|
|
||||||
|
if src_frame_count < start_frame:
|
||||||
|
src_frame_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Frame is in range - create a new frame with reset timestamps.
|
# Frame is in range - create a new frame with reset timestamps.
|
||||||
@@ -661,6 +663,7 @@ def _keep_episodes_from_video_with_av(
|
|||||||
for pkt in v_out.encode(new_frame):
|
for pkt in v_out.encode(new_frame):
|
||||||
out.mux(pkt)
|
out.mux(pkt)
|
||||||
|
|
||||||
|
src_frame_count += 1
|
||||||
frame_count += 1
|
frame_count += 1
|
||||||
|
|
||||||
# Flush encoder.
|
# Flush encoder.
|
||||||
@@ -749,15 +752,17 @@ def _copy_and_reindex_videos(
|
|||||||
f"videos/{video_key}/to_timestamp"
|
f"videos/{video_key}/to_timestamp"
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# Build list of time ranges to keep, in sorted order.
|
# Build list of frame ranges to keep, in sorted order.
|
||||||
sorted_keep_episodes = sorted(episodes_in_file, key=lambda x: episode_mapping[x])
|
sorted_keep_episodes = sorted(episodes_in_file, key=lambda x: episode_mapping[x])
|
||||||
episodes_to_keep_ranges: list[tuple[float, float]] = []
|
episodes_to_keep_ranges: list[tuple[int, int]] = []
|
||||||
|
|
||||||
for old_idx in sorted_keep_episodes:
|
for old_idx in sorted_keep_episodes:
|
||||||
src_ep = src_dataset.meta.episodes[old_idx]
|
src_ep = src_dataset.meta.episodes[old_idx]
|
||||||
from_ts = src_ep[f"videos/{video_key}/from_timestamp"]
|
from_frame = round(src_ep[f"videos/{video_key}/from_timestamp"] * src_dataset.meta.fps)
|
||||||
to_ts = src_ep[f"videos/{video_key}/to_timestamp"]
|
to_frame = round(src_ep[f"videos/{video_key}/to_timestamp"] * src_dataset.meta.fps)
|
||||||
episodes_to_keep_ranges.append((from_ts, to_ts))
|
assert src_ep["length"] == to_frame - from_frame, (
|
||||||
|
f"Episode length mismatch: {src_ep['length']} vs {to_frame - from_frame}"
|
||||||
|
)
|
||||||
|
episodes_to_keep_ranges.append((from_frame, to_frame))
|
||||||
|
|
||||||
# Use PyAV filters to efficiently re-encode only the desired segments.
|
# Use PyAV filters to efficiently re-encode only the desired segments.
|
||||||
assert src_dataset.meta.video_path is not None
|
assert src_dataset.meta.video_path is not None
|
||||||
|
|||||||
@@ -664,11 +664,11 @@ class LeRobotDataset(torch.utils.data.Dataset):
|
|||||||
for the README).
|
for the README).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repo_id (str): This is the repo id that will be used to fetch the dataset. Locally, the dataset
|
repo_id (str): This is the repo id that will be used to fetch the dataset.
|
||||||
will be stored under root/repo_id.
|
root (Path | None, optional): Local directory where the dataset will be downloaded and
|
||||||
root (Path | None, optional): Local directory to use for downloading/writing files. You can also
|
stored. If set, all dataset files will be stored directly under this path. If not set, the
|
||||||
set the HF_LEROBOT_HOME environment variable to point to a different location. Defaults to
|
dataset files will be stored under $HF_LEROBOT_HOME/repo_id (configurable via the
|
||||||
'~/.cache/huggingface/lerobot'.
|
HF_LEROBOT_HOME environment variable).
|
||||||
episodes (list[int] | None, optional): If specified, this will only load episodes specified by
|
episodes (list[int] | None, optional): If specified, this will only load episodes specified by
|
||||||
their episode_index in this list. Defaults to None.
|
their episode_index in this list. Defaults to None.
|
||||||
image_transforms (Callable | None, optional): You can pass standard v2 image transforms from
|
image_transforms (Callable | None, optional): You can pass standard v2 image transforms from
|
||||||
@@ -747,7 +747,7 @@ class LeRobotDataset(torch.utils.data.Dataset):
|
|||||||
# Check if cached dataset contains all requested episodes
|
# Check if cached dataset contains all requested episodes
|
||||||
if not self._check_cached_episodes_sufficient():
|
if not self._check_cached_episodes_sufficient():
|
||||||
raise FileNotFoundError("Cached dataset doesn't contain all requested episodes")
|
raise FileNotFoundError("Cached dataset doesn't contain all requested episodes")
|
||||||
except (AssertionError, FileNotFoundError, NotADirectoryError):
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
if is_valid_version(self.revision):
|
if is_valid_version(self.revision):
|
||||||
self.revision = get_safe_version(self.repo_id, self.revision)
|
self.revision = get_safe_version(self.repo_id, self.revision)
|
||||||
self.download(download_videos)
|
self.download(download_videos)
|
||||||
@@ -839,7 +839,7 @@ class LeRobotDataset(torch.utils.data.Dataset):
|
|||||||
hub_api.upload_folder(**upload_kwargs)
|
hub_api.upload_folder(**upload_kwargs)
|
||||||
|
|
||||||
card = create_lerobot_dataset_card(
|
card = create_lerobot_dataset_card(
|
||||||
tags=tags, dataset_info=self.meta.info, license=license, **card_kwargs
|
tags=tags, dataset_info=self.meta.info, license=license, repo_id=self.repo_id, **card_kwargs
|
||||||
)
|
)
|
||||||
card.push_to_hub(repo_id=self.repo_id, repo_type="dataset", revision=branch)
|
card.push_to_hub(repo_id=self.repo_id, repo_type="dataset", revision=branch)
|
||||||
|
|
||||||
@@ -1771,11 +1771,12 @@ class MultiLeRobotDataset(torch.utils.data.Dataset):
|
|||||||
)
|
)
|
||||||
for repo_id, ds in zip(self.repo_ids, self._datasets, strict=True):
|
for repo_id, ds in zip(self.repo_ids, self._datasets, strict=True):
|
||||||
extra_keys = set(ds.features).difference(intersection_features)
|
extra_keys = set(ds.features).difference(intersection_features)
|
||||||
logging.warning(
|
if extra_keys:
|
||||||
f"keys {extra_keys} of {repo_id} were disabled as they are not contained in all the "
|
logging.warning(
|
||||||
"other datasets."
|
f"keys {extra_keys} of {repo_id} were disabled as they are not contained in all the "
|
||||||
)
|
"other datasets."
|
||||||
self.disabled_features.update(extra_keys)
|
)
|
||||||
|
self.disabled_features.update(extra_keys)
|
||||||
|
|
||||||
self.image_transforms = image_transforms
|
self.image_transforms = image_transforms
|
||||||
self.delta_timestamps = delta_timestamps
|
self.delta_timestamps = delta_timestamps
|
||||||
|
|||||||
@@ -227,16 +227,17 @@ def decode_video_frames_torchvision(
|
|||||||
min_, argmin_ = dist.min(1)
|
min_, argmin_ = dist.min(1)
|
||||||
|
|
||||||
is_within_tol = min_ < tolerance_s
|
is_within_tol = min_ < tolerance_s
|
||||||
assert is_within_tol.all(), (
|
if not is_within_tol.all():
|
||||||
f"One or several query timestamps unexpectedly violate the tolerance ({min_[~is_within_tol]} > {tolerance_s=})."
|
raise FrameTimestampError(
|
||||||
"It means that the closest frame that can be loaded from the video is too far away in time."
|
f"One or several query timestamps unexpectedly violate the tolerance ({min_[~is_within_tol]} > {tolerance_s=})."
|
||||||
"This might be due to synchronization issues with timestamps during data collection."
|
" It means that the closest frame that can be loaded from the video is too far away in time."
|
||||||
"To be safe, we advise to ignore this item during training."
|
" This might be due to synchronization issues with timestamps during data collection."
|
||||||
f"\nqueried timestamps: {query_ts}"
|
" To be safe, we advise to ignore this item during training."
|
||||||
f"\nloaded timestamps: {loaded_ts}"
|
f"\nqueried timestamps: {query_ts}"
|
||||||
f"\nvideo: {video_path}"
|
f"\nloaded timestamps: {loaded_ts}"
|
||||||
f"\nbackend: {backend}"
|
f"\nvideo: {video_path}"
|
||||||
)
|
f"\nbackend: {backend}"
|
||||||
|
)
|
||||||
|
|
||||||
# get closest frames to the query timestamps
|
# get closest frames to the query timestamps
|
||||||
closest_frames = torch.stack([loaded_frames[idx] for idx in argmin_])
|
closest_frames = torch.stack([loaded_frames[idx] for idx in argmin_])
|
||||||
@@ -248,7 +249,11 @@ def decode_video_frames_torchvision(
|
|||||||
# convert to the pytorch format which is float32 in [0,1] range (and channel first)
|
# convert to the pytorch format which is float32 in [0,1] range (and channel first)
|
||||||
closest_frames = closest_frames.type(torch.float32) / 255
|
closest_frames = closest_frames.type(torch.float32) / 255
|
||||||
|
|
||||||
assert len(timestamps) == len(closest_frames)
|
if len(timestamps) != len(closest_frames):
|
||||||
|
raise FrameTimestampError(
|
||||||
|
f"Number of retrieved frames ({len(closest_frames)}) does not match "
|
||||||
|
f"number of queried timestamps ({len(timestamps)})"
|
||||||
|
)
|
||||||
return closest_frames
|
return closest_frames
|
||||||
|
|
||||||
|
|
||||||
@@ -353,15 +358,16 @@ def decode_video_frames_torchcodec(
|
|||||||
min_, argmin_ = dist.min(1)
|
min_, argmin_ = dist.min(1)
|
||||||
|
|
||||||
is_within_tol = min_ < tolerance_s
|
is_within_tol = min_ < tolerance_s
|
||||||
assert is_within_tol.all(), (
|
if not is_within_tol.all():
|
||||||
f"One or several query timestamps unexpectedly violate the tolerance ({min_[~is_within_tol]} > {tolerance_s=})."
|
raise FrameTimestampError(
|
||||||
"It means that the closest frame that can be loaded from the video is too far away in time."
|
f"One or several query timestamps unexpectedly violate the tolerance ({min_[~is_within_tol]} > {tolerance_s=})."
|
||||||
"This might be due to synchronization issues with timestamps during data collection."
|
" It means that the closest frame that can be loaded from the video is too far away in time."
|
||||||
"To be safe, we advise to ignore this item during training."
|
" This might be due to synchronization issues with timestamps during data collection."
|
||||||
f"\nqueried timestamps: {query_ts}"
|
" To be safe, we advise to ignore this item during training."
|
||||||
f"\nloaded timestamps: {loaded_ts}"
|
f"\nqueried timestamps: {query_ts}"
|
||||||
f"\nvideo: {video_path}"
|
f"\nloaded timestamps: {loaded_ts}"
|
||||||
)
|
f"\nvideo: {video_path}"
|
||||||
|
)
|
||||||
|
|
||||||
# get closest frames to the query timestamps
|
# get closest frames to the query timestamps
|
||||||
closest_frames = torch.stack([loaded_frames[idx] for idx in argmin_])
|
closest_frames = torch.stack([loaded_frames[idx] for idx in argmin_])
|
||||||
|
|||||||
@@ -55,10 +55,16 @@ class DiffusionConfig(PreTrainedConfig):
|
|||||||
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to
|
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to
|
||||||
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX)
|
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX)
|
||||||
vision_backbone: Name of the torchvision resnet backbone to use for encoding images.
|
vision_backbone: Name of the torchvision resnet backbone to use for encoding images.
|
||||||
crop_shape: (H, W) shape to crop images to as a preprocessing step for the vision backbone. Must fit
|
resize_shape: (H, W) shape to resize images to as a preprocessing step for the vision
|
||||||
within the image size. If None, no cropping is done.
|
backbone. If None, no resizing is done and the original image resolution is used.
|
||||||
crop_is_random: Whether the crop should be random at training time (it's always a center crop in eval
|
crop_ratio: Ratio in (0, 1] used to derive the crop size from resize_shape
|
||||||
mode).
|
(crop_h = int(resize_shape[0] * crop_ratio), likewise for width).
|
||||||
|
Set to 1.0 to disable cropping. Only takes effect when resize_shape is not None.
|
||||||
|
crop_shape: (H, W) shape to crop images to. When resize_shape is set and crop_ratio < 1.0,
|
||||||
|
this is computed automatically. Can also be set directly for legacy configs that use
|
||||||
|
crop-only (without resize). If None and no derivation applies, no cropping is done.
|
||||||
|
crop_is_random: Whether the crop should be random at training time (it's always a center
|
||||||
|
crop in eval mode).
|
||||||
pretrained_backbone_weights: Pretrained weights from torchvision to initialize the backbone.
|
pretrained_backbone_weights: Pretrained weights from torchvision to initialize the backbone.
|
||||||
`None` means no pretrained weights.
|
`None` means no pretrained weights.
|
||||||
use_group_norm: Whether to replace batch normalization with group normalization in the backbone.
|
use_group_norm: Whether to replace batch normalization with group normalization in the backbone.
|
||||||
@@ -114,7 +120,9 @@ class DiffusionConfig(PreTrainedConfig):
|
|||||||
# Architecture / modeling.
|
# Architecture / modeling.
|
||||||
# Vision backbone.
|
# Vision backbone.
|
||||||
vision_backbone: str = "resnet18"
|
vision_backbone: str = "resnet18"
|
||||||
crop_shape: tuple[int, int] | None = (84, 84)
|
resize_shape: tuple[int, int] | None = None
|
||||||
|
crop_ratio: float = 1.0
|
||||||
|
crop_shape: tuple[int, int] | None = None
|
||||||
crop_is_random: bool = True
|
crop_is_random: bool = True
|
||||||
pretrained_backbone_weights: str | None = None
|
pretrained_backbone_weights: str | None = None
|
||||||
use_group_norm: bool = True
|
use_group_norm: bool = True
|
||||||
@@ -139,6 +147,10 @@ class DiffusionConfig(PreTrainedConfig):
|
|||||||
# Inference
|
# Inference
|
||||||
num_inference_steps: int | None = None
|
num_inference_steps: int | None = None
|
||||||
|
|
||||||
|
# Optimization
|
||||||
|
compile_model: bool = False
|
||||||
|
compile_mode: str = "reduce-overhead"
|
||||||
|
|
||||||
# Loss computation
|
# Loss computation
|
||||||
do_mask_loss_for_padding: bool = False
|
do_mask_loss_for_padding: bool = False
|
||||||
|
|
||||||
@@ -171,6 +183,25 @@ class DiffusionConfig(PreTrainedConfig):
|
|||||||
f"Got {self.noise_scheduler_type}."
|
f"Got {self.noise_scheduler_type}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.resize_shape is not None and (
|
||||||
|
len(self.resize_shape) != 2 or any(d <= 0 for d in self.resize_shape)
|
||||||
|
):
|
||||||
|
raise ValueError(f"`resize_shape` must be a pair of positive integers. Got {self.resize_shape}.")
|
||||||
|
if not (0 < self.crop_ratio <= 1.0):
|
||||||
|
raise ValueError(f"`crop_ratio` must be in (0, 1]. Got {self.crop_ratio}.")
|
||||||
|
|
||||||
|
if self.resize_shape is not None:
|
||||||
|
if self.crop_ratio < 1.0:
|
||||||
|
self.crop_shape = (
|
||||||
|
int(self.resize_shape[0] * self.crop_ratio),
|
||||||
|
int(self.resize_shape[1] * self.crop_ratio),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Explicitly disable cropping for resize+ratio path when crop_ratio == 1.0.
|
||||||
|
self.crop_shape = None
|
||||||
|
if self.crop_shape is not None and (self.crop_shape[0] <= 0 or self.crop_shape[1] <= 0):
|
||||||
|
raise ValueError(f"`crop_shape` must have positive dimensions. Got {self.crop_shape}.")
|
||||||
|
|
||||||
# Check that the horizon size and U-Net downsampling is compatible.
|
# Check that the horizon size and U-Net downsampling is compatible.
|
||||||
# U-Net downsamples by 2 with each stage.
|
# U-Net downsamples by 2 with each stage.
|
||||||
downsampling_factor = 2 ** len(self.down_dims)
|
downsampling_factor = 2 ** len(self.down_dims)
|
||||||
@@ -198,13 +229,12 @@ class DiffusionConfig(PreTrainedConfig):
|
|||||||
if len(self.image_features) == 0 and self.env_state_feature is None:
|
if len(self.image_features) == 0 and self.env_state_feature is None:
|
||||||
raise ValueError("You must provide at least one image or the environment state among the inputs.")
|
raise ValueError("You must provide at least one image or the environment state among the inputs.")
|
||||||
|
|
||||||
if self.crop_shape is not None:
|
if self.resize_shape is None and self.crop_shape is not None:
|
||||||
for key, image_ft in self.image_features.items():
|
for key, image_ft in self.image_features.items():
|
||||||
if self.crop_shape[0] > image_ft.shape[1] or self.crop_shape[1] > image_ft.shape[2]:
|
if self.crop_shape[0] > image_ft.shape[1] or self.crop_shape[1] > image_ft.shape[2]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"`crop_shape` should fit within the images shapes. Got {self.crop_shape} "
|
f"`crop_shape` should fit within the image shapes. Got {self.crop_shape} "
|
||||||
f"for `crop_shape` and {image_ft.shape} for "
|
f"for `crop_shape` and {image_ft.shape} for `{key}`."
|
||||||
f"`{key}`."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that all input images have the same shape.
|
# Check that all input images have the same shape.
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ class DiffusionPolicy(PreTrainedPolicy):
|
|||||||
"""Run the batch through the model and compute the loss for training or validation."""
|
"""Run the batch through the model and compute the loss for training or validation."""
|
||||||
if self.config.image_features:
|
if self.config.image_features:
|
||||||
batch = dict(batch) # shallow copy so that adding a key doesn't modify the original
|
batch = dict(batch) # shallow copy so that adding a key doesn't modify the original
|
||||||
|
for key in self.config.image_features:
|
||||||
|
if self.config.n_obs_steps == 1 and batch[key].ndim == 4:
|
||||||
|
batch[key] = batch[key].unsqueeze(1)
|
||||||
batch[OBS_IMAGES] = torch.stack([batch[key] for key in self.config.image_features], dim=-4)
|
batch[OBS_IMAGES] = torch.stack([batch[key] for key in self.config.image_features], dim=-4)
|
||||||
loss = self.diffusion.compute_loss(batch)
|
loss = self.diffusion.compute_loss(batch)
|
||||||
# no output_dict so returning None
|
# no output_dict so returning None
|
||||||
@@ -182,6 +185,11 @@ class DiffusionModel(nn.Module):
|
|||||||
|
|
||||||
self.unet = DiffusionConditionalUnet1d(config, global_cond_dim=global_cond_dim * config.n_obs_steps)
|
self.unet = DiffusionConditionalUnet1d(config, global_cond_dim=global_cond_dim * config.n_obs_steps)
|
||||||
|
|
||||||
|
if config.compile_model:
|
||||||
|
# Compile the U-Net. "reduce-overhead" is preferred for the small-batch repetitive loops
|
||||||
|
# common in diffusion inference.
|
||||||
|
self.unet = torch.compile(self.unet, mode=config.compile_mode)
|
||||||
|
|
||||||
self.noise_scheduler = _make_noise_scheduler(
|
self.noise_scheduler = _make_noise_scheduler(
|
||||||
config.noise_scheduler_type,
|
config.noise_scheduler_type,
|
||||||
num_train_timesteps=config.num_train_timesteps,
|
num_train_timesteps=config.num_train_timesteps,
|
||||||
@@ -446,12 +454,18 @@ class DiffusionRgbEncoder(nn.Module):
|
|||||||
def __init__(self, config: DiffusionConfig):
|
def __init__(self, config: DiffusionConfig):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# Set up optional preprocessing.
|
# Set up optional preprocessing.
|
||||||
if config.crop_shape is not None:
|
if config.resize_shape is not None:
|
||||||
|
self.resize = torchvision.transforms.Resize(config.resize_shape)
|
||||||
|
else:
|
||||||
|
self.resize = None
|
||||||
|
|
||||||
|
crop_shape = config.crop_shape
|
||||||
|
if crop_shape is not None:
|
||||||
self.do_crop = True
|
self.do_crop = True
|
||||||
# Always use center crop for eval
|
# Always use center crop for eval
|
||||||
self.center_crop = torchvision.transforms.CenterCrop(config.crop_shape)
|
self.center_crop = torchvision.transforms.CenterCrop(crop_shape)
|
||||||
if config.crop_is_random:
|
if config.crop_is_random:
|
||||||
self.maybe_random_crop = torchvision.transforms.RandomCrop(config.crop_shape)
|
self.maybe_random_crop = torchvision.transforms.RandomCrop(crop_shape)
|
||||||
else:
|
else:
|
||||||
self.maybe_random_crop = self.center_crop
|
self.maybe_random_crop = self.center_crop
|
||||||
else:
|
else:
|
||||||
@@ -477,13 +491,16 @@ class DiffusionRgbEncoder(nn.Module):
|
|||||||
|
|
||||||
# Set up pooling and final layers.
|
# Set up pooling and final layers.
|
||||||
# Use a dry run to get the feature map shape.
|
# Use a dry run to get the feature map shape.
|
||||||
# The dummy input should take the number of image channels from `config.image_features` and it should
|
# The dummy shape mirrors the runtime preprocessing order: resize -> crop.
|
||||||
# use the height and width from `config.crop_shape` if it is provided, otherwise it should use the
|
|
||||||
# height and width from `config.image_features`.
|
|
||||||
|
|
||||||
# Note: we have a check in the config class to make sure all images have the same shape.
|
# Note: we have a check in the config class to make sure all images have the same shape.
|
||||||
images_shape = next(iter(config.image_features.values())).shape
|
images_shape = next(iter(config.image_features.values())).shape
|
||||||
dummy_shape_h_w = config.crop_shape if config.crop_shape is not None else images_shape[1:]
|
if config.crop_shape is not None:
|
||||||
|
dummy_shape_h_w = config.crop_shape
|
||||||
|
elif config.resize_shape is not None:
|
||||||
|
dummy_shape_h_w = config.resize_shape
|
||||||
|
else:
|
||||||
|
dummy_shape_h_w = images_shape[1:]
|
||||||
dummy_shape = (1, images_shape[0], *dummy_shape_h_w)
|
dummy_shape = (1, images_shape[0], *dummy_shape_h_w)
|
||||||
feature_map_shape = get_output_shape(self.backbone, dummy_shape)[1:]
|
feature_map_shape = get_output_shape(self.backbone, dummy_shape)[1:]
|
||||||
|
|
||||||
@@ -499,7 +516,10 @@ class DiffusionRgbEncoder(nn.Module):
|
|||||||
Returns:
|
Returns:
|
||||||
(B, D) image feature.
|
(B, D) image feature.
|
||||||
"""
|
"""
|
||||||
# Preprocess: maybe crop (if it was set up in the __init__).
|
# Preprocess: resize if configured, then crop if configured.
|
||||||
|
|
||||||
|
if self.resize is not None:
|
||||||
|
x = self.resize(x)
|
||||||
if self.do_crop:
|
if self.do_crop:
|
||||||
if self.training: # noqa: SIM108
|
if self.training: # noqa: SIM108
|
||||||
x = self.maybe_random_crop(x)
|
x = self.maybe_random_crop(x)
|
||||||
|
|||||||
@@ -277,9 +277,7 @@ class SARMEncodingProcessorStep(ProcessorStep):
|
|||||||
|
|
||||||
# When language is perturbed, targets are zero so perturbed samples don't contribute to progress loss
|
# When language is perturbed, targets are zero so perturbed samples don't contribute to progress loss
|
||||||
if self.dataset_meta is not None:
|
if self.dataset_meta is not None:
|
||||||
episodes_df = None
|
episodes_df = self.dataset_meta.episodes.to_pandas()
|
||||||
if self.sparse_subtask_names != ["task"]:
|
|
||||||
episodes_df = self.dataset_meta.episodes.to_pandas()
|
|
||||||
|
|
||||||
# Generate sparse targets
|
# Generate sparse targets
|
||||||
if self.sparse_temporal_proportions is not None:
|
if self.sparse_temporal_proportions is not None:
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ class SmolVLAConfig(PreTrainedConfig):
|
|||||||
# Real-Time Chunking (RTC) configuration
|
# Real-Time Chunking (RTC) configuration
|
||||||
rtc_config: RTCConfig | None = None
|
rtc_config: RTCConfig | None = None
|
||||||
|
|
||||||
|
compile_model: bool = False # Whether to use torch.compile for model optimization
|
||||||
|
compile_mode: str = "max-autotune" # Torch compile mode
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
super().__post_init__()
|
super().__post_init__()
|
||||||
|
|
||||||
|
|||||||
@@ -593,6 +593,12 @@ class VLAFlowMatching(nn.Module):
|
|||||||
self.prefix_length = self.config.prefix_length
|
self.prefix_length = self.config.prefix_length
|
||||||
self.rtc_processor = rtc_processor
|
self.rtc_processor = rtc_processor
|
||||||
|
|
||||||
|
# Compile model if requested
|
||||||
|
if config.compile_model:
|
||||||
|
torch.set_float32_matmul_precision("high")
|
||||||
|
self.sample_actions = torch.compile(self.sample_actions, mode=config.compile_mode)
|
||||||
|
self.forward = torch.compile(self.forward, mode=config.compile_mode)
|
||||||
|
|
||||||
def _rtc_enabled(self):
|
def _rtc_enabled(self):
|
||||||
return self.config.rtc_config is not None and self.config.rtc_config.enabled
|
return self.config.rtc_config is not None and self.config.rtc_config.enabled
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ class SmolVLMWithExpertModel(nn.Module):
|
|||||||
print(f"Loading {model_id} weights ...")
|
print(f"Loading {model_id} weights ...")
|
||||||
self.vlm = AutoModelForImageTextToText.from_pretrained(
|
self.vlm = AutoModelForImageTextToText.from_pretrained(
|
||||||
model_id,
|
model_id,
|
||||||
device_map=device,
|
|
||||||
torch_dtype="bfloat16",
|
torch_dtype="bfloat16",
|
||||||
low_cpu_mem_usage=True,
|
low_cpu_mem_usage=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ from lerobot.teleoperators import ( # noqa: F401
|
|||||||
make_teleoperator_from_config,
|
make_teleoperator_from_config,
|
||||||
omx_leader,
|
omx_leader,
|
||||||
openarm_leader,
|
openarm_leader,
|
||||||
|
openarm_mini,
|
||||||
so_leader,
|
so_leader,
|
||||||
unitree_g1,
|
unitree_g1,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ from lerobot.teleoperators import ( # noqa: F401
|
|||||||
make_teleoperator_from_config,
|
make_teleoperator_from_config,
|
||||||
omx_leader,
|
omx_leader,
|
||||||
openarm_leader,
|
openarm_leader,
|
||||||
|
openarm_mini,
|
||||||
so_leader,
|
so_leader,
|
||||||
)
|
)
|
||||||
from lerobot.utils.robot_utils import precise_sleep
|
from lerobot.utils.robot_utils import precise_sleep
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ from lerobot.teleoperators import ( # noqa: F401
|
|||||||
make_teleoperator_from_config,
|
make_teleoperator_from_config,
|
||||||
omx_leader,
|
omx_leader,
|
||||||
openarm_leader,
|
openarm_leader,
|
||||||
|
openarm_mini,
|
||||||
reachy2_teleoperator,
|
reachy2_teleoperator,
|
||||||
so_leader,
|
so_leader,
|
||||||
unitree_g1,
|
unitree_g1,
|
||||||
@@ -154,7 +155,7 @@ class DatasetRecordConfig:
|
|||||||
repo_id: str
|
repo_id: str
|
||||||
# A short but accurate description of the task performed during the recording (e.g. "Pick the Lego block and drop it in the box on the right.")
|
# A short but accurate description of the task performed during the recording (e.g. "Pick the Lego block and drop it in the box on the right.")
|
||||||
single_task: str
|
single_task: str
|
||||||
# Root directory where the dataset will be stored (e.g. 'dataset/path').
|
# Root directory where the dataset will be stored (e.g. 'dataset/path'). If None, defaults to $HF_LEROBOT_HOME/repo_id.
|
||||||
root: str | Path | None = None
|
root: str | Path | None = None
|
||||||
# Limit the frames per second.
|
# Limit the frames per second.
|
||||||
fps: int = 30
|
fps: int = 30
|
||||||
@@ -333,6 +334,7 @@ def record_loop(
|
|||||||
preprocessor.reset()
|
preprocessor.reset()
|
||||||
postprocessor.reset()
|
postprocessor.reset()
|
||||||
|
|
||||||
|
no_action_count = 0
|
||||||
timestamp = 0
|
timestamp = 0
|
||||||
start_episode_t = time.perf_counter()
|
start_episode_t = time.perf_counter()
|
||||||
while timestamp < control_time_s:
|
while timestamp < control_time_s:
|
||||||
@@ -380,11 +382,13 @@ def record_loop(
|
|||||||
act = {**arm_action, **base_action} if len(base_action) > 0 else arm_action
|
act = {**arm_action, **base_action} if len(base_action) > 0 else arm_action
|
||||||
act_processed_teleop = teleop_action_processor((act, obs))
|
act_processed_teleop = teleop_action_processor((act, obs))
|
||||||
else:
|
else:
|
||||||
logging.info(
|
no_action_count += 1
|
||||||
"No policy or teleoperator provided, skipping action generation."
|
if no_action_count == 1 or no_action_count % 10 == 0:
|
||||||
"This is likely to happen when resetting the environment without a teleop device."
|
logging.warning(
|
||||||
"The robot won't be at its rest position at the start of the next episode."
|
"No policy or teleoperator provided, skipping action generation. "
|
||||||
)
|
"This is likely to happen when resetting the environment without a teleop device. "
|
||||||
|
"The robot won't be at its rest position at the start of the next episode."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Applies a pipeline to the action, default is IdentityProcessor
|
# Applies a pipeline to the action, default is IdentityProcessor
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class DatasetReplayConfig:
|
|||||||
repo_id: str
|
repo_id: str
|
||||||
# Episode to replay.
|
# Episode to replay.
|
||||||
episode: int
|
episode: int
|
||||||
# Root directory where the dataset will be stored (e.g. 'dataset/path').
|
# Root directory where the dataset will be stored (e.g. 'dataset/path'). If None, defaults to $HF_LEROBOT_HOME/repo_id.
|
||||||
root: str | Path | None = None
|
root: str | Path | None = None
|
||||||
# Limit the frames per second. By default, uses the policy fps.
|
# Limit the frames per second. By default, uses the policy fps.
|
||||||
fps: int = 30
|
fps: int = 30
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from lerobot.teleoperators import ( # noqa: F401
|
|||||||
koch_leader,
|
koch_leader,
|
||||||
make_teleoperator_from_config,
|
make_teleoperator_from_config,
|
||||||
omx_leader,
|
omx_leader,
|
||||||
|
openarm_mini,
|
||||||
so_leader,
|
so_leader,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ COMPATIBLE_DEVICES = [
|
|||||||
"koch_leader",
|
"koch_leader",
|
||||||
"omx_follower",
|
"omx_follower",
|
||||||
"omx_leader",
|
"omx_leader",
|
||||||
|
"openarm_mini",
|
||||||
"so100_follower",
|
"so100_follower",
|
||||||
"so100_leader",
|
"so100_leader",
|
||||||
"so101_follower",
|
"so101_follower",
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ from lerobot.teleoperators import ( # noqa: F401
|
|||||||
make_teleoperator_from_config,
|
make_teleoperator_from_config,
|
||||||
omx_leader,
|
omx_leader,
|
||||||
openarm_leader,
|
openarm_leader,
|
||||||
|
openarm_mini,
|
||||||
reachy2_teleoperator,
|
reachy2_teleoperator,
|
||||||
so_leader,
|
so_leader,
|
||||||
unitree_g1,
|
unitree_g1,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import torch
|
|||||||
from accelerate import Accelerator
|
from accelerate import Accelerator
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
from torch.optim import Optimizer
|
from torch.optim import Optimizer
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
from lerobot.configs import parser
|
from lerobot.configs import parser
|
||||||
from lerobot.configs.train import TrainPipelineConfig
|
from lerobot.configs.train import TrainPipelineConfig
|
||||||
@@ -51,6 +52,7 @@ from lerobot.utils.utils import (
|
|||||||
format_big_number,
|
format_big_number,
|
||||||
has_method,
|
has_method,
|
||||||
init_logging,
|
init_logging,
|
||||||
|
inside_slurm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -378,10 +380,10 @@ def train(cfg: TrainPipelineConfig, accelerator: Accelerator | None = None):
|
|||||||
"dataloading_s": AverageMeter("data_s", ":.3f"),
|
"dataloading_s": AverageMeter("data_s", ":.3f"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Use effective batch size for proper epoch calculation in distributed training
|
# Keep global batch size for logging; MetricsTracker handles world size internally.
|
||||||
effective_batch_size = cfg.batch_size * accelerator.num_processes
|
effective_batch_size = cfg.batch_size * accelerator.num_processes
|
||||||
train_tracker = MetricsTracker(
|
train_tracker = MetricsTracker(
|
||||||
effective_batch_size,
|
cfg.batch_size,
|
||||||
dataset.num_frames,
|
dataset.num_frames,
|
||||||
dataset.num_episodes,
|
dataset.num_episodes,
|
||||||
train_metrics,
|
train_metrics,
|
||||||
@@ -390,6 +392,14 @@ def train(cfg: TrainPipelineConfig, accelerator: Accelerator | None = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_main_process:
|
if is_main_process:
|
||||||
|
progbar = tqdm(
|
||||||
|
total=cfg.steps - step,
|
||||||
|
desc="Training",
|
||||||
|
unit="step",
|
||||||
|
disable=inside_slurm(),
|
||||||
|
position=0,
|
||||||
|
leave=True,
|
||||||
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Start offline training on a fixed dataset, with effective batch size: {effective_batch_size}"
|
f"Start offline training on a fixed dataset, with effective batch size: {effective_batch_size}"
|
||||||
)
|
)
|
||||||
@@ -414,6 +424,8 @@ def train(cfg: TrainPipelineConfig, accelerator: Accelerator | None = None):
|
|||||||
# Note: eval and checkpoint happens *after* the `step`th training update has completed, so we
|
# Note: eval and checkpoint happens *after* the `step`th training update has completed, so we
|
||||||
# increment `step` here.
|
# increment `step` here.
|
||||||
step += 1
|
step += 1
|
||||||
|
if is_main_process:
|
||||||
|
progbar.update(1)
|
||||||
train_tracker.step()
|
train_tracker.step()
|
||||||
is_log_step = cfg.log_freq > 0 and step % cfg.log_freq == 0 and is_main_process
|
is_log_step = cfg.log_freq > 0 and step % cfg.log_freq == 0 and is_main_process
|
||||||
is_saving_step = step % cfg.save_freq == 0 or step == cfg.steps
|
is_saving_step = step % cfg.save_freq == 0 or step == cfg.steps
|
||||||
@@ -507,6 +519,9 @@ def train(cfg: TrainPipelineConfig, accelerator: Accelerator | None = None):
|
|||||||
|
|
||||||
accelerator.wait_for_everyone()
|
accelerator.wait_for_everyone()
|
||||||
|
|
||||||
|
if is_main_process:
|
||||||
|
progbar.close()
|
||||||
|
|
||||||
if eval_env:
|
if eval_env:
|
||||||
close_envs(eval_env)
|
close_envs(eval_env)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright 2025 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.
|
||||||
|
|
||||||
|
from .config_openarm_mini import OpenArmMiniConfig
|
||||||
|
from .openarm_mini import OpenArmMini
|
||||||
|
|
||||||
|
__all__ = ["OpenArmMini", "OpenArmMiniConfig"]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright 2025 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.
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from ..config import TeleoperatorConfig
|
||||||
|
|
||||||
|
|
||||||
|
@TeleoperatorConfig.register_subclass("openarm_mini")
|
||||||
|
@dataclass
|
||||||
|
class OpenArmMiniConfig(TeleoperatorConfig):
|
||||||
|
"""Configuration for OpenArm Mini teleoperator with Feetech motors (dual arms)."""
|
||||||
|
|
||||||
|
port_right: str = "/dev/ttyUSB0"
|
||||||
|
port_left: str = "/dev/ttyUSB1"
|
||||||
|
|
||||||
|
use_degrees: bool = True
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright 2025 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 logging
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from lerobot.motors import Motor, MotorCalibration, MotorNormMode
|
||||||
|
from lerobot.motors.feetech import (
|
||||||
|
FeetechMotorsBus,
|
||||||
|
OperatingMode,
|
||||||
|
)
|
||||||
|
from lerobot.processor import RobotAction
|
||||||
|
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||||
|
|
||||||
|
from ..teleoperator import Teleoperator
|
||||||
|
from .config_openarm_mini import OpenArmMiniConfig
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Motors whose direction is inverted during readout
|
||||||
|
RIGHT_MOTORS_TO_FLIP = ["joint_1", "joint_2", "joint_3", "joint_4", "joint_5"]
|
||||||
|
LEFT_MOTORS_TO_FLIP = ["joint_1", "joint_3", "joint_4", "joint_5", "joint_6", "joint_7"]
|
||||||
|
|
||||||
|
|
||||||
|
class OpenArmMini(Teleoperator):
|
||||||
|
"""
|
||||||
|
OpenArm Mini Teleoperator with dual Feetech-based arms (8 motors per arm).
|
||||||
|
|
||||||
|
Each arm has 7 joints plus a gripper, using Feetech STS3215 servos.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config_class = OpenArmMiniConfig
|
||||||
|
name = "openarm_mini"
|
||||||
|
|
||||||
|
def __init__(self, config: OpenArmMiniConfig):
|
||||||
|
super().__init__(config)
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
norm_mode_body = MotorNormMode.DEGREES
|
||||||
|
|
||||||
|
motors_right = {
|
||||||
|
"joint_1": Motor(1, "sts3215", norm_mode_body),
|
||||||
|
"joint_2": Motor(2, "sts3215", norm_mode_body),
|
||||||
|
"joint_3": Motor(3, "sts3215", norm_mode_body),
|
||||||
|
"joint_4": Motor(4, "sts3215", norm_mode_body),
|
||||||
|
"joint_5": Motor(5, "sts3215", norm_mode_body),
|
||||||
|
"joint_6": Motor(6, "sts3215", norm_mode_body),
|
||||||
|
"joint_7": Motor(7, "sts3215", norm_mode_body),
|
||||||
|
"gripper": Motor(8, "sts3215", MotorNormMode.RANGE_0_100),
|
||||||
|
}
|
||||||
|
|
||||||
|
motors_left = {
|
||||||
|
"joint_1": Motor(1, "sts3215", norm_mode_body),
|
||||||
|
"joint_2": Motor(2, "sts3215", norm_mode_body),
|
||||||
|
"joint_3": Motor(3, "sts3215", norm_mode_body),
|
||||||
|
"joint_4": Motor(4, "sts3215", norm_mode_body),
|
||||||
|
"joint_5": Motor(5, "sts3215", norm_mode_body),
|
||||||
|
"joint_6": Motor(6, "sts3215", norm_mode_body),
|
||||||
|
"joint_7": Motor(7, "sts3215", norm_mode_body),
|
||||||
|
"gripper": Motor(8, "sts3215", MotorNormMode.RANGE_0_100),
|
||||||
|
}
|
||||||
|
|
||||||
|
cal_right = {
|
||||||
|
k.replace("right_", ""): v for k, v in (self.calibration or {}).items() if k.startswith("right_")
|
||||||
|
}
|
||||||
|
cal_left = {
|
||||||
|
k.replace("left_", ""): v for k, v in (self.calibration or {}).items() if k.startswith("left_")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bus_right = FeetechMotorsBus(
|
||||||
|
port=self.config.port_right,
|
||||||
|
motors=motors_right,
|
||||||
|
calibration=cal_right,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bus_left = FeetechMotorsBus(
|
||||||
|
port=self.config.port_left,
|
||||||
|
motors=motors_left,
|
||||||
|
calibration=cal_left,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def action_features(self) -> dict[str, type]:
|
||||||
|
features: dict[str, type] = {}
|
||||||
|
for motor in self.bus_right.motors:
|
||||||
|
features[f"right_{motor}.pos"] = float
|
||||||
|
for motor in self.bus_left.motors:
|
||||||
|
features[f"left_{motor}.pos"] = float
|
||||||
|
return features
|
||||||
|
|
||||||
|
@property
|
||||||
|
def feedback_features(self) -> dict[str, type]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
return self.bus_right.is_connected and self.bus_left.is_connected
|
||||||
|
|
||||||
|
@check_if_already_connected
|
||||||
|
def connect(self, calibrate: bool = True) -> None:
|
||||||
|
logger.info(f"Connecting right arm on {self.config.port_right}...")
|
||||||
|
self.bus_right.connect()
|
||||||
|
logger.info(f"Connecting left arm on {self.config.port_left}...")
|
||||||
|
self.bus_left.connect()
|
||||||
|
|
||||||
|
if calibrate:
|
||||||
|
self.calibrate()
|
||||||
|
|
||||||
|
self.configure()
|
||||||
|
logger.info(f"{self} connected.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_calibrated(self) -> bool:
|
||||||
|
return self.bus_right.is_calibrated and self.bus_left.is_calibrated
|
||||||
|
|
||||||
|
def calibrate(self) -> None:
|
||||||
|
"""
|
||||||
|
Run calibration procedure for OpenArm Mini.
|
||||||
|
|
||||||
|
1. Disable torque
|
||||||
|
2. Ask user to position arms in hanging position with grippers closed
|
||||||
|
3. Set this as zero position via half-turn homing
|
||||||
|
4. Interactive gripper calibration (open/close positions)
|
||||||
|
5. Save calibration
|
||||||
|
"""
|
||||||
|
if self.calibration:
|
||||||
|
user_input = input(
|
||||||
|
f"Press ENTER to use existing calibration for {self.id}, "
|
||||||
|
f"or type 'c' and press ENTER to run new calibration: "
|
||||||
|
)
|
||||||
|
if user_input.strip().lower() != "c":
|
||||||
|
logger.info(f"Using existing calibration for {self.id}")
|
||||||
|
cal_right = {
|
||||||
|
k.replace("right_", ""): v for k, v in self.calibration.items() if k.startswith("right_")
|
||||||
|
}
|
||||||
|
cal_left = {
|
||||||
|
k.replace("left_", ""): v for k, v in self.calibration.items() if k.startswith("left_")
|
||||||
|
}
|
||||||
|
self.bus_right.write_calibration(cal_right)
|
||||||
|
self.bus_left.write_calibration(cal_left)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"\nRunning calibration for {self}")
|
||||||
|
|
||||||
|
self._calibrate_arm("right", self.bus_right)
|
||||||
|
self._calibrate_arm("left", self.bus_left)
|
||||||
|
|
||||||
|
self._save_calibration()
|
||||||
|
print(f"\nCalibration complete and saved to {self.calibration_fpath}")
|
||||||
|
|
||||||
|
def _calibrate_arm(self, arm_name: str, bus: FeetechMotorsBus) -> None:
|
||||||
|
"""Calibrate a single arm with Feetech motors."""
|
||||||
|
logger.info(f"\n=== Calibrating {arm_name.upper()} arm ===")
|
||||||
|
|
||||||
|
bus.disable_torque()
|
||||||
|
|
||||||
|
logger.info(f"Setting Phase to 12 for all motors in {arm_name.upper()} arm...")
|
||||||
|
for motor in bus.motors:
|
||||||
|
bus.write("Phase", motor, 12)
|
||||||
|
|
||||||
|
for motor in bus.motors:
|
||||||
|
bus.write("Operating_Mode", motor, OperatingMode.POSITION.value)
|
||||||
|
|
||||||
|
input(
|
||||||
|
f"\nCalibration: Zero Position ({arm_name.upper()} arm)\n"
|
||||||
|
"Position the arm in the following configuration:\n"
|
||||||
|
" - Arm hanging straight down\n"
|
||||||
|
" - Gripper closed\n"
|
||||||
|
"Press ENTER when ready..."
|
||||||
|
)
|
||||||
|
|
||||||
|
homing_offsets = bus.set_half_turn_homings()
|
||||||
|
logger.info(f"{arm_name.capitalize()} arm zero position set.")
|
||||||
|
|
||||||
|
print(f"\nSetting motor ranges for {arm_name.upper()} arm\n")
|
||||||
|
|
||||||
|
if self.calibration is None:
|
||||||
|
self.calibration = {}
|
||||||
|
|
||||||
|
motor_resolution = bus.model_resolution_table[list(bus.motors.values())[0].model]
|
||||||
|
max_res = motor_resolution - 1
|
||||||
|
|
||||||
|
for motor_name, motor in bus.motors.items():
|
||||||
|
prefixed_name = f"{arm_name}_{motor_name}"
|
||||||
|
|
||||||
|
if motor_name == "gripper":
|
||||||
|
input(
|
||||||
|
f"\nGripper Calibration ({arm_name.upper()} arm)\n"
|
||||||
|
f"Step 1: CLOSE the gripper fully\n"
|
||||||
|
f"Press ENTER when gripper is closed..."
|
||||||
|
)
|
||||||
|
closed_pos = bus.read("Present_Position", motor_name, normalize=False)
|
||||||
|
logger.info(f" Gripper closed position recorded: {closed_pos}")
|
||||||
|
|
||||||
|
input("\nStep 2: OPEN the gripper fully\nPress ENTER when gripper is fully open...")
|
||||||
|
open_pos = bus.read("Present_Position", motor_name, normalize=False)
|
||||||
|
logger.info(f" Gripper open position recorded: {open_pos}")
|
||||||
|
|
||||||
|
if closed_pos < open_pos:
|
||||||
|
range_min = int(closed_pos)
|
||||||
|
range_max = int(open_pos)
|
||||||
|
drive_mode = 0
|
||||||
|
else:
|
||||||
|
range_min = int(open_pos)
|
||||||
|
range_max = int(closed_pos)
|
||||||
|
drive_mode = 1
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f" {prefixed_name}: range set to [{range_min}, {range_max}] "
|
||||||
|
f"(0=closed, 100=open, drive_mode={drive_mode})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
range_min = 0
|
||||||
|
range_max = max_res
|
||||||
|
drive_mode = 0
|
||||||
|
logger.info(f" {prefixed_name}: range set to [0, {max_res}] (full motor range)")
|
||||||
|
|
||||||
|
self.calibration[prefixed_name] = MotorCalibration(
|
||||||
|
id=motor.id,
|
||||||
|
drive_mode=drive_mode,
|
||||||
|
homing_offset=homing_offsets[motor_name],
|
||||||
|
range_min=range_min,
|
||||||
|
range_max=range_max,
|
||||||
|
)
|
||||||
|
|
||||||
|
cal_for_bus = {
|
||||||
|
k.replace(f"{arm_name}_", ""): v
|
||||||
|
for k, v in self.calibration.items()
|
||||||
|
if k.startswith(f"{arm_name}_")
|
||||||
|
}
|
||||||
|
bus.write_calibration(cal_for_bus)
|
||||||
|
|
||||||
|
def configure(self) -> None:
|
||||||
|
self.bus_right.disable_torque()
|
||||||
|
self.bus_right.configure_motors()
|
||||||
|
for motor in self.bus_right.motors:
|
||||||
|
self.bus_right.write("Operating_Mode", motor, OperatingMode.POSITION.value)
|
||||||
|
|
||||||
|
self.bus_left.disable_torque()
|
||||||
|
self.bus_left.configure_motors()
|
||||||
|
for motor in self.bus_left.motors:
|
||||||
|
self.bus_left.write("Operating_Mode", motor, OperatingMode.POSITION.value)
|
||||||
|
|
||||||
|
def setup_motors(self) -> None:
|
||||||
|
print("\nSetting up RIGHT arm motors...")
|
||||||
|
for motor in reversed(self.bus_right.motors):
|
||||||
|
input(f"Connect the controller board to the RIGHT '{motor}' motor only and press enter.")
|
||||||
|
self.bus_right.setup_motor(motor)
|
||||||
|
print(f"RIGHT '{motor}' motor id set to {self.bus_right.motors[motor].id}")
|
||||||
|
|
||||||
|
print("\nSetting up LEFT arm motors...")
|
||||||
|
for motor in reversed(self.bus_left.motors):
|
||||||
|
input(f"Connect the controller board to the LEFT '{motor}' motor only and press enter.")
|
||||||
|
self.bus_left.setup_motor(motor)
|
||||||
|
print(f"LEFT '{motor}' motor id set to {self.bus_left.motors[motor].id}")
|
||||||
|
|
||||||
|
@check_if_not_connected
|
||||||
|
def get_action(self) -> RobotAction:
|
||||||
|
"""Get current action from both arms (read positions from all motors)."""
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
right_positions = self.bus_right.sync_read("Present_Position")
|
||||||
|
left_positions = self.bus_left.sync_read("Present_Position")
|
||||||
|
|
||||||
|
action: dict[str, Any] = {}
|
||||||
|
for motor, val in right_positions.items():
|
||||||
|
action[f"right_{motor}.pos"] = -val if motor in RIGHT_MOTORS_TO_FLIP else val
|
||||||
|
for motor, val in left_positions.items():
|
||||||
|
action[f"left_{motor}.pos"] = -val if motor in LEFT_MOTORS_TO_FLIP else val
|
||||||
|
|
||||||
|
dt_ms = (time.perf_counter() - start) * 1e3
|
||||||
|
logger.debug(f"{self} read action: {dt_ms:.1f}ms")
|
||||||
|
return action
|
||||||
|
|
||||||
|
def send_feedback(self, feedback: dict[str, float]) -> None:
|
||||||
|
raise NotImplementedError("Feedback is not yet implemented for OpenArm Mini.")
|
||||||
|
|
||||||
|
@check_if_not_connected
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
self.bus_right.disconnect()
|
||||||
|
self.bus_left.disconnect()
|
||||||
|
logger.info(f"{self} disconnected.")
|
||||||
@@ -95,6 +95,10 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> "Teleoperator":
|
|||||||
from .bi_openarm_leader import BiOpenArmLeader
|
from .bi_openarm_leader import BiOpenArmLeader
|
||||||
|
|
||||||
return BiOpenArmLeader(config)
|
return BiOpenArmLeader(config)
|
||||||
|
elif config.type == "openarm_mini":
|
||||||
|
from .openarm_mini import OpenArmMini
|
||||||
|
|
||||||
|
return OpenArmMini(config)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return cast("Teleoperator", make_device_from_device_class(config))
|
return cast("Teleoperator", make_device_from_device_class(config))
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def sanity_check_dataset_name(repo_id, policy_cfg):
|
|||||||
# Check if dataset_name starts with "eval_" but policy is missing
|
# Check if dataset_name starts with "eval_" but policy is missing
|
||||||
if dataset_name.startswith("eval_") and policy_cfg is None:
|
if dataset_name.startswith("eval_") and policy_cfg is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Your dataset name begins with 'eval_' ({dataset_name}), but no policy is provided ({policy_cfg.type})."
|
f"Your dataset name begins with 'eval_' ({dataset_name}), but no policy is provided."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if dataset_name does not start with "eval_" but policy is provided
|
# Check if dataset_name does not start with "eval_" but policy is provided
|
||||||
|
|||||||
@@ -104,9 +104,10 @@ class MetricsTracker:
|
|||||||
self.metrics = metrics
|
self.metrics = metrics
|
||||||
|
|
||||||
self.steps = initial_step
|
self.steps = initial_step
|
||||||
|
world_size = accelerator.num_processes if accelerator else 1
|
||||||
# A sample is an (observation,action) pair, where observation and action
|
# A sample is an (observation,action) pair, where observation and action
|
||||||
# can be on multiple timestamps. In a batch, we have `batch_size` number of samples.
|
# can be on multiple timestamps. In a batch, we have `batch_size` number of samples.
|
||||||
self.samples = self.steps * self._batch_size
|
self.samples = self.steps * self._batch_size * world_size
|
||||||
self.episodes = self.samples / self._avg_samples_per_ep
|
self.episodes = self.samples / self._avg_samples_per_ep
|
||||||
self.epochs = self.samples / self._num_frames
|
self.epochs = self.samples / self._num_frames
|
||||||
self.accelerator = accelerator
|
self.accelerator = accelerator
|
||||||
@@ -132,7 +133,8 @@ class MetricsTracker:
|
|||||||
Updates metrics that depend on 'step' for one step.
|
Updates metrics that depend on 'step' for one step.
|
||||||
"""
|
"""
|
||||||
self.steps += 1
|
self.steps += 1
|
||||||
self.samples += self._batch_size * (self.accelerator.num_processes if self.accelerator else 1)
|
world_size = self.accelerator.num_processes if self.accelerator else 1
|
||||||
|
self.samples += self._batch_size * world_size
|
||||||
self.episodes = self.samples / self._avg_samples_per_ep
|
self.episodes = self.samples / self._avg_samples_per_ep
|
||||||
self.epochs = self.samples / self._num_frames
|
self.epochs = self.samples / self._num_frames
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19eaaa85f66ba4aa6388dbb83819ffad6ea4363247208f871a8dc385689f6fc8
|
oid sha256:54aecbc1af72a4cd5e9261492f5e7601890517516257aacdf2a0ffb3ce281f1b
|
||||||
size 992
|
size 992
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:227296eaeeb54acdc3dae2eb8af3d4d08fb87e245337624447140b1e91cfd002
|
oid sha256:88a9c3775a2aa1e90a08850521970070a4fcf0f6b82aab43cd8ccc5cf77e0013
|
||||||
size 47424
|
size 47424
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:271b00cb2f0cd5fd26b1d53463638e3d1a6e92692ec625fcffb420ca190869e5
|
oid sha256:91a2635e05a75fe187a5081504c5f35ce3417378813fa2deaf9ca4e8200e1819
|
||||||
size 68
|
size 68
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:778fddbbaa64248cee35cb377c02cc2b6076f7ce5855146de677128900617ddf
|
oid sha256:645bff922ac7bea63ad018ebf77c303c0e4cd2c1c0dc5ef3192865281bef3dc6
|
||||||
size 47424
|
size 47424
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ def mock_metrics():
|
|||||||
return {"loss": AverageMeter("loss", ":.3f"), "accuracy": AverageMeter("accuracy", ":.2f")}
|
return {"loss": AverageMeter("loss", ":.3f"), "accuracy": AverageMeter("accuracy", ":.2f")}
|
||||||
|
|
||||||
|
|
||||||
|
class MockAccelerator:
|
||||||
|
def __init__(self, num_processes: int):
|
||||||
|
self.num_processes = num_processes
|
||||||
|
|
||||||
|
|
||||||
def test_average_meter_initialization():
|
def test_average_meter_initialization():
|
||||||
meter = AverageMeter("loss", ":.2f")
|
meter = AverageMeter("loss", ":.2f")
|
||||||
assert meter.name == "loss"
|
assert meter.name == "loss"
|
||||||
@@ -82,6 +87,37 @@ def test_metrics_tracker_step(mock_metrics):
|
|||||||
assert tracker.epochs == tracker.samples / 1000
|
assert tracker.epochs == tracker.samples / 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_metrics_tracker_initialization_with_accelerator(mock_metrics):
|
||||||
|
tracker = MetricsTracker(
|
||||||
|
batch_size=32,
|
||||||
|
num_frames=1000,
|
||||||
|
num_episodes=50,
|
||||||
|
metrics=mock_metrics,
|
||||||
|
initial_step=10,
|
||||||
|
accelerator=MockAccelerator(num_processes=2),
|
||||||
|
)
|
||||||
|
assert tracker.steps == 10
|
||||||
|
assert tracker.samples == 10 * 32 * 2
|
||||||
|
assert tracker.episodes == tracker.samples / (1000 / 50)
|
||||||
|
assert tracker.epochs == tracker.samples / 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_metrics_tracker_step_with_accelerator(mock_metrics):
|
||||||
|
tracker = MetricsTracker(
|
||||||
|
batch_size=32,
|
||||||
|
num_frames=1000,
|
||||||
|
num_episodes=50,
|
||||||
|
metrics=mock_metrics,
|
||||||
|
initial_step=5,
|
||||||
|
accelerator=MockAccelerator(num_processes=2),
|
||||||
|
)
|
||||||
|
tracker.step()
|
||||||
|
assert tracker.steps == 6
|
||||||
|
assert tracker.samples == (5 * 32 * 2) + (32 * 2)
|
||||||
|
assert tracker.episodes == tracker.samples / (1000 / 50)
|
||||||
|
assert tracker.epochs == tracker.samples / 1000
|
||||||
|
|
||||||
|
|
||||||
def test_metrics_tracker_getattr(mock_metrics):
|
def test_metrics_tracker_getattr(mock_metrics):
|
||||||
tracker = MetricsTracker(batch_size=32, num_frames=1000, num_episodes=50, metrics=mock_metrics)
|
tracker = MetricsTracker(batch_size=32, num_frames=1000, num_episodes=50, metrics=mock_metrics)
|
||||||
assert tracker.loss == mock_metrics["loss"]
|
assert tracker.loss == mock_metrics["loss"]
|
||||||
|
|||||||
Reference in New Issue
Block a user