chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,286 @@
"""File chunkers for DataSourceV2.
A ``FileChunker`` decides how a single listed file is split into one or
more parallel-read units. The indexer drives the chunker once per file
and emits one manifest row per chunk; downstream the partitioner /
reader carry the per-chunk metadata through to the read task.
"""
import abc
import logging
import math
from typing import (
TYPE_CHECKING,
Iterable,
Optional,
Tuple,
Type,
TypedDict,
TypeVar,
cast,
get_type_hints,
)
from ray.data._internal.util import MiB, infer_compression
from ray.util.annotations import DeveloperAPI
if TYPE_CHECKING:
from pyarrow.fs import FileSystem
logger = logging.getLogger(__name__)
class ChunkMetadata(TypedDict):
"""Base interface for chunk metadata types."""
pass
_ChunkMetadataT = TypeVar("_ChunkMetadataT", bound=ChunkMetadata)
def create_chunk_metadata(cls: Type[_ChunkMetadataT], **kwargs) -> _ChunkMetadataT:
"""Create a metadata instance with validation, ensure the keys are correct."""
required_keys = list(get_type_hints(cls).keys())
missing_keys = [key for key in required_keys if key not in kwargs]
if missing_keys:
raise ValueError(f"Missing required keys: {missing_keys}")
extra_keys = [key for key in kwargs if key not in required_keys]
if extra_keys:
raise ValueError(f"Unexpected keys: {extra_keys}")
return cast(_ChunkMetadataT, kwargs)
class LineDelimitedFileChunkMetadata(ChunkMetadata):
"""Metadata for line-delimited file chunks."""
chunk_byte_start_idx: int
chunk_byte_end_idx: int
class ParquetFileChunkMetadata(ChunkMetadata):
"""Metadata for Parquet file chunks.
A chunk is an explicit, half-open range of consecutive row groups
``[row_group_start, row_group_end)`` within a single file, computed at
listing time from the file's footer. The reader slices the fragment to
exactly this range — no estimation or read-time reconciliation.
"""
row_group_start: int # inclusive
row_group_end: int # exclusive
@DeveloperAPI
class FileChunker(abc.ABC):
"""Abstract base class for chunking files into smaller pieces for parallel processing.
File chunkers determine how large files should be split into chunks that can be
processed in parallel. Different file formats may require different chunking strategies.
For example:
- Line-delimited files (JSONL, CSV) can be chunked by byte ranges
- Parquet files can be chunked by row groups
"""
# Whether ``generate_chunk_metadatas`` performs file I/O (e.g. reading a
# Parquet footer). When True, the indexer fans chunking across its thread
# pool so the per-file reads parallelize even for a single input
# directory. When False, the indexer chunks inline (no thread hand-off).
reads_file_metadata: bool = False
@abc.abstractmethod
def generate_chunk_metadatas(
self,
path: str,
file_size: int,
filesystem: Optional["FileSystem"] = None,
) -> Iterable[Tuple[Optional[ChunkMetadata], int]]:
"""Generate metadata for file chunks.
Args:
path: The file path being chunked.
file_size: The total size in bytes of the file to be chunked.
filesystem: PyArrow filesystem used to read per-file metadata
(e.g. the Parquet footer). Ignored by chunkers that do not
read file metadata.
Returns:
An iterable of tuples containing (metadata, chunk_size) where metadata
describes the chunk and chunk_size is the size of the chunk in bytes.
Metadata can be None for chunks that don't require metadata
(e.g., whole file processing).
"""
...
@DeveloperAPI
class WholeFileChunker(FileChunker):
"""File chunker that treats the whole file as a single chunk.
This chunker is used when files should be processed as a single unit,
typically for smaller files or when the file format doesn't support
efficient chunking (e.g., compressed files).
Yields a single chunk with no metadata, indicating the entire file
should be processed as one unit.
"""
def generate_chunk_metadatas(
self,
path: str,
file_size: int,
filesystem: Optional["FileSystem"] = None,
) -> Iterable[Tuple[Optional[ChunkMetadata], int]]:
yield None, file_size
@DeveloperAPI
class LineDelimitedFileChunker(FileChunker):
"""File chunker for line-delimited files (JSONL, CSV, TSV, etc.).
This chunker splits files into fixed-size byte chunks (default: 256 MiB)
and provides metadata about the byte ranges for each chunk. The actual
line boundaries are handled by the reader to ensure complete records.
"""
_CHUNK_BYTE_SIZE = 256 * MiB # 256 MiB
def generate_chunk_metadatas(
self,
path: str,
file_size: int,
filesystem: Optional["FileSystem"] = None,
) -> Iterable[Tuple[Optional[ChunkMetadata], int]]:
compression = infer_compression(path)
if compression is not None:
yield None, file_size
else:
num_chunks = math.ceil(file_size / self._CHUNK_BYTE_SIZE)
for chunk_idx in range(num_chunks):
chunk_start = self._CHUNK_BYTE_SIZE * chunk_idx
chunk_end = min(self._CHUNK_BYTE_SIZE * (chunk_idx + 1), file_size)
chunk_size = chunk_end - chunk_start
yield (
create_chunk_metadata(
LineDelimitedFileChunkMetadata,
chunk_byte_start_idx=chunk_start,
chunk_byte_end_idx=chunk_end,
),
chunk_size,
)
@DeveloperAPI
class ParquetFileChunker(FileChunker):
"""File chunker for Parquet files.
Reads each file's footer at listing time and chunks on **true row-group
boundaries**: consecutive row groups are bundled into a chunk until the
bundle's on-disk size reaches ``target_chunk_size`` (always at least one
row group per chunk). Each chunk carries an explicit half-open row-group
range, so the reader slices to exactly those row groups with no
estimation or read-time reconciliation, and the listing stage never
produces empty read tasks.
The row group is Parquet's atomic read unit, so a chunk can never be
smaller than a single row group. With the default target (which falls
back to ``DataContext.target_min_block_size``), a file's row groups map
1:1 to chunks unless they are smaller than the target, in which case
consecutive small row groups are bundled to avoid an excessive number of
tiny chunks.
"""
# Hard fallback used only when neither an explicit target nor the
# DataContext size knobs are set.
_FALLBACK_TARGET_CHUNK_SIZE = 1 * MiB
# Footer reads are file I/O — let the indexer parallelize them.
reads_file_metadata: bool = True
def __init__(self, target_chunk_size: Optional[int] = None):
from ray.data.context import DataContext
ctx = DataContext.get_current()
# Resolve with explicit ``is not None`` checks rather than ``or`` so an
# explicit ``0`` (e.g. to force one row group per chunk) isn't treated as
# "unset" and silently overridden by a falsy-coalescing fallback.
if target_chunk_size is not None:
self._target_chunk_size = target_chunk_size
elif ctx.parquet_chunker_target_chunk_size is not None:
self._target_chunk_size = ctx.parquet_chunker_target_chunk_size
elif ctx.target_min_block_size is not None:
self._target_chunk_size = ctx.target_min_block_size
else:
self._target_chunk_size = self._FALLBACK_TARGET_CHUNK_SIZE
def generate_chunk_metadatas(
self,
path: str,
file_size: int,
filesystem: Optional["FileSystem"] = None,
) -> Iterable[Tuple[Optional[ChunkMetadata], int]]:
import pyarrow.parquet as pq
try:
# Reads only the Parquet footer (file metadata), not data.
metadata = pq.read_metadata(path, filesystem=filesystem)
except Exception as e:
# Corrupt / unreadable footer (or a non-Parquet file that slipped
# through). Fall back to a single whole-file chunk so the file is
# still read rather than dropped.
logger.debug(
"Could not read Parquet footer for chunking (%s): %s; "
"falling back to a whole-file chunk.",
path,
e,
)
yield None, file_size
return
num_row_groups = metadata.num_row_groups
if num_row_groups == 0:
yield None, file_size
return
# Greedily bundle consecutive row groups until the running on-disk
# size reaches the target. Always emit at least one row group per
# chunk (the atomic read unit).
start = 0
running_size = 0
for rg_idx in range(num_row_groups):
rg_meta = metadata.row_group(rg_idx)
# On-disk (compressed) row-group size. ``RowGroupMetaData`` exposes
# only the *uncompressed* ``total_byte_size``; the on-disk size lives
# on each ``ColumnChunkMetaData``, so sum the per-column compressed
# sizes. Keeping chunk sizes in on-disk units matches the manifest's
# ``file_sizes`` and the ``×encoding_ratio`` in-memory estimator.
rg_size = sum(
rg_meta.column(c).total_compressed_size
for c in range(rg_meta.num_columns)
)
if running_size > 0 and running_size + rg_size > self._target_chunk_size:
yield (
create_chunk_metadata(
ParquetFileChunkMetadata,
row_group_start=start,
row_group_end=rg_idx,
),
running_size,
)
start = rg_idx
running_size = 0
running_size += rg_size
# Flush the final bundle.
yield (
create_chunk_metadata(
ParquetFileChunkMetadata,
row_group_start=start,
row_group_end=num_row_groups,
),
running_size,
)
@@ -0,0 +1,49 @@
"""Parquet file-level chunking helpers for DataSourceV2.
Maps planner chunk metadata (``ParquetFileChunkMetadata``) to PyArrow
``ParquetFileFragment`` subsets for parallel reads. Chunk metadata carries
an explicit half-open row-group range computed at listing time from the
file's footer, so no estimation or reconciliation is needed here.
"""
from typing import List, Tuple
import pyarrow.dataset as pds
from ray.data._internal.datasource_v2.chunkers.file_chunker import (
ParquetFileChunkMetadata,
)
def _fragments_from_chunk_metadata(
fragment: pds.ParquetFileFragment,
chunk_metadata: ParquetFileChunkMetadata,
) -> List[Tuple[pds.ParquetFileFragment, int]]:
"""Slice ``fragment`` into per-row-group sub-fragments for the chunk's range.
The chunk carries an explicit ``[row_group_start, row_group_end)`` range.
Returns one ``(ParquetFileFragment, file_row_offset)`` pair per row group
in that range, where ``file_row_offset`` is the sum of ``num_rows`` across
all row groups that precede the sub-fragment in the underlying file.
Callers seed per-fragment hashing offsets with this value so sub-fragments
of the same file don't collide on ``(path, 0, n)``.
The range is defensively clamped to the file's actual row-group count;
since ranges are computed from the same footer the reader sees, the clamp
is a no-op in practice and never drops real row groups.
"""
start = chunk_metadata["row_group_start"]
end = chunk_metadata["row_group_end"]
metadata = fragment.metadata
total_row_groups = metadata.num_row_groups
start = min(start, total_row_groups)
end = min(end, total_row_groups)
file_row_offset = sum(metadata.row_group(i).num_rows for i in range(start))
sub_fragments: List[Tuple[pds.ParquetFileFragment, int]] = []
for row_group_index in range(start, end):
sub_fragments.append(
(fragment.subset(row_group_ids=[row_group_index]), file_row_offset)
)
file_row_offset += metadata.row_group(row_group_index).num_rows
return sub_fragments