|
| 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