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,29 @@
"""Cloud filesystem module for provider-specific implementations.
This module provides a unified interface for cloud storage operations across
different providers (S3, GCS, Azure) while allowing provider-specific optimizations.
"""
from ray.llm._internal.common.utils.cloud_filesystem.azure_filesystem import (
AzureFileSystem,
)
from ray.llm._internal.common.utils.cloud_filesystem.base import (
BaseCloudFileSystem,
)
from ray.llm._internal.common.utils.cloud_filesystem.gcs_filesystem import (
GCSFileSystem,
)
from ray.llm._internal.common.utils.cloud_filesystem.pyarrow_filesystem import (
PyArrowFileSystem,
)
from ray.llm._internal.common.utils.cloud_filesystem.s3_filesystem import (
S3FileSystem,
)
__all__ = [
"BaseCloudFileSystem",
"PyArrowFileSystem",
"GCSFileSystem",
"AzureFileSystem",
"S3FileSystem",
]
@@ -0,0 +1,82 @@
"""Azure-specific filesystem implementation.
This module provides an Azure-specific implementation that delegates to PyArrowFileSystem.
This maintains backward compatibility while allowing for future optimizations using
native Azure tools (azcopy, azure-storage-blob SDK).
"""
from typing import List, Optional, Union
from ray.llm._internal.common.utils.cloud_filesystem.base import BaseCloudFileSystem
from ray.llm._internal.common.utils.cloud_filesystem.pyarrow_filesystem import (
PyArrowFileSystem,
)
class AzureFileSystem(BaseCloudFileSystem):
"""Azure-specific implementation of cloud filesystem operations.
**Note**: This implementation currently delegates to PyArrowFileSystem to maintain
stability. Optimized implementation using azure-storage-blob SDK and azcopy
will be added in a future PR.
"""
@staticmethod
def get_file(
object_uri: str, decode_as_utf_8: bool = True
) -> Optional[Union[str, bytes]]:
"""Download a file from cloud storage into memory.
Args:
object_uri: URI of the file (abfss:// or azure://)
decode_as_utf_8: If True, decode the file as UTF-8
Returns:
File contents as string or bytes, or None if file doesn't exist
"""
return PyArrowFileSystem.get_file(object_uri, decode_as_utf_8)
@staticmethod
def list_subfolders(folder_uri: str) -> List[str]:
"""List the immediate subfolders in a cloud directory.
Args:
folder_uri: URI of the directory (abfss:// or azure://)
Returns:
List of subfolder names (without trailing slashes)
"""
return PyArrowFileSystem.list_subfolders(folder_uri)
@staticmethod
def download_files(
path: str,
bucket_uri: str,
substrings_to_include: Optional[List[str]] = None,
suffixes_to_exclude: Optional[List[str]] = None,
) -> None:
"""Download files from cloud storage to a local directory.
Args:
path: Local directory where files will be downloaded
bucket_uri: URI of cloud directory
substrings_to_include: Only include files containing these substrings
suffixes_to_exclude: Exclude certain files from download (e.g .safetensors)
"""
PyArrowFileSystem.download_files(
path, bucket_uri, substrings_to_include, suffixes_to_exclude
)
@staticmethod
def upload_files(
local_path: str,
bucket_uri: str,
) -> None:
"""Upload files to cloud storage.
Args:
local_path: The local path of the files to upload.
bucket_uri: The bucket uri to upload the files to, must start with
`abfss://` or `azure://`.
"""
PyArrowFileSystem.upload_files(local_path, bucket_uri)
@@ -0,0 +1,81 @@
"""Abstract base class for cloud filesystem implementations.
This module defines the interface that all cloud storage provider implementations
must follow, ensuring consistency across different providers while allowing
provider-specific optimizations.
"""
from abc import ABC, abstractmethod
from typing import List, Optional, Union
class BaseCloudFileSystem(ABC):
"""Abstract base class for cloud filesystem implementations.
This class defines the interface that all cloud storage provider implementations
must implement. Provider-specific classes (S3FileSystem, GCSFileSystem, etc.)
will inherit from this base class and provide optimized implementations for
their respective cloud storage platforms.
"""
@staticmethod
@abstractmethod
def get_file(
object_uri: str, decode_as_utf_8: bool = True
) -> Optional[Union[str, bytes]]:
"""Download a file from cloud storage into memory.
Args:
object_uri: URI of the file (s3://, gs://, abfss://, or azure://)
decode_as_utf_8: If True, decode the file as UTF-8
Returns:
File contents as string or bytes, or None if file doesn't exist
"""
pass
@staticmethod
@abstractmethod
def list_subfolders(folder_uri: str) -> List[str]:
"""List the immediate subfolders in a cloud directory.
Args:
folder_uri: URI of the directory (s3://, gs://, abfss://, or azure://)
Returns:
List of subfolder names (without trailing slashes)
"""
pass
@staticmethod
@abstractmethod
def download_files(
path: str,
bucket_uri: str,
substrings_to_include: Optional[List[str]] = None,
suffixes_to_exclude: Optional[List[str]] = None,
) -> None:
"""Download files from cloud storage to a local directory.
Args:
path: Local directory where files will be downloaded
bucket_uri: URI of cloud directory
substrings_to_include: Only include files containing these substrings
suffixes_to_exclude: Exclude certain files from download (e.g .safetensors)
"""
pass
@staticmethod
@abstractmethod
def upload_files(
local_path: str,
bucket_uri: str,
) -> None:
"""Upload files to cloud storage.
Args:
local_path: The local path of the files to upload.
bucket_uri: The bucket uri to upload the files to, must start with
`s3://`, `gs://`, `abfss://`, or `azure://`.
"""
pass
@@ -0,0 +1,81 @@
"""GCS-specific filesystem implementation.
This module provides a GCS-specific implementation.
This maintains backward compatibility while allowing for future optimizations using
native GCS tools (gsutil, google-cloud-storage SDK).
"""
from typing import List, Optional, Union
from ray.llm._internal.common.utils.cloud_filesystem.base import BaseCloudFileSystem
from ray.llm._internal.common.utils.cloud_filesystem.pyarrow_filesystem import (
PyArrowFileSystem,
)
class GCSFileSystem(BaseCloudFileSystem):
"""GCS-specific implementation of cloud filesystem operations.
**Note**: This implementation currently delegates to PyArrowFileSystem to maintain
stability. Optimized implementation using google-cloud-storage SDK and gsutil
will be added in a future PR.
"""
@staticmethod
def get_file(
object_uri: str, decode_as_utf_8: bool = True
) -> Optional[Union[str, bytes]]:
"""Download a file from cloud storage into memory.
Args:
object_uri: URI of the file (gs://)
decode_as_utf_8: If True, decode the file as UTF-8
Returns:
File contents as string or bytes, or None if file doesn't exist
"""
return PyArrowFileSystem.get_file(object_uri, decode_as_utf_8)
@staticmethod
def list_subfolders(folder_uri: str) -> List[str]:
"""List the immediate subfolders in a cloud directory.
Args:
folder_uri: URI of the directory (gs://)
Returns:
List of subfolder names (without trailing slashes)
"""
return PyArrowFileSystem.list_subfolders(folder_uri)
@staticmethod
def download_files(
path: str,
bucket_uri: str,
substrings_to_include: Optional[List[str]] = None,
suffixes_to_exclude: Optional[List[str]] = None,
) -> None:
"""Download files from cloud storage to a local directory.
Args:
path: Local directory where files will be downloaded
bucket_uri: URI of cloud directory
substrings_to_include: Only include files containing these substrings
suffixes_to_exclude: Exclude certain files from download (e.g .safetensors)
"""
PyArrowFileSystem.download_files(
path, bucket_uri, substrings_to_include, suffixes_to_exclude
)
@staticmethod
def upload_files(
local_path: str,
bucket_uri: str,
) -> None:
"""Upload files to cloud storage.
Args:
local_path: The local path of the files to upload.
bucket_uri: The bucket uri to upload the files to, must start with `gs://`.
"""
PyArrowFileSystem.upload_files(local_path, bucket_uri)
@@ -0,0 +1,389 @@
"""PyArrow-based filesystem implementation for cloud storage.
This module provides a PyArrow-based implementation of the cloud filesystem
interface, supporting S3, GCS, and Azure storage providers.
"""
import os
from concurrent.futures import ThreadPoolExecutor
from typing import List, Optional, Tuple, Union
from urllib.parse import urlparse
import pyarrow.fs as pa_fs
from ray.llm._internal.common.observability.logging import get_logger
from ray.llm._internal.common.utils.cloud_filesystem.base import BaseCloudFileSystem
logger = get_logger(__name__)
class PyArrowFileSystem(BaseCloudFileSystem):
"""PyArrow-based implementation of cloud filesystem operations.
This class provides a unified interface for cloud storage operations using
PyArrow's filesystem abstraction. It supports S3, GCS, and Azure storage
providers.
"""
@staticmethod
def get_fs_and_path(object_uri: str) -> Tuple[pa_fs.FileSystem, str]:
"""Get the appropriate filesystem and path from a URI.
Args:
object_uri: URI of the file (s3://, gs://, abfss://, or azure://)
If URI contains 'anonymous@', anonymous access is used.
Example: s3://anonymous@bucket/path
Returns:
Tuple of (filesystem, path)
"""
if object_uri.startswith("pyarrow-"):
object_uri = object_uri[8:]
anonymous = False
# Check for anonymous access pattern (only for S3/GCS)
# e.g. s3://anonymous@bucket/path
if "@" in object_uri and not (
object_uri.startswith("abfss://") or object_uri.startswith("azure://")
):
parts = object_uri.split("@", 1)
# Check if the first part ends with "anonymous"
if parts[0].endswith("anonymous"):
anonymous = True
# Remove the anonymous@ part, keeping the scheme
scheme = parts[0].split("://")[0]
object_uri = f"{scheme}://{parts[1]}"
if object_uri.startswith("s3://"):
endpoint = os.getenv("AWS_ENDPOINT_URL_S3", None)
virtual_hosted_style = os.getenv("AWS_S3_ADDRESSING_STYLE", None)
fs = pa_fs.S3FileSystem(
anonymous=anonymous,
endpoint_override=endpoint,
force_virtual_addressing=(virtual_hosted_style == "virtual"),
)
path = object_uri[5:] # Remove "s3://"
elif object_uri.startswith("gs://"):
fs = pa_fs.GcsFileSystem(anonymous=anonymous)
path = object_uri[5:] # Remove "gs://"
elif object_uri.startswith("abfss://"):
fs, path = PyArrowFileSystem._create_abfss_filesystem(object_uri)
elif object_uri.startswith("azure://"):
fs, path = PyArrowFileSystem._create_azure_filesystem(object_uri)
else:
raise ValueError(f"Unsupported URI scheme: {object_uri}")
return fs, path
@staticmethod
def _create_azure_filesystem(object_uri: str) -> Tuple[pa_fs.FileSystem, str]:
"""Create an Azure filesystem for Azure Blob Storage or ABFSS.
Args:
object_uri: Azure URI (azure://container@account.blob.core.windows.net/path or
abfss://container@account.dfs.core.windows.net/path)
Returns:
Tuple of (PyArrow FileSystem, path without scheme prefix)
Raises:
ImportError: If required dependencies are not installed.
ValueError: If the Azure URI format is invalid.
"""
try:
import adlfs
from azure.identity import DefaultAzureCredential
except ImportError:
raise ImportError(
"You must `pip install adlfs azure-identity` "
"to use Azure/ABFSS URIs. "
"Note that these must be preinstalled on all nodes in the Ray cluster."
)
# Parse and validate the Azure URI
parsed = urlparse(object_uri)
scheme = parsed.scheme.lower()
# Validate URI format: scheme://container@account.domain/path
if not parsed.netloc or "@" not in parsed.netloc:
raise ValueError(
f"Invalid {scheme.upper()} URI format - missing container@account: {object_uri}"
)
container_part, hostname_part = parsed.netloc.split("@", 1)
# Validate container name (must be non-empty)
if not container_part:
raise ValueError(
f"Invalid {scheme.upper()} URI format - empty container name: {object_uri}"
)
# Validate hostname format based on scheme
valid_hostname = False
if scheme == "abfss":
valid_hostname = hostname_part.endswith(".dfs.core.windows.net")
expected_domains = ".dfs.core.windows.net"
elif scheme == "azure":
valid_hostname = hostname_part.endswith(
".blob.core.windows.net"
) or hostname_part.endswith(".dfs.core.windows.net")
expected_domains = ".blob.core.windows.net or .dfs.core.windows.net"
if not hostname_part or not valid_hostname:
raise ValueError(
f"Invalid {scheme.upper()} URI format - invalid hostname (must end with {expected_domains}): {object_uri}"
)
# Extract and validate account name
azure_storage_account_name = hostname_part.split(".")[0]
if not azure_storage_account_name:
raise ValueError(
f"Invalid {scheme.upper()} URI format - empty account name: {object_uri}"
)
# Create the adlfs filesystem
adlfs_fs = adlfs.AzureBlobFileSystem(
account_name=azure_storage_account_name,
credential=DefaultAzureCredential(),
)
# Wrap with PyArrow's PyFileSystem for compatibility
fs = pa_fs.PyFileSystem(pa_fs.FSSpecHandler(adlfs_fs))
# Return the path without the scheme prefix
path = f"{container_part}{parsed.path}"
return fs, path
@staticmethod
def _create_abfss_filesystem(object_uri: str) -> Tuple[pa_fs.FileSystem, str]:
"""Create an ABFSS filesystem for Azure Data Lake Storage Gen2.
This is a wrapper around _create_azure_filesystem for backward compatibility.
Args:
object_uri: ABFSS URI (abfss://container@account.dfs.core.windows.net/path)
Returns:
Tuple of (PyArrow FileSystem, path without abfss:// prefix)
"""
return PyArrowFileSystem._create_azure_filesystem(object_uri)
@staticmethod
def _filter_files(
fs: pa_fs.FileSystem,
source_path: str,
destination_path: str,
substrings_to_include: Optional[List[str]] = None,
suffixes_to_exclude: Optional[List[str]] = None,
) -> List[Tuple[str, str]]:
"""Filter files from cloud storage based on inclusion and exclusion criteria.
Args:
fs: PyArrow filesystem instance
source_path: Source path in cloud storage
destination_path: Local destination path
substrings_to_include: Only include files containing these substrings
suffixes_to_exclude: Exclude files ending with these suffixes
Returns:
List of tuples containing (source_file_path, destination_file_path)
"""
file_selector = pa_fs.FileSelector(source_path, recursive=True)
file_infos = fs.get_file_info(file_selector)
path_pairs = []
for file_info in file_infos:
if file_info.type != pa_fs.FileType.File:
continue
rel_path = file_info.path[len(source_path) :].lstrip("/")
# Apply filters
if substrings_to_include:
if not any(
substring in rel_path for substring in substrings_to_include
):
continue
if suffixes_to_exclude:
if any(rel_path.endswith(suffix) for suffix in suffixes_to_exclude):
continue
path_pairs.append(
(file_info.path, os.path.join(destination_path, rel_path))
)
return path_pairs
@staticmethod
def get_file(
object_uri: str, decode_as_utf_8: bool = True
) -> Optional[Union[str, bytes]]:
"""Download a file from cloud storage into memory.
Args:
object_uri: URI of the file (s3://, gs://, abfss://, or azure://)
decode_as_utf_8: If True, decode the file as UTF-8
Returns:
File contents as string or bytes, or None if file doesn't exist
"""
try:
fs, path = PyArrowFileSystem.get_fs_and_path(object_uri)
# Check if file exists
if not fs.get_file_info(path).type == pa_fs.FileType.File:
logger.info(f"URI {object_uri} does not exist.")
return None
# Read file
with fs.open_input_file(path) as f:
body = f.read()
if decode_as_utf_8:
body = body.decode("utf-8")
return body
except Exception as e:
logger.warning(f"Error reading {object_uri}: {e}")
return None
@staticmethod
def list_subfolders(folder_uri: str) -> List[str]:
"""List the immediate subfolders in a cloud directory.
Args:
folder_uri: URI of the directory (s3://, gs://, abfss://, or azure://)
Returns:
List of subfolder names (without trailing slashes)
"""
# Ensure that the folder_uri has a trailing slash.
folder_uri = f"{folder_uri.rstrip('/')}/"
try:
fs, path = PyArrowFileSystem.get_fs_and_path(folder_uri)
# List directory contents
file_infos = fs.get_file_info(pa_fs.FileSelector(path, recursive=False))
# Filter for directories and extract subfolder names
subfolders = []
for file_info in file_infos:
if file_info.type == pa_fs.FileType.Directory:
# Extract just the subfolder name without the full path
subfolder = os.path.basename(file_info.path.rstrip("/"))
subfolders.append(subfolder)
return subfolders
except Exception as e:
logger.error(f"Error listing subfolders in {folder_uri}: {e}")
return []
@staticmethod
def download_files(
path: str,
bucket_uri: str,
substrings_to_include: Optional[List[str]] = None,
suffixes_to_exclude: Optional[List[str]] = None,
max_concurrency: int = 10,
chunk_size: int = 64 * 1024 * 1024,
) -> None:
"""Download files from cloud storage to a local directory.
Args:
path: Local directory where files will be downloaded
bucket_uri: URI of cloud directory
substrings_to_include: Only include files containing these substrings
suffixes_to_exclude: Exclude certain files from download
max_concurrency: Maximum number of concurrent files to download (default: 10)
chunk_size: Size of transfer chunks (default: 64MB)
"""
try:
fs, source_path = PyArrowFileSystem.get_fs_and_path(bucket_uri)
# Ensure destination exists
os.makedirs(path, exist_ok=True)
# If no filters, use direct copy_files
if not substrings_to_include and not suffixes_to_exclude:
pa_fs.copy_files(
source=source_path,
destination=path,
source_filesystem=fs,
destination_filesystem=pa_fs.LocalFileSystem(),
use_threads=True,
chunk_size=chunk_size,
)
return
# List and filter files
files_to_download = PyArrowFileSystem._filter_files(
fs, source_path, path, substrings_to_include, suffixes_to_exclude
)
if not files_to_download:
logger.info("Filters do not match any of the files, skipping download")
return
def download_single_file(file_paths):
source_file_path, dest_file_path = file_paths
# Create destination directory if needed
dest_dir = os.path.dirname(dest_file_path)
if dest_dir:
os.makedirs(dest_dir, exist_ok=True)
# Use PyArrow's copy_files for individual files,
pa_fs.copy_files(
source=source_file_path,
destination=dest_file_path,
source_filesystem=fs,
destination_filesystem=pa_fs.LocalFileSystem(),
use_threads=True,
chunk_size=chunk_size,
)
return dest_file_path
max_workers = min(max_concurrency, len(files_to_download))
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(download_single_file, file_paths)
for file_paths in files_to_download
]
for future in futures:
try:
future.result()
except Exception as e:
logger.error(f"Failed to download file: {e}")
raise
except Exception as e:
logger.exception(f"Error downloading files from {bucket_uri}: {e}")
raise
@staticmethod
def upload_files(
local_path: str,
bucket_uri: str,
) -> None:
"""Upload files to cloud storage.
Args:
local_path: The local path of the files to upload.
bucket_uri: The bucket uri to upload the files to, must start with
`s3://`, `gs://`, `abfss://`, or `azure://`.
"""
try:
fs, dest_path = PyArrowFileSystem.get_fs_and_path(bucket_uri)
pa_fs.copy_files(
source=local_path,
destination=dest_path,
source_filesystem=pa_fs.LocalFileSystem(),
destination_filesystem=fs,
)
except Exception as e:
logger.exception(f"Error uploading files to {bucket_uri}: {e}")
raise
@@ -0,0 +1,397 @@
"""S3-specific filesystem implementation using boto3.
This module provides an S3-specific implementation that uses boto3 (AWS SDK for Python)
for reliable and efficient S3 operations.
"""
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, List, Optional, Union
try:
import boto3
from botocore import UNSIGNED
from botocore.config import Config
except ImportError:
boto3 = None # type: ignore[assignment]
UNSIGNED = None # type: ignore[assignment]
Config = None # type: ignore[assignment]
from ray.llm._internal.common.observability.logging import get_logger
from ray.llm._internal.common.utils.cloud_filesystem.base import BaseCloudFileSystem
logger = get_logger(__name__)
def _check_boto3() -> None:
"""Raise a clear error if boto3/botocore are not installed."""
if boto3 is None:
raise ImportError(
"boto3 and botocore are required for S3 operations but are not installed. "
"Install them with: pip install boto3"
)
class S3FileSystem(BaseCloudFileSystem):
"""S3-specific implementation of cloud filesystem operations using boto3.
This implementation uses boto3 (AWS SDK for Python) for reliable and efficient
operations with S3 storage.
"""
@staticmethod
def _parse_s3_uri(uri: str) -> tuple[str, str, bool]:
"""Parse S3 URI into bucket and key.
Args:
uri: S3 URI (e.g., s3://bucket/path/to/object or s3://anonymous@bucket/path/to/object)
Returns:
Tuple of (bucket_name, key, is_anonymous)
Raises:
ValueError: If URI is not a valid S3 URI
"""
# Check if anonymous@ prefix exists
is_anonymous = False
if uri.startswith("s3://anonymous@"):
is_anonymous = True
uri = uri.replace("s3://anonymous@", "s3://", 1)
if not uri.startswith("s3://"):
raise ValueError(f"Invalid S3 URI: {uri}")
# Remove s3:// prefix and split into bucket and key
path = uri[5:] # Remove "s3://"
parts = path.split("/", 1)
bucket = parts[0]
key = parts[1] if len(parts) > 1 else ""
return bucket, key, is_anonymous
@staticmethod
def _get_s3_client(max_pool_connections: int = 50, anonymous: bool = False):
"""Create a new S3 client instance with connection pooling.
Args:
max_pool_connections: Maximum number of connections in the pool.
Should be >= max_workers for optimal performance.
anonymous: Whether to use anonymous access to S3.
Returns:
boto3 S3 client with connection pooling configured
"""
_check_boto3()
# Configure connection pooling for better concurrent performance
config = Config(
max_pool_connections=max_pool_connections,
# Retry configuration for transient failures
retries={
"max_attempts": 3,
"mode": "adaptive", # Adapts retry behavior based on error type
},
# TCP keepalive helps with long-running connections
tcp_keepalive=True,
signature_version=UNSIGNED if anonymous else None,
)
return boto3.client("s3", config=config)
@staticmethod
def get_file(
object_uri: str, decode_as_utf_8: bool = True
) -> Optional[Union[str, bytes]]:
"""Download a file from cloud storage into memory.
Args:
object_uri: URI of the file (s3://)
decode_as_utf_8: If True, decode the file as UTF-8
Returns:
File contents as string or bytes, or None if file doesn't exist
"""
_check_boto3()
try:
bucket, key, is_anonymous = S3FileSystem._parse_s3_uri(object_uri)
s3_client = S3FileSystem._get_s3_client(anonymous=is_anonymous)
# Download file directly into memory
response = s3_client.get_object(Bucket=bucket, Key=key)
body = response["Body"].read()
if decode_as_utf_8:
return body.decode("utf-8")
return body
except Exception as e:
logger.error(f"Error reading {object_uri}: {e}")
@staticmethod
def list_subfolders(folder_uri: str) -> List[str]:
"""List the immediate subfolders in a cloud directory.
Args:
folder_uri: URI of the directory (s3://)
Returns:
List of subfolder names (without trailing slashes)
"""
_check_boto3()
try:
bucket, prefix, is_anonymous = S3FileSystem._parse_s3_uri(folder_uri)
# Ensure that the prefix has a trailing slash
if prefix and not prefix.endswith("/"):
prefix = f"{prefix}/"
s3_client = S3FileSystem._get_s3_client(anonymous=is_anonymous)
# List objects with delimiter to get only immediate subfolders
response = s3_client.list_objects_v2(
Bucket=bucket, Prefix=prefix, Delimiter="/"
)
subfolders = []
# CommonPrefixes contains the subdirectories
for common_prefix in response.get("CommonPrefixes", []):
folder_path = common_prefix["Prefix"]
# Extract the folder name from the full prefix
# Remove the parent prefix and trailing slash
folder_name = folder_path[len(prefix) :].rstrip("/")
if folder_name:
subfolders.append(folder_name)
return subfolders
except Exception as e:
logger.error(f"Error listing subfolders in {folder_uri}: {e}")
return []
@staticmethod
def _calculate_optimal_workers(
num_files: int, total_size: int, default_max: int = 100, default_min: int = 10
) -> int:
"""Calculate optimal number of workers based on file characteristics.
Args:
num_files: Number of files to download
total_size: Total size of all files in bytes
default_max: Maximum workers to cap at
default_min: Minimum workers to use
Returns:
Optimal number of workers between default_min and default_max
"""
if num_files == 0:
return default_min
avg_file_size = total_size / num_files if total_size > 0 else 0
# Strategy: More workers for smaller files, fewer for larger files
if avg_file_size < 1024 * 1024: # < 1MB (small files)
# Use more workers for many small files
workers = min(num_files, default_max)
elif avg_file_size < 10 * 1024 * 1024: # 1-10MB (medium files)
# Use moderate workers
workers = min(num_files // 2, default_max // 2)
else: # > 10MB (large files)
# Use fewer workers since each download is bandwidth-intensive
workers = min(20, num_files)
# Ensure workers is between min and max
return max(default_min, min(workers, default_max))
@staticmethod
def _download_single_file(
s3_client: Any, bucket: str, key: str, local_file_path: str
) -> tuple[str, bool]:
"""Download a single file from S3.
Args:
s3_client: Shared boto3 S3 client
bucket: S3 bucket name
key: S3 object key
local_file_path: Local path where file will be saved
Returns:
Tuple of (key, success)
"""
try:
# Create parent directories if needed
os.makedirs(os.path.dirname(local_file_path), exist_ok=True)
s3_client.download_file(bucket, key, local_file_path)
logger.debug(f"Downloaded {key} to {local_file_path}")
return key, True
except Exception as e:
logger.error(f"Failed to download {key}: {e}")
return key, False
@staticmethod
def download_files(
path: str,
bucket_uri: str,
substrings_to_include: Optional[List[str]] = None,
suffixes_to_exclude: Optional[List[str]] = None,
max_workers: Optional[int] = None,
) -> None:
"""Download files from cloud storage to a local directory concurrently.
Args:
path: Local directory where files will be downloaded
bucket_uri: URI of cloud directory
substrings_to_include: Only include files containing these substrings
suffixes_to_exclude: Exclude certain files from download (e.g .safetensors)
max_workers: Maximum number of concurrent downloads. If None, automatically
calculated based on file count and sizes (min: 10, max: 100)
"""
try:
bucket, prefix, is_anonymous = S3FileSystem._parse_s3_uri(bucket_uri)
# Ensure the destination directory exists
os.makedirs(path, exist_ok=True)
# Ensure prefix has trailing slash for directory listing
if prefix and not prefix.endswith("/"):
prefix = f"{prefix}/"
# Create initial client for listing (will recreate with proper pool size later)
s3_client = S3FileSystem._get_s3_client(anonymous=is_anonymous)
# List all objects in the bucket with the given prefix
paginator = s3_client.get_paginator("list_objects_v2")
pages = paginator.paginate(Bucket=bucket, Prefix=prefix)
# Collect all files to download and track total size
files_to_download = []
total_size = 0
for page in pages:
for obj in page.get("Contents", []):
key = obj["Key"]
size = obj.get("Size", 0)
# Skip if it's a directory marker
if key.endswith("/"):
continue
# Get the relative path (remove the prefix)
relative_path = key[len(prefix) :]
# Apply include filters
if substrings_to_include:
if not any(
substr in relative_path for substr in substrings_to_include
):
continue
# Apply exclude filters
if suffixes_to_exclude:
if any(
relative_path.endswith(suffix.lstrip("*"))
for suffix in suffixes_to_exclude
):
continue
# Construct local file path
local_file_path = os.path.join(path, relative_path)
files_to_download.append((bucket, key, local_file_path))
total_size += size
# Download files concurrently
if not files_to_download:
logger.info(f"No files matching filters to download from {bucket_uri}")
return
# Dynamically calculate workers if not provided
if max_workers is None:
max_workers = S3FileSystem._calculate_optimal_workers(
num_files=len(files_to_download),
total_size=total_size,
default_max=100,
default_min=10,
)
# Create shared client with proper connection pool size for downloads
s3_client = S3FileSystem._get_s3_client(
max_pool_connections=max_workers + 10, anonymous=is_anonymous
)
failed_downloads = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit all download tasks with shared client
future_to_key = {
executor.submit(
S3FileSystem._download_single_file,
s3_client, # Pass shared client to each worker
bucket,
key,
local_path,
): key
for bucket, key, local_path in files_to_download
}
# Process completed downloads
for future in as_completed(future_to_key):
key, success = future.result()
if not success:
failed_downloads.append(key)
# Report any failures
if failed_downloads:
logger.error(
f"Failed to download {len(failed_downloads)} files: {failed_downloads[:5]}..."
)
except Exception as e:
logger.exception(f"Error downloading files from {bucket_uri}: {e}")
raise
@staticmethod
def upload_files(
local_path: str,
bucket_uri: str,
) -> None:
"""Upload files to cloud storage.
Args:
local_path: The local path of the files to upload.
bucket_uri: The bucket uri to upload the files to, must start with `s3://`.
"""
try:
bucket, prefix, is_anonymous = S3FileSystem._parse_s3_uri(bucket_uri)
# Ensure prefix has trailing slash for directory upload
if prefix and not prefix.endswith("/"):
prefix = f"{prefix}/"
s3_client = S3FileSystem._get_s3_client(anonymous=is_anonymous)
local_path_obj = Path(local_path)
# Walk through the local directory and upload each file
if local_path_obj.is_file():
# Upload a single file
file_name = local_path_obj.name
s3_key = f"{prefix}{file_name}" if prefix else file_name
s3_client.upload_file(str(local_path_obj), bucket, s3_key)
logger.debug(f"Uploaded {local_path_obj} to s3://{bucket}/{s3_key}")
elif local_path_obj.is_dir():
# Upload directory recursively
for file_path in local_path_obj.rglob("*"):
if file_path.is_file():
# Calculate relative path from local_path
relative_path = file_path.relative_to(local_path_obj)
# Construct S3 key
s3_key = f"{prefix}{relative_path.as_posix()}"
# Upload file
s3_client.upload_file(str(file_path), bucket, s3_key)
logger.debug(f"Uploaded {file_path} to s3://{bucket}/{s3_key}")
else:
raise ValueError(
f"Path {local_path} does not exist or is not a file/directory"
)
except Exception as e:
logger.exception(f"Error uploading files to {bucket_uri}: {e}")
raise