Skip to content

Commit 005cff6

Browse files
add HF dataset plugin (#992)
This PR adds a Hugging Face datasets plugin for Flyte. It lets tasks consume Hub datasets directly via `from_hf(...)` and transparently materializes them as `datasets.Dataset` or `datasets.IterableDataset` with optional shared caching through `cache_root`. ```python from flyteplugins.huggingface.datasets import from_hf @env.task async def train( ds: datasets.Dataset = from_hf( "stanfordnlp/imdb", name="plain_text", split="train", cache_root="s3://my-bucket/flyte-hf-cache", ), ) -> int: return len(ds) ``` The plugin streams converted parquet shards from the Hub, stores them as sharded parquet artifacts, and reuses cached materializations across runs when available. You can also return and pass real `datasets.Dataset` objects between tasks: ```python @env.task async def create_dataset() -> datasets.Dataset: return datasets.Dataset.from_dict( { "text": ["hello", "world", "flyte"], "label": [0, 1, 0], } ) @env.task async def filter_positive(ds: datasets.Dataset) -> datasets.Dataset: return ds.filter(lambda row: row["label"] == 1) ``` <img width="1836" height="580" alt="Screenshot 2026-04-24 at 3 24 30 PM" src="https://github.com/user-attachments/assets/5a433cae-3f7a-473d-9e07-44e671361f1b" /> --------- Signed-off-by: André Ahlert <andre@aex.partners> Signed-off-by: Samhita Alla <aallasamhita@gmail.com> Co-authored-by: André Ahlert <andre@aex.partners>
1 parent ed8d69c commit 005cff6

14 files changed

Lines changed: 5341 additions & 0 deletions

File tree

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ jobs:
9292
- "plugins/pandera"
9393
- "plugins/hydra"
9494
- "plugins/omegaconf"
95+
- "plugins/huggingface"
9596
include:
9697
- workdir: "plugins/sglang"
9798
image-type: sglang

plugins/huggingface/README.md

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# Hugging Face Plugin
2+
3+
Native Flyte support for Hugging Face integrations in Flyte.
4+
5+
This plugin provides dataset support for Hugging Face `datasets.Dataset`
6+
and `datasets.IterableDataset` objects. It gives you two related capabilities:
7+
8+
1. Use `from_hf(...)` to reference a dataset on the Hugging Face Hub as a task
9+
input default.
10+
2. Pass Hugging Face dataset objects between Flyte tasks with automatic Parquet
11+
serialization.
12+
13+
The plugin works by treating Hub datasets as Parquet-backed structured data. For
14+
Hub sources, it first resolves the dataset's converted Parquet shards, then
15+
materializes them either into a generated path for the current run or into a
16+
shared artifact registry rooted at `cache_root`.
17+
18+
## Installation
19+
20+
```bash
21+
pip install flyteplugins-huggingface
22+
```
23+
24+
## Quick start
25+
26+
```python
27+
import datasets
28+
import flyte
29+
from flyteplugins.huggingface.datasets import from_hf
30+
31+
env = flyte.TaskEnvironment(name="hf-example")
32+
33+
@env.task
34+
async def count_reviews(
35+
ds: datasets.Dataset = from_hf(
36+
"stanfordnlp/imdb",
37+
name="plain_text",
38+
split="train",
39+
),
40+
) -> int:
41+
return len(ds)
42+
```
43+
44+
At the Flyte literal level this source is represented as an `hf://` URI, for
45+
example:
46+
47+
```text
48+
hf://stanfordnlp/imdb?name=plain_text&split=train
49+
```
50+
51+
The task receives a hydrated `datasets.Dataset`. The `hf://` URI is only the
52+
reference used between Flyte and the plugin.
53+
54+
## `from_hf(...)`
55+
56+
`from_hf(...)` is the entry point for Hub-backed task defaults:
57+
58+
```python
59+
from flyteplugins.huggingface.datasets import from_hf
60+
61+
from_hf(
62+
repo: str,
63+
*,
64+
name: str | None = None,
65+
split: str | None = None,
66+
revision: str | None = None,
67+
cache_root: str | None = None,
68+
)
69+
```
70+
71+
Arguments:
72+
73+
- `repo`: Hugging Face dataset repo, such as `"stanfordnlp/imdb"` or `"glue"`.
74+
- `name`: Optional dataset config/subset.
75+
- `split`: Optional split such as `"train"` or `"validation"`.
76+
- `revision`: Optional Hub revision. Defaults to `refs/convert/parquet`.
77+
- `cache_root`: Optional shared remote cache root for cross-run reuse.
78+
79+
`from_hf(...)` returns a Flyte `DataFrame` reference, not an eagerly loaded
80+
dataset object. When the task input is typed as `datasets.Dataset` or
81+
`datasets.IterableDataset`, the plugin decoder materializes that reference into
82+
the requested Hugging Face type.
83+
84+
## Config resolution
85+
86+
If you specify `name`, the plugin uses that config directly.
87+
88+
If you omit `name`, the plugin resolves the config as follows:
89+
90+
1. Try actual converted-parquet config `default`.
91+
2. If `default` does not exist and there is exactly one config, use that one.
92+
3. If there are multiple configs, raise an error and ask for `name=...`.
93+
94+
Examples:
95+
96+
```python
97+
# Works: imdb has a single converted-parquet config, plain_text.
98+
from_hf("stanfordnlp/imdb", split="train")
99+
100+
# Required: glue has multiple configs such as mrpc, sst2, qnli, ...
101+
from_hf("glue", name="mrpc", split="train")
102+
```
103+
104+
Using `name=` explicitly is recommended in examples and production code because
105+
it makes the UI literal and task signature more obvious.
106+
107+
## Split behavior
108+
109+
If you specify `split`, only that split is materialized:
110+
111+
```python
112+
@env.task
113+
async def train_split(
114+
ds: datasets.Dataset = from_hf(
115+
"stanfordnlp/imdb",
116+
name="plain_text",
117+
split="train",
118+
),
119+
) -> int:
120+
return len(ds)
121+
```
122+
123+
If you omit `split`, the plugin reads every converted Parquet split under the
124+
resolved config and presents them as one dataset stream/table:
125+
126+
```python
127+
@env.task
128+
async def all_splits(
129+
ds: datasets.Dataset = from_hf(
130+
"stanfordnlp/imdb",
131+
name="plain_text",
132+
),
133+
) -> list[str]:
134+
return ds.column_names
135+
```
136+
137+
That means the result is a combined dataset, not a mapping of split name to
138+
dataset.
139+
140+
## Cross-run reuse with `cache_root`
141+
142+
Without `cache_root`, a Hub source is materialized into a generated path for the
143+
current execution only.
144+
145+
With `cache_root`, the plugin uses a shared cache registry so later runs can
146+
skip the Hub download entirely:
147+
148+
```python
149+
@env.task
150+
async def train_cached(
151+
ds: datasets.Dataset = from_hf(
152+
"stanfordnlp/imdb",
153+
name="plain_text",
154+
split="train",
155+
cache_root="s3://my-bucket/flyte-hf-cache",
156+
),
157+
) -> int:
158+
return len(ds)
159+
```
160+
161+
The shared cache layout is:
162+
163+
```text
164+
{cache_root}/huggingface/datasets/
165+
by-key/{source-cache-key}.json
166+
blobs/{source-cache-key}/...
167+
```
168+
169+
The cache key is derived from:
170+
171+
- repo
172+
- config name
173+
- split
174+
- revision
175+
- resolved Parquet shard metadata
176+
177+
This means the cache is stable across runs as long as the underlying converted
178+
Parquet source does not change.
179+
180+
The canonical artifact location is always
181+
`{cache_root}/huggingface/datasets/blobs/{source-cache-key}/...`. The registry
182+
record under `by-key/` is metadata for that cache key.
183+
184+
## What the plugin logs
185+
186+
When `LOG_LEVEL` is `INFO` or lower, the plugin logs whether it is:
187+
188+
- checking the shared dataset cache
189+
- materializing from the Hugging Face Hub
190+
- using a cached artifact
191+
- reading Parquet from a local or remote directory
192+
193+
This is the easiest way to confirm whether a run is reading from the Hub or
194+
from your shared cache artifact.
195+
196+
## `datasets.Dataset` between tasks
197+
198+
You can return and pass real `datasets.Dataset` objects between tasks:
199+
200+
```python
201+
import datasets
202+
import flyte
203+
204+
env = flyte.TaskEnvironment(name="hf-transform")
205+
206+
207+
@env.task
208+
async def create_dataset() -> datasets.Dataset:
209+
return datasets.Dataset.from_dict(
210+
{
211+
"text": ["hello", "world", "flyte"],
212+
"label": [0, 1, 0],
213+
}
214+
)
215+
216+
217+
@env.task
218+
async def filter_positive(ds: datasets.Dataset) -> datasets.Dataset:
219+
return ds.filter(lambda row: row["label"] == 1)
220+
```
221+
222+
Task-produced in-memory datasets are serialized to Parquet automatically. This
223+
is separate from `from_hf(...)`, which is a source reference rather than a
224+
materialized dataset object.
225+
226+
## `datasets.IterableDataset`
227+
228+
Use `datasets.IterableDataset` when you want row streaming behavior instead of a
229+
fully materialized table:
230+
231+
```python
232+
@env.task
233+
async def stream_reviews(
234+
ds: datasets.IterableDataset = from_hf(
235+
"stanfordnlp/imdb",
236+
name="plain_text",
237+
split="train",
238+
cache_root="s3://my-bucket/flyte-hf-cache",
239+
),
240+
) -> datasets.IterableDataset:
241+
def add_length(batch):
242+
batch["length"] = [len(text) for text in batch["text"]]
243+
return batch
244+
245+
return ds.map(add_length, batched=True)
246+
```
247+
248+
Notes:
249+
250+
- The returned Hugging Face `IterableDataset` is consumed with normal synchronous
251+
iteration.
252+
- Internally the plugin streams row batches from Parquet files.
253+
- Iterable outputs are written back as sharded Parquet directories.
254+
255+
## Column projection
256+
257+
Use a Flyte structured-dataset column annotation when you only want selected
258+
columns:
259+
260+
```python
261+
from collections import OrderedDict
262+
from typing import Annotated
263+
264+
265+
@env.task
266+
async def load_text_only(
267+
ds: Annotated[datasets.Dataset, OrderedDict(text=str)] = from_hf(
268+
"stanfordnlp/imdb",
269+
name="plain_text",
270+
split="train",
271+
),
272+
) -> list[str]:
273+
return ds["text"][:10]
274+
```
275+
276+
The plugin uses the annotation to request only those columns when reading
277+
Parquet.
278+
279+
## Revision selection
280+
281+
Use `revision=` if you want to pin a specific converted-Parquet revision:
282+
283+
```python
284+
@env.task
285+
async def pinned_revision(
286+
ds: datasets.Dataset = from_hf(
287+
"stanfordnlp/imdb",
288+
name="plain_text",
289+
split="train",
290+
revision="refs/convert/parquet",
291+
cache_root="s3://my-bucket/flyte-hf-cache",
292+
),
293+
) -> int:
294+
return len(ds)
295+
```
296+
297+
If you do not specify a revision, the plugin uses `refs/convert/parquet`.
298+
299+
## Local vs remote behavior
300+
301+
There are two distinct layers to keep in mind:
302+
303+
1. Task inputs and outputs inside Flyte tasks.
304+
2. What your launcher process sees when a run completes.
305+
306+
Inside a task, a parameter typed as `datasets.Dataset` or
307+
`datasets.IterableDataset` is hydrated by the plugin into a Hugging Face object.
308+
309+
Outside the task, especially for remote runs, outputs are often represented to
310+
the launcher as Flyte `DataFrame` references rather than already-opened Hugging
311+
Face dataset objects. That is expected: the structured dataset literal remains
312+
the transport format.
313+
314+
## Private datasets
315+
316+
Set `HF_TOKEN` in the task environment to access private Hugging Face datasets.
317+
Without it, the plugin uses anonymous Hub access.
318+
319+
## Failure modes
320+
321+
Common issues:
322+
323+
- Missing `name` for a dataset with multiple configs:
324+
the plugin raises and asks for `name=...`.
325+
- No converted Parquet shards available:
326+
the dataset may not have an auto-converted Parquet representation yet.
327+
- Remote cache path credentials:
328+
your Flyte runtime must be able to read and write the chosen `cache_root`.
329+
330+
## Example
331+
332+
See the example workflow in `plugins/huggingface/examples/hf_dataset_workflow.py`
333+
for end-to-end local and remote scenarios.

0 commit comments

Comments
 (0)