Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.

Commit b18cef2

Browse files
authored
feat(dataset): add subtask support (huggingface#2860)
* add subtask * remove folder * add docs * update doc * add testing * update test * update constant naming + doc * more docs
1 parent 5c61821 commit b18cef2

9 files changed

Lines changed: 1003 additions & 2 deletions

File tree

‎docs/source/_toctree.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
title: Porting Large Datasets
2828
- local: using_dataset_tools
2929
title: Using the Dataset Tools
30+
- local: dataset_subtask
31+
title: Using Subtasks in the Dataset
3032
title: "Datasets"
3133
- sections:
3234
- local: act

‎docs/source/dataset_subtask.mdx‎

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# Using Subtasks in LeRobot Datasets
2+
3+
Subtask support in robotics datasets has proven effective in improving robot reasoning and understanding. Subtasks are particularly useful for:
4+
5+
- **Hierarchical policies**: Building policies that include subtask predictions to visualize robot reasoning in real time
6+
- **Reward modeling**: Helping reward models understand task progression (e.g., SARM-style stage-aware reward models)
7+
- **Task decomposition**: Breaking down complex manipulation tasks into atomic, interpretable steps
8+
9+
LeRobotDataset now supports subtasks as part of its dataset structure, alongside tasks.
10+
11+
## What are Subtasks?
12+
13+
While a **task** describes the overall goal (e.g., "Pick up the apple and place it in the basket"), **subtasks** break down the execution into finer-grained steps:
14+
15+
1. "Approach the apple"
16+
2. "Grasp the apple"
17+
3. "Lift the apple"
18+
4. "Move to basket"
19+
5. "Release the apple"
20+
21+
Each frame in the dataset can be annotated with its corresponding subtask, enabling models to learn and predict these intermediate stages.
22+
23+
<img
24+
src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lerobot/subtask-asset.png"
25+
alt="An overview of subtask annotation showing how frames are labeled with intermediate subtask stages"
26+
width="80%"
27+
/>
28+
29+
<p>
30+
<em>Figure: Overview of subtask annotation.</em>
31+
</p>
32+
33+
**Reference:** _Subtask-learning based for robot self-assembly in flexible collaborative assembly in manufacturing_, Original Article, Published: 19 April 2022.
34+
35+
## Dataset Structure
36+
37+
Subtask information is stored in the dataset metadata:
38+
39+
```
40+
my-dataset/
41+
├── data/
42+
│ └── ...
43+
├── meta/
44+
│ ├── info.json
45+
│ ├── stats.json
46+
│ ├── tasks.parquet
47+
│ ├── subtasks.parquet # Subtask index → subtask string mapping
48+
│ └── episodes/
49+
│ └── ...
50+
└── videos/
51+
└── ...
52+
```
53+
54+
### Subtasks Parquet File
55+
56+
The `meta/subtasks.parquet` file maps subtask indices to their natural language descriptions:
57+
58+
| subtask_index | subtask (index column) |
59+
| ------------- | ---------------------- |
60+
| 0 | "Approach the apple" |
61+
| 1 | "Grasp the apple" |
62+
| 2 | "Lift the apple" |
63+
| ... | ... |
64+
65+
### Frame-Level Annotations
66+
67+
Each frame in the dataset can include a `subtask_index` field that references the subtasks parquet file:
68+
69+
```python
70+
# Example frame data in the parquet file
71+
{
72+
"index": 42,
73+
"timestamp": 1.4,
74+
"episode_index": 0,
75+
"task_index": 0,
76+
"subtask_index": 2, # References "Lift the apple"
77+
"observation.state": [...],
78+
"action": [...],
79+
}
80+
```
81+
82+
## Annotating Datasets with Subtasks
83+
84+
We provide a HuggingFace Space for easily annotating any LeRobotDataset with subtasks:
85+
86+
**[https://huggingface.co/spaces/lerobot/annotate](https://huggingface.co/spaces/lerobot/annotate)**
87+
88+
After completing your annotation:
89+
90+
1. Click "Push to Hub" to upload your annotated dataset
91+
2. You can also run the annotation space locally by following the instructions at [github.com/huggingface/lerobot-annotate](https://github.com/huggingface/lerobot-annotate)
92+
93+
## Loading Datasets with Subtasks
94+
95+
When you load a dataset with subtask annotations, the subtask information is automatically available:
96+
97+
```python
98+
from lerobot.datasets.lerobot_dataset import LeRobotDataset
99+
100+
# Load a dataset with subtask annotations
101+
dataset = LeRobotDataset("jadechoghari/collect-fruit-annotated")
102+
103+
# Access a sample
104+
sample = dataset[100]
105+
106+
# The sample includes both task and subtask information
107+
print(sample["task"]) # "Collect the fruit"
108+
print(sample["subtask"]) # "Grasp the apple"
109+
print(sample["task_index"]) # tensor(0)
110+
print(sample["subtask_index"]) # tensor(2)
111+
```
112+
113+
### Checking for Subtask Support
114+
115+
You can check if a dataset has subtask annotations:
116+
117+
```python
118+
# Check if subtasks are available
119+
has_subtasks = (
120+
"subtask_index" in dataset.features
121+
and dataset.meta.subtasks is not None
122+
)
123+
124+
if has_subtasks:
125+
print(f"Dataset has {len(dataset.meta.subtasks)} unique subtasks")
126+
print("Subtasks:", list(dataset.meta.subtasks.index))
127+
```
128+
129+
## Using Subtasks for Training
130+
131+
### With the Tokenizer Processor
132+
133+
The `TokenizerProcessor` automatically handles subtask tokenization for Vision-Language Action (VLA) models:
134+
135+
```python
136+
from lerobot.processor.tokenizer_processor import TokenizerProcessor
137+
from lerobot.processor.pipeline import ProcessorPipeline
138+
139+
# Create a tokenizer processor
140+
tokenizer_processor = TokenizerProcessor(
141+
tokenizer_name_or_path="google/paligemma-3b-pt-224",
142+
padding="max_length",
143+
max_length=64,
144+
)
145+
146+
# The processor will automatically tokenize subtasks if present in the batch
147+
# and add them to the observation under:
148+
# - "observation.subtask.tokens"
149+
# - "observation.subtask.attention_mask"
150+
```
151+
152+
When subtasks are available in the batch, the tokenizer processor adds:
153+
154+
- `observation.subtask.tokens`: Tokenized subtask text
155+
- `observation.subtask.attention_mask`: Attention mask for the subtask tokens
156+
157+
### DataLoader with Subtasks
158+
159+
```python
160+
import torch
161+
from lerobot.datasets.lerobot_dataset import LeRobotDataset
162+
163+
dataset = LeRobotDataset("jadechoghari/collect-fruit-annotated")
164+
165+
dataloader = torch.utils.data.DataLoader(
166+
dataset,
167+
batch_size=16,
168+
shuffle=True,
169+
)
170+
171+
for batch in dataloader:
172+
# Access subtask information in the batch
173+
subtasks = batch["subtask"] # List of subtask strings
174+
subtask_indices = batch["subtask_index"] # Tensor of subtask indices
175+
176+
# Use for training hierarchical policies or reward models
177+
print(f"Batch subtasks: {set(subtasks)}")
178+
```
179+
180+
## Example Datasets with Subtask Annotations
181+
182+
Try loading a dataset with subtask annotations:
183+
184+
```python
185+
from lerobot.datasets.lerobot_dataset import LeRobotDataset
186+
187+
# Example dataset with subtask annotations
188+
dataset = LeRobotDataset("jadechoghari/collect-fruit-annotated")
189+
190+
# Explore the subtasks
191+
print("Available subtasks:")
192+
for subtask_name in dataset.meta.subtasks.index:
193+
print(f" - {subtask_name}")
194+
195+
# Get subtask distribution
196+
subtask_counts = {}
197+
for i in range(len(dataset)):
198+
sample = dataset[i]
199+
subtask = sample["subtask"]
200+
subtask_counts[subtask] = subtask_counts.get(subtask, 0) + 1
201+
202+
print("\nSubtask distribution:")
203+
for subtask, count in sorted(subtask_counts.items(), key=lambda x: -x[1]):
204+
print(f" {subtask}: {count} frames")
205+
```
206+
207+
## Use Cases
208+
209+
### 1. Hierarchical Policy Training
210+
211+
Train policies that predict both actions and current subtask:
212+
213+
```python
214+
class HierarchicalPolicy(nn.Module):
215+
def __init__(self, num_subtasks):
216+
super().__init__()
217+
self.action_head = nn.Linear(hidden_dim, action_dim)
218+
self.subtask_head = nn.Linear(hidden_dim, num_subtasks)
219+
220+
def forward(self, observations):
221+
features = self.encoder(observations)
222+
actions = self.action_head(features)
223+
subtask_logits = self.subtask_head(features)
224+
return actions, subtask_logits
225+
```
226+
227+
### 2. Stage-Aware Reward Modeling (SARM)
228+
229+
Build reward models that understand task progression:
230+
231+
```python
232+
# SARM predicts:
233+
# - Stage: Which subtask is being executed (discrete)
234+
# - Progress: How far along the subtask (continuous 0-1)
235+
236+
class SARMRewardModel(nn.Module):
237+
def forward(self, observations):
238+
features = self.encoder(observations)
239+
stage_logits = self.stage_classifier(features)
240+
progress = self.progress_regressor(features)
241+
return stage_logits, progress
242+
```
243+
244+
### 3. Progress Visualization
245+
246+
Monitor robot execution by tracking subtask progression:
247+
248+
```python
249+
def visualize_execution(model, observations):
250+
for t, obs in enumerate(observations):
251+
action, subtask_logits = model(obs)
252+
predicted_subtask = subtask_names[subtask_logits.argmax()]
253+
print(f"t={t}: Executing '{predicted_subtask}'")
254+
```
255+
256+
## API Reference
257+
258+
### LeRobotDataset Properties
259+
260+
| Property | Type | Description |
261+
| --------------------------- | ---------------------- | ------------------------------------------ |
262+
| `meta.subtasks` | `pd.DataFrame \| None` | DataFrame mapping subtask names to indices |
263+
| `features["subtask_index"]` | `dict` | Feature spec for subtask_index if present |
264+
265+
### Sample Keys
266+
267+
When subtasks are available, each sample includes:
268+
269+
| Key | Type | Description |
270+
| --------------- | -------------- | ------------------------------------ |
271+
| `subtask_index` | `torch.Tensor` | Integer index of the current subtask |
272+
| `subtask` | `str` | Natural language subtask description |
273+
274+
## Related Resources
275+
276+
- [SARM Paper](https://arxiv.org/pdf/2509.25358) - Stage-Aware Reward Modeling for Long Horizon Robot Manipulation
277+
- [LeRobot Annotate Space](https://huggingface.co/spaces/lerobot/annotate) - Interactive annotation tool
278+
- [LeRobotDataset v3.0](./lerobot-dataset-v3) - Dataset format documentation

‎src/lerobot/datasets/lerobot_dataset.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
load_info,
5858
load_nested_dataset,
5959
load_stats,
60+
load_subtasks,
6061
load_tasks,
6162
update_chunk_file_indices,
6263
validate_episode_buffer,
@@ -162,6 +163,7 @@ def load_metadata(self):
162163
self.info = load_info(self.root)
163164
check_version_compatibility(self.repo_id, self._version, CODEBASE_VERSION)
164165
self.tasks = load_tasks(self.root)
166+
self.subtasks = load_subtasks(self.root)
165167
self.episodes = load_episodes(self.root)
166168
self.stats = load_stats(self.root)
167169

@@ -518,6 +520,7 @@ def create(
518520
_validate_feature_names(features)
519521

520522
obj.tasks = None
523+
obj.subtasks = None
521524
obj.episodes = None
522525
obj.stats = None
523526
obj.info = create_empty_dataset_info(
@@ -1075,6 +1078,12 @@ def __getitem__(self, idx) -> dict:
10751078
# Add task as a string
10761079
task_idx = item["task_index"].item()
10771080
item["task"] = self.meta.tasks.iloc[task_idx].name
1081+
1082+
# add subtask information if available
1083+
if "subtask_index" in self.features and self.meta.subtasks is not None:
1084+
subtask_idx = item["subtask_index"].item()
1085+
item["subtask"] = self.meta.subtasks.iloc[subtask_idx].name
1086+
10781087
return item
10791088

10801089
def __repr__(self):

‎src/lerobot/datasets/utils.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060

6161
CHUNK_FILE_PATTERN = "chunk-{chunk_index:03d}/file-{file_index:03d}"
6262
DEFAULT_TASKS_PATH = "meta/tasks.parquet"
63+
DEFAULT_SUBTASKS_PATH = "meta/subtasks.parquet"
6364
DEFAULT_EPISODES_PATH = EPISODES_DIR + "/" + CHUNK_FILE_PATTERN + ".parquet"
6465
DEFAULT_DATA_PATH = DATA_DIR + "/" + CHUNK_FILE_PATTERN + ".parquet"
6566
DEFAULT_VIDEO_PATH = VIDEO_DIR + "/{video_key}/" + CHUNK_FILE_PATTERN + ".mp4"
@@ -353,6 +354,14 @@ def load_tasks(local_dir: Path) -> pandas.DataFrame:
353354
return tasks
354355

355356

357+
def load_subtasks(local_dir: Path) -> pandas.DataFrame | None:
358+
"""Load subtasks from subtasks.parquet if it exists."""
359+
subtasks_path = local_dir / DEFAULT_SUBTASKS_PATH
360+
if subtasks_path.exists():
361+
return pd.read_parquet(subtasks_path)
362+
return None
363+
364+
356365
def write_episodes(episodes: Dataset, local_dir: Path) -> None:
357366
"""Write episode metadata to a parquet file in the LeRobot v3.0 format.
358367
This function writes episode-level metadata to a single parquet file.

‎src/lerobot/processor/converters.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,12 @@ def _extract_complementary_data(batch: dict[str, Any]) -> dict[str, Any]:
168168
"""
169169
pad_keys = {k: v for k, v in batch.items() if "_is_pad" in k}
170170
task_key = {"task": batch["task"]} if "task" in batch else {}
171+
subtask_key = {"subtask": batch["subtask"]} if "subtask" in batch else {}
171172
index_key = {"index": batch["index"]} if "index" in batch else {}
172173
task_index_key = {"task_index": batch["task_index"]} if "task_index" in batch else {}
173174
episode_index_key = {"episode_index": batch["episode_index"]} if "episode_index" in batch else {}
174175

175-
return {**pad_keys, **task_key, **index_key, **task_index_key, **episode_index_key}
176+
return {**pad_keys, **task_key, **subtask_key, **index_key, **task_index_key, **episode_index_key}
176177

177178

178179
def create_transition(

0 commit comments

Comments
 (0)