litdata 0.2.61__tar.gz → 0.2.65__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {litdata-0.2.61/src/litdata.egg-info → litdata-0.2.65}/PKG-INFO +3 -3
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/__about__.py +1 -1
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/cli/commands.py +1 -1
- litdata-0.2.65/src/litdata/cli/handler/__init__.py +21 -0
- litdata-0.2.65/src/litdata/cli/handler/cache.py +32 -0
- litdata-0.2.65/src/litdata/cli/handler/optimize.py +19 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/constants.py +3 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/processing/data_processor.py +1 -1
- litdata-0.2.65/src/litdata/streaming/async_prefetch.py +256 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/client.py +90 -49
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/config.py +144 -30
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/dataloader.py +58 -1
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/dataset.py +46 -40
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/downloader.py +150 -28
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/fs_provider.py +9 -7
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/item_loader.py +320 -63
- litdata-0.2.65/src/litdata/streaming/reader.py +1090 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/resolver.py +13 -1
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/serializers.py +49 -6
- litdata-0.2.65/src/litdata/streaming/timing.py +84 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/writer.py +5 -1
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/parquet.py +11 -3
- {litdata-0.2.61 → litdata-0.2.65/src/litdata.egg-info}/PKG-INFO +3 -3
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata.egg-info/SOURCES.txt +5 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata.egg-info/requires.txt +2 -2
- litdata-0.2.61/src/litdata/streaming/reader.py +0 -629
- {litdata-0.2.61 → litdata-0.2.65}/CONTRIBUTING.md +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/LICENSE +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/MANIFEST.in +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/README.md +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/requirements.txt +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/setup.cfg +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/setup.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/__init__.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/__main__.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/cli/__init__.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/cli/parser.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/debugger.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/helpers.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/imports.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/processing/__init__.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/processing/functions.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/processing/readers.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/processing/utilities.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/raw/__init__.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/raw/dataset.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/raw/indexer.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/raw/types.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/requirements.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/__init__.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/cache.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/combined.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/compression.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/parallel.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/sampler.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/streaming/shuffle.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/__init__.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/_pytree.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/base.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/breakpoint.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/broadcast.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/dataset_utilities.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/encryption.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/env.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/format.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/hf_dataset.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/packing.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/shuffle.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/subsample.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/torch_utils.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata/utilities/train_test_split.py +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata.egg-info/dependency_links.txt +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata.egg-info/entry_points.txt +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata.egg-info/not-zip-safe +0 -0
- {litdata-0.2.61 → litdata-0.2.65}/src/litdata.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: litdata
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.65
|
|
4
4
|
Summary: The Deep Learning framework to train, deploy, and ship AI products Lightning fast.
|
|
5
5
|
Home-page: https://github.com/Lightning-AI/litdata
|
|
6
6
|
Download-URL: https://github.com/Lightning-AI/litdata
|
|
@@ -39,10 +39,10 @@ Requires-Dist: obstore
|
|
|
39
39
|
Provides-Extra: extras
|
|
40
40
|
Requires-Dist: fsspec; extra == "extras"
|
|
41
41
|
Requires-Dist: google-cloud-storage; extra == "extras"
|
|
42
|
-
Requires-Dist: lightning-sdk==
|
|
42
|
+
Requires-Dist: lightning-sdk==2026.2.6; extra == "extras"
|
|
43
43
|
Requires-Dist: pillow; extra == "extras"
|
|
44
44
|
Requires-Dist: polars; extra == "extras"
|
|
45
|
-
Requires-Dist: pyarrow; extra == "extras"
|
|
45
|
+
Requires-Dist: pyarrow<25.0.0; extra == "extras"
|
|
46
46
|
Requires-Dist: tqdm; extra == "extras"
|
|
47
47
|
Requires-Dist: viztracer; extra == "extras"
|
|
48
48
|
Dynamic: author
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
from argparse import _SubParsersAction
|
|
15
15
|
|
|
16
|
+
from litdata.cli import LitFormatter
|
|
16
17
|
from litdata.cli.handler.cache import clear_cache, show_cache_path
|
|
17
18
|
from litdata.cli.handler.optimize import optimize_dataset
|
|
18
|
-
from litdata.cli.parser import LitFormatter
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def register_cache_subcommand(subparser: _SubParsersAction) -> None:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
from litdata.cli.handler.cache import clear_cache, show_cache_path
|
|
15
|
+
from litdata.cli.handler.optimize import optimize_dataset
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"clear_cache",
|
|
19
|
+
"show_cache_path",
|
|
20
|
+
"optimize_dataset",
|
|
21
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
import shutil
|
|
15
|
+
from argparse import Namespace
|
|
16
|
+
|
|
17
|
+
from litdata.utilities.dataset_utilities import get_default_cache_dir
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def clear_cache(args: Namespace) -> None:
|
|
21
|
+
"""Clear default cache used for StreamingDataset and other utilities."""
|
|
22
|
+
streaming_default_cache_dir = get_default_cache_dir()
|
|
23
|
+
|
|
24
|
+
shutil.rmtree(streaming_default_cache_dir, ignore_errors=True)
|
|
25
|
+
|
|
26
|
+
print(f"Cache directory '{streaming_default_cache_dir}' cleared.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def show_cache_path(args: Namespace) -> None:
|
|
30
|
+
"""Show the path to the cache directory."""
|
|
31
|
+
streaming_default_cache_dir = get_default_cache_dir()
|
|
32
|
+
print(f"Default cache directory: {streaming_default_cache_dir}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
from argparse import Namespace
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def optimize_dataset(args: Namespace) -> None:
|
|
18
|
+
"""Handle the optimize command."""
|
|
19
|
+
print(f"Optimizing dataset at {args.dataset}...")
|
|
@@ -49,6 +49,7 @@ _PYARROW_AVAILABLE = RequirementCache("pyarrow")
|
|
|
49
49
|
_POLARS_AVAILABLE = RequirementCache("polars>1.0.0")
|
|
50
50
|
_PIL_AVAILABLE = RequirementCache("PIL")
|
|
51
51
|
_TORCH_VISION_AVAILABLE = RequirementCache("torchvision")
|
|
52
|
+
_TORCH_VISION_LESS_THAN_0_26 = RequirementCache("torchvision<0.26.0")
|
|
52
53
|
_AV_AVAILABLE = RequirementCache("av")
|
|
53
54
|
_OBSTORE_AVAILABLE = RequirementCache("obstore")
|
|
54
55
|
|
|
@@ -58,6 +59,8 @@ _PRINT_DEBUG_LOGS = bool(int(os.getenv("PRINT_DEBUG_LOGS", "0")))
|
|
|
58
59
|
_MAX_WAIT_TIME = int(os.getenv("MAX_WAIT_TIME", "120"))
|
|
59
60
|
_FORCE_DOWNLOAD_TIME = int(os.getenv("FORCE_DOWNLOAD_TIME", "30"))
|
|
60
61
|
_LITDATA_DISABLE_VERSION_CHECK = int(os.getenv("LITDATA_DISABLE_VERSION_CHECK", "0"))
|
|
62
|
+
# Experimental async chunk prefetch: set env ``LITDATA_ASYNC_CHUNK_PREFETCH=1``
|
|
63
|
+
# (see ``litdata.streaming.async_prefetch``). Off by default.
|
|
61
64
|
|
|
62
65
|
# DON'T CHANGE ORDER
|
|
63
66
|
_TORCH_DTYPES_MAPPING = {
|
|
@@ -160,7 +160,7 @@ def _download_data_target(
|
|
|
160
160
|
if input_dir.path:
|
|
161
161
|
local_path = path.replace(input_dir.path, cache_dir)
|
|
162
162
|
|
|
163
|
-
if input_dir.url and input_dir.path:
|
|
163
|
+
if input_dir.url and input_dir.path and not os.path.isfile(path):
|
|
164
164
|
path = path.replace(input_dir.path, input_dir.url)
|
|
165
165
|
|
|
166
166
|
obj = parse.urlparse(path)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
"""Experimental asyncio helpers for overlapping **remote chunk downloads**.
|
|
15
|
+
|
|
16
|
+
This is intentionally **not** an async ``StreamingDataLoader``. Training stays on
|
|
17
|
+
the sync ``for batch in loader`` API; decode stays on process workers. Asyncio is
|
|
18
|
+
only useful where we wait on network IO.
|
|
19
|
+
|
|
20
|
+
On by default for remote datasets; force off with ``LITDATA_ASYNC_CHUNK_PREFETCH=0``,
|
|
21
|
+
or force on locally with ``LITDATA_ASYNC_CHUNK_PREFETCH=1``.
|
|
22
|
+
|
|
23
|
+
Strategy:
|
|
24
|
+
* Prefer ``Downloader.adownload_file`` (streaming to disk) when overridden.
|
|
25
|
+
* Else ``Downloader.adownload_fileobj`` + atomic write.
|
|
26
|
+
* Otherwise run sync ``download_chunk_from_index`` in ``asyncio.to_thread`` and
|
|
27
|
+
``gather`` several chunk indexes — still overlaps latency for blocking cloud SDKs.
|
|
28
|
+
|
|
29
|
+
Real-S3 note (Studio benches on ~67MB ImageNet chunks): gather concurrency needs
|
|
30
|
+
``max_pre_download >= 4`` to matter; the prepare thread raises the floor when
|
|
31
|
+
async prefetch is enabled (see :func:`async_prefetch_min_pre_download`).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import asyncio
|
|
37
|
+
import contextlib
|
|
38
|
+
import logging
|
|
39
|
+
import os
|
|
40
|
+
import threading
|
|
41
|
+
from typing import TYPE_CHECKING
|
|
42
|
+
|
|
43
|
+
from litdata.streaming.downloader import Downloader
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from litdata.streaming.config import ChunksConfig
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger("litdata.streaming.async_prefetch")
|
|
49
|
+
|
|
50
|
+
# Thread-local event loop so PrepareChunksThread does not pay asyncio.run()
|
|
51
|
+
# startup on every drain batch.
|
|
52
|
+
_THREAD_LOOPS = threading.local()
|
|
53
|
+
|
|
54
|
+
# Empirically, async gather on real S3 is bottlenecked when max_pre_download==2
|
|
55
|
+
# (only 1–2 in-flight). Floor to 4 when the feature is enabled unless overridden.
|
|
56
|
+
_DEFAULT_ASYNC_MIN_PRE_DOWNLOAD = 4
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def async_chunk_prefetch_enabled(remote_dir: str | None = None) -> bool:
|
|
60
|
+
"""Return True when async chunk prefetch should run.
|
|
61
|
+
|
|
62
|
+
Env ``LITDATA_ASYNC_CHUNK_PREFETCH`` wins when set (``1``/``0``). When unset,
|
|
63
|
+
default **on** for remote datasets (real-S3: process+async is the fast path)
|
|
64
|
+
and **off** for local-only caches.
|
|
65
|
+
"""
|
|
66
|
+
raw = os.getenv("LITDATA_ASYNC_CHUNK_PREFETCH")
|
|
67
|
+
if raw is not None:
|
|
68
|
+
return bool(int(raw))
|
|
69
|
+
return bool(remote_dir)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def async_prefetch_min_pre_download() -> int:
|
|
73
|
+
"""Minimum ``max_pre_download`` applied when async chunk prefetch is on.
|
|
74
|
+
|
|
75
|
+
Override with ``LITDATA_ASYNC_MIN_PRE_DOWNLOAD`` (default 4). Set to ``0`` to
|
|
76
|
+
disable the floor and keep the caller's ``max_pre_download`` unchanged.
|
|
77
|
+
"""
|
|
78
|
+
raw = os.getenv("LITDATA_ASYNC_MIN_PRE_DOWNLOAD")
|
|
79
|
+
if raw is None:
|
|
80
|
+
return _DEFAULT_ASYNC_MIN_PRE_DOWNLOAD
|
|
81
|
+
return max(0, int(raw))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def apply_async_pre_download_floor(max_pre_download: int, remote_dir: str | None = None) -> int:
|
|
85
|
+
"""Raise ``max_pre_download`` when async prefetch needs gather width."""
|
|
86
|
+
if not async_chunk_prefetch_enabled(remote_dir):
|
|
87
|
+
return max_pre_download
|
|
88
|
+
floor = async_prefetch_min_pre_download()
|
|
89
|
+
if floor <= 0 or max_pre_download >= floor:
|
|
90
|
+
return max_pre_download
|
|
91
|
+
logger.info(
|
|
92
|
+
"Async chunk prefetch: raising max_pre_download %s → %s so "
|
|
93
|
+
"asyncio.gather can overlap remote chunk downloads. "
|
|
94
|
+
"Set LITDATA_ASYNC_MIN_PRE_DOWNLOAD=0 to keep the original value.",
|
|
95
|
+
max_pre_download,
|
|
96
|
+
floor,
|
|
97
|
+
)
|
|
98
|
+
return floor
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def downloader_supports_adownload(downloader: Downloader | None) -> bool:
|
|
102
|
+
"""True when an async download path is available on ``downloader``."""
|
|
103
|
+
if downloader is None:
|
|
104
|
+
return False
|
|
105
|
+
cls = type(downloader)
|
|
106
|
+
return (
|
|
107
|
+
cls.adownload_file is not Downloader.adownload_file or cls.adownload_fileobj is not Downloader.adownload_fileobj
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _remote_join(remote_dir: str, filename: str) -> str:
|
|
112
|
+
"""Join cloud URLs without ``os.path.join`` quirks on schemes."""
|
|
113
|
+
return remote_dir.rstrip("/") + "/" + filename.lstrip("/")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _adownload_file_to_path(downloader: Downloader, remote_filepath: str, local_filepath: str) -> None:
|
|
117
|
+
"""Fetch ``remote_filepath`` asynchronously and publish atomically."""
|
|
118
|
+
if os.path.exists(local_filepath):
|
|
119
|
+
return
|
|
120
|
+
# Prefer streaming-to-disk when the backend overrides adownload_file.
|
|
121
|
+
if type(downloader).adownload_file is not Downloader.adownload_file:
|
|
122
|
+
await downloader.adownload_file(remote_filepath, local_filepath)
|
|
123
|
+
return
|
|
124
|
+
data = await downloader.adownload_fileobj(remote_filepath)
|
|
125
|
+
if data is None:
|
|
126
|
+
raise NotImplementedError(
|
|
127
|
+
f"{type(downloader).__name__}.adownload_fileobj returned None; "
|
|
128
|
+
"cannot use async chunk prefetch for this backend."
|
|
129
|
+
)
|
|
130
|
+
tmp_path = downloader._temp_download_path(local_filepath)
|
|
131
|
+
try:
|
|
132
|
+
os.makedirs(os.path.dirname(local_filepath) or ".", exist_ok=True)
|
|
133
|
+
with open(tmp_path, "wb") as f:
|
|
134
|
+
f.write(data)
|
|
135
|
+
downloader._atomic_replace(tmp_path, local_filepath)
|
|
136
|
+
except Exception:
|
|
137
|
+
with contextlib.suppress(FileNotFoundError, PermissionError):
|
|
138
|
+
os.remove(tmp_path)
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def _adownload_chunk_index(config: ChunksConfig, chunk_index: int) -> None:
|
|
143
|
+
"""Async download + decompress for one chunk index (mirrors sync config path)."""
|
|
144
|
+
assert config._chunks is not None
|
|
145
|
+
downloader = config._downloader
|
|
146
|
+
if downloader is None:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
chunk_filename = config._chunks[chunk_index]["filename"]
|
|
150
|
+
local_chunkpath = os.path.join(config._cache_dir, chunk_filename)
|
|
151
|
+
remote_chunkpath = _remote_join(downloader._remote_dir, chunk_filename)
|
|
152
|
+
lazily_ref_counted = chunk_index not in config._shared_chunk_indexes
|
|
153
|
+
lock_path = (
|
|
154
|
+
local_chunkpath.replace(f".{config._compressor_name}", "") if config._compressor_name else local_chunkpath
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if os.path.exists(local_chunkpath):
|
|
158
|
+
config.try_decompress(local_chunkpath)
|
|
159
|
+
if lazily_ref_counted:
|
|
160
|
+
downloader._increment_local_lock(lock_path, chunk_index)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if lazily_ref_counted:
|
|
164
|
+
downloader._increment_local_lock(lock_path, chunk_index)
|
|
165
|
+
|
|
166
|
+
if downloader_supports_adownload(downloader):
|
|
167
|
+
await _adownload_file_to_path(downloader, remote_chunkpath, local_chunkpath)
|
|
168
|
+
else:
|
|
169
|
+
# Overlap blocking SDK calls across threads when native async is unavailable.
|
|
170
|
+
await asyncio.to_thread(downloader.download_chunk_from_index, chunk_index)
|
|
171
|
+
|
|
172
|
+
config.try_decompress(local_chunkpath)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def adownload_chunk_indexes(config: ChunksConfig, chunk_indexes: list[int]) -> None:
|
|
176
|
+
"""Download several chunk indexes concurrently (gather)."""
|
|
177
|
+
if not chunk_indexes:
|
|
178
|
+
return
|
|
179
|
+
if len(chunk_indexes) == 1:
|
|
180
|
+
await _adownload_chunk_index(config, chunk_indexes[0])
|
|
181
|
+
return
|
|
182
|
+
await asyncio.gather(*[_adownload_chunk_index(config, idx) for idx in chunk_indexes])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _thread_event_loop() -> asyncio.AbstractEventLoop:
|
|
186
|
+
"""Return a reusable event loop for the current thread."""
|
|
187
|
+
loop = getattr(_THREAD_LOOPS, "loop", None)
|
|
188
|
+
if loop is None or loop.is_closed():
|
|
189
|
+
loop = asyncio.new_event_loop()
|
|
190
|
+
_THREAD_LOOPS.loop = loop
|
|
191
|
+
return loop
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def close_thread_event_loop() -> None:
|
|
195
|
+
"""Shut down the thread-local loop and its default executor.
|
|
196
|
+
|
|
197
|
+
``asyncio.to_thread`` (used when a downloader has no native async path) parks
|
|
198
|
+
workers named ``asyncio_N`` on the loop's default executor. Leaving the loop
|
|
199
|
+
open leaks those threads and trips the session thread-police on Windows.
|
|
200
|
+
|
|
201
|
+
Executor shutdown is non-blocking (``wait=False``) so prepare-thread ``finally``
|
|
202
|
+
never joins forever if a download worker is stuck. Threads are marked daemon and
|
|
203
|
+
futures cancelled so orphans cannot keep the process alive or poison later
|
|
204
|
+
DataLoader forks under pytest-xdist.
|
|
205
|
+
"""
|
|
206
|
+
loop = getattr(_THREAD_LOOPS, "loop", None)
|
|
207
|
+
_THREAD_LOOPS.loop = None
|
|
208
|
+
if loop is None or loop.is_closed():
|
|
209
|
+
return
|
|
210
|
+
try:
|
|
211
|
+
if not loop.is_running():
|
|
212
|
+
with contextlib.suppress(Exception):
|
|
213
|
+
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
214
|
+
# Prefer non-blocking teardown over ``shutdown_default_executor()``,
|
|
215
|
+
# which joins executor threads and can hang prepare-thread exit.
|
|
216
|
+
executor = getattr(loop, "_default_executor", None)
|
|
217
|
+
if executor is not None:
|
|
218
|
+
with contextlib.suppress(Exception):
|
|
219
|
+
loop._default_executor = None
|
|
220
|
+
for thread in getattr(executor, "_threads", set()):
|
|
221
|
+
thread.daemon = True
|
|
222
|
+
# cancel_futures is 3.9+; litdata already requires newer Python.
|
|
223
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
224
|
+
finally:
|
|
225
|
+
with contextlib.suppress(Exception):
|
|
226
|
+
loop.close()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def download_chunk_indexes_concurrently(config: ChunksConfig, chunk_indexes: list[int]) -> None:
|
|
230
|
+
"""Sync entry point for ``PrepareChunksThread``: run ``adownload_chunk_indexes``."""
|
|
231
|
+
if not chunk_indexes:
|
|
232
|
+
return
|
|
233
|
+
if len(chunk_indexes) == 1:
|
|
234
|
+
config.download_chunk_from_index(chunk_indexes[0])
|
|
235
|
+
return
|
|
236
|
+
# Reuse a per-thread loop instead of asyncio.run() (which creates+closes a loop
|
|
237
|
+
# on every prefetch batch — measurable overhead under high chunk churn).
|
|
238
|
+
try:
|
|
239
|
+
running = asyncio.get_running_loop()
|
|
240
|
+
except RuntimeError:
|
|
241
|
+
running = None
|
|
242
|
+
if running is not None:
|
|
243
|
+
# Already inside an event loop (tests / nested callers): block via a bridge.
|
|
244
|
+
import concurrent.futures
|
|
245
|
+
|
|
246
|
+
def _run() -> None:
|
|
247
|
+
try:
|
|
248
|
+
_thread_event_loop().run_until_complete(adownload_chunk_indexes(config, chunk_indexes))
|
|
249
|
+
finally:
|
|
250
|
+
close_thread_event_loop()
|
|
251
|
+
|
|
252
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
253
|
+
pool.submit(_run).result()
|
|
254
|
+
return
|
|
255
|
+
loop = _thread_event_loop()
|
|
256
|
+
loop.run_until_complete(adownload_chunk_indexes(config, chunk_indexes))
|
|
@@ -45,6 +45,63 @@ class _CustomRetryAdapter(HTTPAdapter):
|
|
|
45
45
|
return super().send(request, **kwargs)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _login_and_get_temp_bucket_credentials(data_connection_id: str) -> dict[str, Any]:
|
|
49
|
+
"""Mint temporary bucket credentials for a data connection via the Lightning Cloud API.
|
|
50
|
+
|
|
51
|
+
Shared by R2 (lightning storage) connections and by S3 connections marked
|
|
52
|
+
``available_in_non_aws_providers``: both bypass the FUSE mount and need short-lived creds
|
|
53
|
+
from the same control-plane ``temp-bucket-credentials`` endpoint. Returns the raw response
|
|
54
|
+
(accessKeyId/secretAccessKey/sessionToken, plus accountId for R2).
|
|
55
|
+
"""
|
|
56
|
+
retry_strategy = Retry(
|
|
57
|
+
total=_CONNECTION_RETRY_TOTAL,
|
|
58
|
+
backoff_factor=_CONNECTION_RETRY_BACKOFF_FACTOR,
|
|
59
|
+
status_forcelist=[
|
|
60
|
+
408, # Request Timeout
|
|
61
|
+
429, # Too Many Requests
|
|
62
|
+
500, # Internal Server Error
|
|
63
|
+
502, # Bad Gateway
|
|
64
|
+
503, # Service Unavailable
|
|
65
|
+
504, # Gateway Timeout
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
adapter = _CustomRetryAdapter(max_retries=retry_strategy, timeout=_DEFAULT_REQUEST_TIMEOUT)
|
|
69
|
+
session = requests.Session()
|
|
70
|
+
session.mount("http://", adapter)
|
|
71
|
+
session.mount("https://", adapter)
|
|
72
|
+
|
|
73
|
+
cloud_url = os.getenv("LIGHTNING_CLOUD_URL", "https://lightning.ai")
|
|
74
|
+
api_key = os.getenv("LIGHTNING_API_KEY")
|
|
75
|
+
username = os.getenv("LIGHTNING_USERNAME")
|
|
76
|
+
project_id = os.getenv("LIGHTNING_CLOUD_PROJECT_ID")
|
|
77
|
+
|
|
78
|
+
if not all([api_key, username, project_id]):
|
|
79
|
+
raise RuntimeError("Missing required environment variables")
|
|
80
|
+
|
|
81
|
+
# Login to get token
|
|
82
|
+
payload = {"apiKey": api_key, "username": username}
|
|
83
|
+
login_url = f"{cloud_url}/v1/auth/login"
|
|
84
|
+
response = session.post(login_url, data=json.dumps(payload))
|
|
85
|
+
|
|
86
|
+
if "token" not in response.json():
|
|
87
|
+
raise RuntimeError("Failed to get authentication token")
|
|
88
|
+
|
|
89
|
+
token = response.json()["token"]
|
|
90
|
+
|
|
91
|
+
# Get temporary bucket credentials
|
|
92
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
93
|
+
credentials_url = (
|
|
94
|
+
f"{cloud_url}/v1/projects/{project_id}/data-connections/{data_connection_id}/temp-bucket-credentials"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
credentials_response = session.get(credentials_url, headers=headers, timeout=10)
|
|
98
|
+
|
|
99
|
+
if credentials_response.status_code != 200:
|
|
100
|
+
raise RuntimeError(f"Failed to get credentials: {credentials_response.status_code}")
|
|
101
|
+
|
|
102
|
+
return credentials_response.json()
|
|
103
|
+
|
|
104
|
+
|
|
48
105
|
class S3Client:
|
|
49
106
|
# TODO: Generalize to support more cloud providers.
|
|
50
107
|
|
|
@@ -61,6 +118,15 @@ class S3Client:
|
|
|
61
118
|
self._session_options: dict = session_options or {}
|
|
62
119
|
|
|
63
120
|
def _create_client(self) -> None:
|
|
121
|
+
# S3 data connections marked available on non-AWS providers can't reach the bucket via the
|
|
122
|
+
# FUSE mount or an instance profile off AWS, so mint temporary project-role credentials from
|
|
123
|
+
# the control plane instead (mirrors R2Client). Gated on data_connection_id being threaded
|
|
124
|
+
# through storage_options, so plain S3 access is unaffected.
|
|
125
|
+
data_connection_id = self._storage_options.get("data_connection_id")
|
|
126
|
+
if data_connection_id:
|
|
127
|
+
self._create_client_from_temp_credentials(data_connection_id)
|
|
128
|
+
return
|
|
129
|
+
|
|
64
130
|
has_shared_credentials_file = (
|
|
65
131
|
os.getenv("AWS_SHARED_CREDENTIALS_FILE") == os.getenv("AWS_CONFIG_FILE") == "/.credentials/.aws_credentials"
|
|
66
132
|
)
|
|
@@ -86,6 +152,29 @@ class S3Client:
|
|
|
86
152
|
config=botocore.config.Config(retries={"max_attempts": 1000, "mode": "adaptive"}),
|
|
87
153
|
)
|
|
88
154
|
|
|
155
|
+
def _create_client_from_temp_credentials(self, data_connection_id: str) -> None:
|
|
156
|
+
"""Create an S3 client backed by temporary project-role credentials for a data connection.
|
|
157
|
+
|
|
158
|
+
Used for S3 connections available on non-AWS providers. Unlike R2 there is no custom
|
|
159
|
+
endpoint — this is real AWS S3, so botocore resolves the bucket region on first use.
|
|
160
|
+
"""
|
|
161
|
+
temp_credentials = _login_and_get_temp_bucket_credentials(data_connection_id)
|
|
162
|
+
|
|
163
|
+
# data_connection_id is our own metadata; drop it before handing options to boto3.
|
|
164
|
+
storage_options = {k: v for k, v in self._storage_options.items() if k != "data_connection_id"}
|
|
165
|
+
|
|
166
|
+
session = boto3.Session(**self._session_options)
|
|
167
|
+
self._client = session.client(
|
|
168
|
+
"s3",
|
|
169
|
+
**{
|
|
170
|
+
"aws_access_key_id": temp_credentials["accessKeyId"],
|
|
171
|
+
"aws_secret_access_key": temp_credentials["secretAccessKey"],
|
|
172
|
+
"aws_session_token": temp_credentials["sessionToken"],
|
|
173
|
+
"config": botocore.config.Config(retries={"max_attempts": 1000, "mode": "adaptive"}),
|
|
174
|
+
**storage_options,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
89
178
|
@property
|
|
90
179
|
def client(self) -> Any:
|
|
91
180
|
if self._client is None:
|
|
@@ -121,56 +210,8 @@ class R2Client(S3Client):
|
|
|
121
210
|
|
|
122
211
|
def get_r2_bucket_credentials(self, data_connection_id: str) -> dict[str, str]:
|
|
123
212
|
"""Fetch temporary R2 credentials for the current lightning storage connection."""
|
|
124
|
-
# Create session with retry logic
|
|
125
|
-
retry_strategy = Retry(
|
|
126
|
-
total=_CONNECTION_RETRY_TOTAL,
|
|
127
|
-
backoff_factor=_CONNECTION_RETRY_BACKOFF_FACTOR,
|
|
128
|
-
status_forcelist=[
|
|
129
|
-
408, # Request Timeout
|
|
130
|
-
429, # Too Many Requests
|
|
131
|
-
500, # Internal Server Error
|
|
132
|
-
502, # Bad Gateway
|
|
133
|
-
503, # Service Unavailable
|
|
134
|
-
504, # Gateway Timeout
|
|
135
|
-
],
|
|
136
|
-
)
|
|
137
|
-
adapter = _CustomRetryAdapter(max_retries=retry_strategy, timeout=_DEFAULT_REQUEST_TIMEOUT)
|
|
138
|
-
session = requests.Session()
|
|
139
|
-
session.mount("http://", adapter)
|
|
140
|
-
session.mount("https://", adapter)
|
|
141
|
-
|
|
142
213
|
try:
|
|
143
|
-
|
|
144
|
-
cloud_url = os.getenv("LIGHTNING_CLOUD_URL", "https://lightning.ai")
|
|
145
|
-
api_key = os.getenv("LIGHTNING_API_KEY")
|
|
146
|
-
username = os.getenv("LIGHTNING_USERNAME")
|
|
147
|
-
project_id = os.getenv("LIGHTNING_CLOUD_PROJECT_ID")
|
|
148
|
-
|
|
149
|
-
if not all([api_key, username, project_id]):
|
|
150
|
-
raise RuntimeError("Missing required environment variables")
|
|
151
|
-
|
|
152
|
-
# Login to get token
|
|
153
|
-
payload = {"apiKey": api_key, "username": username}
|
|
154
|
-
login_url = f"{cloud_url}/v1/auth/login"
|
|
155
|
-
response = session.post(login_url, data=json.dumps(payload))
|
|
156
|
-
|
|
157
|
-
if "token" not in response.json():
|
|
158
|
-
raise RuntimeError("Failed to get authentication token")
|
|
159
|
-
|
|
160
|
-
token = response.json()["token"]
|
|
161
|
-
|
|
162
|
-
# Get temporary bucket credentials
|
|
163
|
-
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
164
|
-
credentials_url = (
|
|
165
|
-
f"{cloud_url}/v1/projects/{project_id}/data-connections/{data_connection_id}/temp-bucket-credentials"
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
credentials_response = session.get(credentials_url, headers=headers, timeout=10)
|
|
169
|
-
|
|
170
|
-
if credentials_response.status_code != 200:
|
|
171
|
-
raise RuntimeError(f"Failed to get credentials: {credentials_response.status_code}")
|
|
172
|
-
|
|
173
|
-
temp_credentials = credentials_response.json()
|
|
214
|
+
temp_credentials = _login_and_get_temp_bucket_credentials(data_connection_id)
|
|
174
215
|
|
|
175
216
|
endpoint_url = f"https://{temp_credentials['accountId']}.r2.cloudflarestorage.com"
|
|
176
217
|
|