195 lines
8.4 KiB
Python
195 lines
8.4 KiB
Python
import sys
|
|
from typing import TYPE_CHECKING, Iterable, List, Optional, Union
|
|
|
|
from ray.data._internal.tensor_extensions.arrow import pyarrow_table_from_pydict
|
|
from ray.data._internal.util import _check_pyarrow_version
|
|
from ray.data.block import Block, BlockAccessor, BlockMetadata
|
|
from ray.data.dataset import Dataset
|
|
from ray.data.datasource import Datasource, ReadTask
|
|
|
|
if TYPE_CHECKING:
|
|
import datasets
|
|
|
|
from ray.data.context import DataContext
|
|
|
|
|
|
TRANSFORMERS_IMPORT_ERROR: Optional[ImportError] = None
|
|
|
|
try:
|
|
# Due to HF Dataset's dynamic module system, we need to dynamically import the
|
|
# datasets_modules module on every actor when training.
|
|
# We accomplish this by simply running the following bit of code directly
|
|
# in the module you are currently viewing. This ensures that when we
|
|
# unpickle the Dataset, it runs before pickle tries to
|
|
# import datasets_modules and prevents an exception from being thrown.
|
|
# Same logic is present inside HF Transformers Ray
|
|
# integration: https://github.com/huggingface/transformers/blob/\
|
|
# 7d5fde991d598370d961be8cb7add6541e2b59ce/src/transformers/integrations.py#L271
|
|
# Also see https://github.com/ray-project/ray/issues/28084
|
|
from transformers.utils import is_datasets_available
|
|
|
|
if "datasets_modules" not in sys.modules and is_datasets_available():
|
|
import importlib
|
|
import importlib.metadata
|
|
import os
|
|
|
|
import datasets.load
|
|
from packaging.version import parse
|
|
|
|
# Datasets >= 4.0 removed dataset scripts support and the dynamic-modules cache.
|
|
# Only initialize dynamic modules on <= 3.x where the initializer `init_dynamic_modules` exists.
|
|
DATASETS_VERSION = parse(importlib.metadata.version("datasets"))
|
|
DATASETS_VERSION_WITHOUT_SCRIPT_SUPPORT = parse("4.0.0")
|
|
|
|
if DATASETS_VERSION < DATASETS_VERSION_WITHOUT_SCRIPT_SUPPORT:
|
|
dynamic_modules_path = os.path.join(
|
|
datasets.load.init_dynamic_modules(), "__init__.py"
|
|
)
|
|
# load dynamic_modules from path
|
|
spec = importlib.util.spec_from_file_location(
|
|
"datasets_modules", dynamic_modules_path
|
|
)
|
|
datasets_modules = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = datasets_modules
|
|
spec.loader.exec_module(datasets_modules)
|
|
except ImportError as e:
|
|
TRANSFORMERS_IMPORT_ERROR = e
|
|
|
|
|
|
class HuggingFaceDatasource(Datasource):
|
|
"""Hugging Face Dataset datasource, for reading from a
|
|
`Hugging Face Datasets Dataset <https://huggingface.co/docs/datasets/package_reference/main_classes#datasets.Dataset/>`_.
|
|
This Datasource implements a streamed read using a
|
|
single read task, most beneficial for a
|
|
`Hugging Face Datasets IterableDataset <https://huggingface.co/docs/datasets/package_reference/main_classes#datasets.IterableDataset/>`_
|
|
or datasets which are too large to fit in-memory.
|
|
For an in-memory Hugging Face Dataset (`datasets.Dataset`), use :meth:`~ray.data.from_huggingface`
|
|
directly for faster performance.
|
|
""" # noqa: E501
|
|
|
|
def __init__(
|
|
self,
|
|
dataset: Union["datasets.Dataset", "datasets.IterableDataset"],
|
|
batch_size: int = 4096,
|
|
):
|
|
if TRANSFORMERS_IMPORT_ERROR is not None:
|
|
raise TRANSFORMERS_IMPORT_ERROR
|
|
|
|
self._dataset = dataset
|
|
self._batch_size = batch_size
|
|
|
|
@classmethod
|
|
def list_parquet_urls_from_dataset(
|
|
cls, dataset: Union["datasets.Dataset", "datasets.IterableDataset"]
|
|
) -> Dataset:
|
|
"""Return list of Hugging Face hosted parquet file URLs if they
|
|
exist for the data (i.e. if the dataset is a public dataset that
|
|
has not been transformed) else return an empty list."""
|
|
import datasets
|
|
|
|
# We can use the dataset name, config name, and split name to load
|
|
# public hugging face datasets from the Hugging Face Hub. More info
|
|
# here: https://huggingface.co/docs/datasets-server/parquet
|
|
dataset_name = dataset.info.dataset_name
|
|
config_name = dataset.info.config_name
|
|
split_name = str(dataset.split)
|
|
|
|
# If a dataset is not an iterable dataset, we will check if the
|
|
# dataset with the matching dataset name, config name, and split name
|
|
# on the Hugging Face Hub has the same fingerprint as the
|
|
# dataset passed into this function. If it is not matching, transforms
|
|
# or other operations have been performed so we cannot use the parquet
|
|
# files on the Hugging Face Hub, so we return an empty list.
|
|
if not isinstance(dataset, datasets.IterableDataset):
|
|
from datasets import load_dataset
|
|
|
|
try:
|
|
ds = load_dataset(dataset_name, config_name, split=split_name)
|
|
if ds._fingerprint != dataset._fingerprint:
|
|
return []
|
|
except Exception:
|
|
# If an exception is thrown when trying to reload the dataset
|
|
# we should exit gracefully by returning an empty list.
|
|
return []
|
|
|
|
import requests
|
|
|
|
public_url = (
|
|
f"https://huggingface.co/api/datasets/{dataset_name}"
|
|
f"/parquet/{config_name}/{split_name}"
|
|
)
|
|
resp = requests.get(public_url)
|
|
if resp.status_code == requests.codes["ok"]:
|
|
# dataset corresponds to a public dataset, return list of parquet_files
|
|
return resp.json()
|
|
else:
|
|
return []
|
|
|
|
def estimate_inmemory_data_size(self) -> Optional[int]:
|
|
return self._dataset.dataset_size
|
|
|
|
def _read_dataset(self) -> Iterable[Block]:
|
|
# Note: This is a method instead of a higher level function because
|
|
# we need to capture `self`. This will trigger the try-import logic at
|
|
# the top of file to avoid import error of dataset_modules.
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pyarrow
|
|
|
|
for batch in self._dataset.with_format("arrow").iter(
|
|
batch_size=self._batch_size
|
|
):
|
|
# HuggingFace IterableDatasets do not fully support methods like
|
|
# `set_format`, `with_format`, and `formatted_as`, so the dataset
|
|
# can return whatever is the default configured batch type, even if
|
|
# the format is manually overridden before iterating above.
|
|
# Therefore, we limit support to batch formats which have native
|
|
# block types in Ray Data (pyarrow.Table, pd.DataFrame),
|
|
# or can easily be converted to such (dict, np.array).
|
|
# See: https://github.com/huggingface/datasets/issues/3444
|
|
if not isinstance(batch, (pyarrow.Table, pd.DataFrame, dict, np.array)):
|
|
raise ValueError(
|
|
f"Batch format {type(batch)} isn't supported. Only the "
|
|
f"following batch formats are supported: "
|
|
f"dict (corresponds to `None` in `dataset.with_format()`), "
|
|
f"pyarrow.Table, np.array, pd.DataFrame."
|
|
)
|
|
# Ensure np.arrays are wrapped in a dict
|
|
# (subsequently converted to a pyarrow.Table).
|
|
if isinstance(batch, np.ndarray):
|
|
batch = {"item": batch}
|
|
if isinstance(batch, dict):
|
|
batch = pyarrow_table_from_pydict(batch)
|
|
# Ensure that we return the default block type.
|
|
block = BlockAccessor.for_block(batch).to_default()
|
|
yield block
|
|
|
|
def get_read_tasks(
|
|
self,
|
|
parallelism: int,
|
|
per_task_row_limit: Optional[int] = None,
|
|
data_context: Optional["DataContext"] = None,
|
|
) -> List[ReadTask]:
|
|
# Note: `parallelism` arg is currently not used by HuggingFaceDatasource.
|
|
# We always generate a single ReadTask to perform the read.
|
|
_check_pyarrow_version()
|
|
|
|
# TODO(scottjlee): IterableDataset doesn't provide APIs
|
|
# for getting number of rows, byte size, etc., so the
|
|
# BlockMetadata is currently empty. Properly retrieve
|
|
# or calculate these so that progress bars have meaning.
|
|
meta = BlockMetadata(
|
|
num_rows=None,
|
|
size_bytes=None,
|
|
input_files=None,
|
|
exec_stats=None,
|
|
)
|
|
read_tasks: List[ReadTask] = [
|
|
ReadTask(
|
|
self._read_dataset,
|
|
meta,
|
|
per_task_row_limit=per_task_row_limit,
|
|
)
|
|
]
|
|
return read_tasks
|