Compare commits

..

2 Commits

Author SHA1 Message Date
Steven Palma a5b29d4301 chore(installation): remove libero installation patch (#2416)
* chore(installation): remove libero installation patch

* fix(ci): exclude groot for unbound deps test
2025-11-10 11:51:52 +01:00
Steven Palma a4aa316470 fix(dataset): fix data access bottleneck for faster training (#2408) 2025-11-07 21:54:44 +01:00
7 changed files with 56 additions and 54 deletions
+3 -3
View File
@@ -83,11 +83,11 @@ jobs:
fi fi
- name: Remove Tags with Git dependencies - name: Remove Tags with Git dependencies
# TODO(Steven): Temporary patch to remove libero and pi from PyPi 0.4.0 release due to its reliance on git dependencies. # TODO(Steven): Temporary patch to remove pi from PyPi 0.4.0 release due to its reliance on git dependencies.
run: | run: |
echo "::info:: Checking for Git dependencies to remove from pyproject.toml..." echo "::info:: Checking for Git dependencies to remove from pyproject.toml..."
grep -E '@ git\+https|lerobot\[pi\]|lerobot\[libero\]' pyproject.toml | sed 's/^/::warning:: Removing line: /' || true grep -E '@ git\+https|lerobot\[pi\]' pyproject.toml | sed 's/^/::warning:: Removing line: /' || true
sed -E -i '/@ git\+https|lerobot\[pi\]|lerobot\[libero\]/d' pyproject.toml sed -E -i '/@ git\+https|lerobot\[pi\]/d' pyproject.toml
echo "::info:: Git dependencies removed. Proceeding with build." echo "::info:: Git dependencies removed. Proceeding with build."
- name: Install build dependencies - name: Install build dependencies
+1 -1
View File
@@ -70,7 +70,7 @@ jobs:
echo "Dependencies unbound:" && cat pyproject.toml echo "Dependencies unbound:" && cat pyproject.toml
- name: Install lerobot with all extras - name: Install lerobot with all extras
run: uv sync --all-extras run: uv sync --all-extras --no-extra groot # TODO(Steven): Make flash-attn optional
- name: Run pytest (all extras) - name: Run pytest (all extras)
run: uv run pytest tests -vv run: uv run pytest tests -vv
+1 -1
View File
@@ -186,7 +186,7 @@ For a full list of optional dependencies, see:
https://pypi.org/project/lerobot/ https://pypi.org/project/lerobot/
> [!NOTE] > [!NOTE]
> For lerobot 0.4.0, if you want to install libero or pi tags, you will have to do: `pip install "lerobot[pi,libero]@git+https://github.com/huggingface/lerobot.git"`. > For lerobot 0.4.0, if you want to install pi tags, you will have to do: `pip install "lerobot[pi]@git+https://github.com/huggingface/lerobot.git"`.
> >
> This will be solved in the next patch release > This will be solved in the next patch release
+1 -1
View File
@@ -82,7 +82,7 @@ For a full list of optional dependencies, see:
https://pypi.org/project/lerobot/ https://pypi.org/project/lerobot/
> [!NOTE] > [!NOTE]
> For lerobot 0.4.0, if you want to install libero or pi, you will have to do: `pip install "lerobot[pi,libero]@git+https://github.com/huggingface/lerobot.git"` > For lerobot 0.4.0, if you want to install pi, you will have to do: `pip install "lerobot[pi]@git+https://github.com/huggingface/lerobot.git"`
### Troubleshooting ### Troubleshooting
-5
View File
@@ -28,11 +28,6 @@ LIBERO is now part of our **multi-eval supported simulation**, meaning you can b
To Install LIBERO, after following LeRobot official instructions, just do: To Install LIBERO, after following LeRobot official instructions, just do:
`pip install -e ".[libero]"` `pip install -e ".[libero]"`
> [!NOTE]
> For lerobot 0.4.0, if you want to install libero tag, you will have to do: `pip install "lerobot[libero]@git+https://github.com/huggingface/lerobot.git"`.
>
> This will be solved in the next patch release
### Single-suite evaluation ### Single-suite evaluation
Evaluate a policy on one LIBERO suite: Evaluate a policy on one LIBERO suite:
+30 -38
View File
@@ -23,8 +23,6 @@ import platform
import time import time
from pathlib import Path from pathlib import Path
from threading import Event, Lock, Thread from threading import Event, Lock, Thread
from multiprocessing import Process, Event as EventProcess, JoinableQueue as Queue
from queue import Empty
from typing import Any from typing import Any
from numpy.typing import NDArray # type: ignore # TODO: add type stubs for numpy.typing from numpy.typing import NDArray # type: ignore # TODO: add type stubs for numpy.typing
@@ -121,10 +119,11 @@ class OpenCVCamera(Camera):
self.videocapture: cv2.VideoCapture | None = None self.videocapture: cv2.VideoCapture | None = None
self.process: Process | None = None self.thread: Thread | None = None
self.stop_event: EventProcess | None = None self.stop_event: Event | None = None
self.frame_queue: Queue = Queue() self.frame_lock: Lock = Lock()
self.latest_frame: NDArray[Any] | None = None self.latest_frame: NDArray[Any] | None = None
self.new_frame_event: Event = Event()
self.rotation: int | None = get_cv2_rotation(config.rotation) self.rotation: int | None = get_cv2_rotation(config.rotation)
self.backend: int = get_cv2_backend() self.backend: int = get_cv2_backend()
@@ -443,36 +442,37 @@ class OpenCVCamera(Camera):
while not self.stop_event.is_set(): while not self.stop_event.is_set():
try: try:
color_image = self.read() color_image = self.read()
self.frame_queue.put_nowait(color_image)
with self.frame_lock:
self.latest_frame = color_image
self.new_frame_event.set()
except DeviceNotConnectedError: except DeviceNotConnectedError:
break break
except Exception as e: except Exception as e:
logger.warning(f"Error reading frame in background thread for {self}: {e}") logger.warning(f"Error reading frame in background thread for {self}: {e}")
def _start_read_process(self) -> None: def _start_read_thread(self) -> None:
"""Starts or restarts the background read thread if it's not running.""" """Starts or restarts the background read thread if it's not running."""
if self.process is not None and self.process.is_alive(): if self.thread is not None and self.thread.is_alive():
self.frame_queue.join() self.thread.join(timeout=0.1)
self.process.join()
if self.stop_event is not None: if self.stop_event is not None:
self.stop_event.set() self.stop_event.set()
self.stop_event = Event() self.stop_event = Event()
self.process = Process(target=self._read_loop, args=(), name=f"{self}_read_loop") self.thread = Thread(target=self._read_loop, args=(), name=f"{self}_read_loop")
self.process.daemon = True self.thread.daemon = True
self.process.start() self.thread.start()
def _stop_read_thread(self) -> None: def _stop_read_thread(self) -> None:
"""Signals the background read thread to stop and waits for it to join.""" """Signals the background read thread to stop and waits for it to join."""
if self.stop_event is not None: if self.stop_event is not None:
self.stop_event.set() self.stop_event.set()
if self.process is not None and self.process.is_alive(): if self.thread is not None and self.thread.is_alive():
self.frame_queue.join() self.thread.join(timeout=2.0)
self.process.join()
self.process = None self.thread = None
self.stop_event = None self.stop_event = None
def async_read(self, timeout_ms: float = 200) -> NDArray[Any]: def async_read(self, timeout_ms: float = 200) -> NDArray[Any]:
@@ -499,32 +499,24 @@ class OpenCVCamera(Camera):
if not self.is_connected: if not self.is_connected:
raise DeviceNotConnectedError(f"{self} is not connected.") raise DeviceNotConnectedError(f"{self} is not connected.")
if self.process is None or not self.process.is_alive(): if self.thread is None or not self.thread.is_alive():
self._start_read_process() self._start_read_thread()
if self.latest_frame is None: if not self.new_frame_event.wait(timeout=timeout_ms / 1000.0):
self.latest_frame = self.frame_queue.get() thread_alive = self.thread is not None and self.thread.is_alive()
self.frame_queue.task_done() raise TimeoutError(
return self.latest_frame f"Timed out waiting for frame from camera {self} after {timeout_ms} ms. "
f"Read thread alive: {thread_alive}."
)
try: with self.frame_lock:
frame = self.frame_queue.get(timeout=timeout_ms / 1000.0) frame = self.latest_frame
self.frame_queue.task_done() self.new_frame_event.clear()
except Empty:
process_alive = self.process is not None and self.process.is_alive()
if process_alive:
logger.warning(f"{self} async_read timed out after {timeout_ms} ms but camera is still running.")
return self.latest_frame
else:
raise TimeoutError(
f"{self} async_read timed out after {timeout_ms} ms: camera is not responding !"
)
if frame is None: if frame is None:
raise RuntimeError(f"Internal error: Event set but no frame available for {self}.") raise RuntimeError(f"Internal error: Event set but no frame available for {self}.")
else:
self.latest_frame = frame return frame
return self.latest_frame
def disconnect(self) -> None: def disconnect(self) -> None:
""" """
+20 -5
View File
@@ -940,11 +940,26 @@ class LeRobotDataset(torch.utils.data.Dataset):
return query_timestamps return query_timestamps
def _query_hf_dataset(self, query_indices: dict[str, list[int]]) -> dict: def _query_hf_dataset(self, query_indices: dict[str, list[int]]) -> dict:
return { """
key: torch.stack(self.hf_dataset[q_idx][key]) Query dataset for indices across keys, skipping video keys.
for key, q_idx in query_indices.items()
if key not in self.meta.video_keys Tries column-first [key][indices] for speed, falls back to row-first.
}
Args:
query_indices: Dict mapping keys to index lists to retrieve
Returns:
Dict with stacked tensors of queried data (video keys excluded)
"""
result: dict = {}
for key, q_idx in query_indices.items():
if key in self.meta.video_keys:
continue
try:
result[key] = torch.stack(self.hf_dataset[key][q_idx])
except (KeyError, TypeError, IndexError):
result[key] = torch.stack(self.hf_dataset[q_idx][key])
return result
def _query_videos(self, query_timestamps: dict[str, list[float]], ep_idx: int) -> dict[str, torch.Tensor]: def _query_videos(self, query_timestamps: dict[str, list[float]], ep_idx: int) -> dict[str, torch.Tensor]:
"""Note: When using data workers (e.g. DataLoader with num_workers>0), do not call this function """Note: When using data workers (e.g. DataLoader with num_workers>0), do not call this function