litdata 0.2.71__tar.gz → 0.2.73__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.71/src/litdata.egg-info → litdata-0.2.73}/PKG-INFO +2 -1
- {litdata-0.2.71 → litdata-0.2.73}/requirements.txt +1 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/__about__.py +1 -1
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/processing/data_processor.py +13 -2
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/processing/functions.py +22 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/client.py +190 -35
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/dataloader.py +10 -10
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/serializers.py +2 -2
- {litdata-0.2.71 → litdata-0.2.73/src/litdata.egg-info}/PKG-INFO +2 -1
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata.egg-info/requires.txt +1 -0
- {litdata-0.2.71 → litdata-0.2.73}/CONTRIBUTING.md +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/LICENSE +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/MANIFEST.in +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/README.md +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/setup.cfg +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/setup.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/__init__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/__main__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/cli/__init__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/cli/commands.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/cli/handler/__init__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/cli/handler/cache.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/cli/handler/optimize.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/cli/parser.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/constants.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/debugger.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/exceptions.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/helpers.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/imports.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/processing/__init__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/processing/complete.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/processing/media_folder.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/processing/readers.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/processing/utilities.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/raw/__init__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/raw/dataset.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/raw/indexer.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/raw/types.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/requirements.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/__init__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/async_prefetch.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/cache.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/collate.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/combined.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/compression.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/config.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/dataset.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/dataset_update.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/downloader.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/elastic.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/fs_provider.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/item_loader.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/parallel.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/posix_fast.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/reader.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/resolver.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/sampler.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/shuffle.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/timing.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/streaming/writer.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/types.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/__init__.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/_pytree.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/base.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/breakpoint.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/broadcast.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/dataset_utilities.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/encryption.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/env.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/format.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/hf_dataset.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/keys_index.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/packing.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/parquet.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/shuffle.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/subsample.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/torch_utils.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata/utilities/train_test_split.py +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata.egg-info/SOURCES.txt +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata.egg-info/dependency_links.txt +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata.egg-info/entry_points.txt +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/src/litdata.egg-info/not-zip-safe +0 -0
- {litdata-0.2.71 → litdata-0.2.73}/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.73
|
|
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
|
|
@@ -34,6 +34,7 @@ Requires-Dist: filelock
|
|
|
34
34
|
Requires-Dist: numpy
|
|
35
35
|
Requires-Dist: boto3
|
|
36
36
|
Requires-Dist: requests
|
|
37
|
+
Requires-Dist: urllib3>=1.26
|
|
37
38
|
Requires-Dist: tifffile
|
|
38
39
|
Requires-Dist: obstore
|
|
39
40
|
Provides-Extra: extras
|
|
@@ -1523,7 +1523,16 @@ class DataChunkRecipe(DataRecipe):
|
|
|
1523
1523
|
|
|
1524
1524
|
merge_cache = Cache(cache_dir, chunk_bytes=1)
|
|
1525
1525
|
node_rank = _get_node_rank()
|
|
1526
|
-
|
|
1526
|
+
# With more than one node, every node would otherwise fold the existing
|
|
1527
|
+
# index into its own {node_rank}-index.json, and the final cross-node
|
|
1528
|
+
# merge below then repeats those chunks once per node. Add the existing
|
|
1529
|
+
# index only once, either here for the single-node case or in the final
|
|
1530
|
+
# merge for the multi-node case.
|
|
1531
|
+
existing_index = getattr(self, "existing_index", None)
|
|
1532
|
+
merge_cache._merge_no_wait(
|
|
1533
|
+
node_rank if num_nodes > 1 else None,
|
|
1534
|
+
existing_index if num_nodes == 1 else None,
|
|
1535
|
+
)
|
|
1527
1536
|
|
|
1528
1537
|
self._merge_and_upload_keys(output_dir, cache_dir, num_nodes, node_rank)
|
|
1529
1538
|
self._upload_index(output_dir, cache_dir, num_nodes, node_rank)
|
|
@@ -1734,7 +1743,9 @@ class DataChunkRecipe(DataRecipe):
|
|
|
1734
1743
|
shutil.copyfile(remote_filepath, node_index_filepath)
|
|
1735
1744
|
|
|
1736
1745
|
merge_cache = Cache(merge_dir, chunk_bytes=1)
|
|
1737
|
-
|
|
1746
|
+
# The per-node index files hold only their own new chunks now, so the
|
|
1747
|
+
# existing index is folded in here, once, as the node files are merged.
|
|
1748
|
+
merge_cache._merge_no_wait(existing_index=getattr(self, "existing_index", None))
|
|
1738
1749
|
self._upload_index(output_dir, merge_dir, 1, None)
|
|
1739
1750
|
|
|
1740
1751
|
|
|
@@ -147,6 +147,17 @@ class LambdaMapRecipe(MapRecipe):
|
|
|
147
147
|
self._contains_device = "device" in params
|
|
148
148
|
self._contains_is_last = "is_last" in params
|
|
149
149
|
|
|
150
|
+
def __getstate__(self) -> dict[str, Any]:
|
|
151
|
+
"""Drop the full input sequence when pickling into spawn workers.
|
|
152
|
+
|
|
153
|
+
The parent already shards items onto per-worker queues. Workers only need
|
|
154
|
+
``prepare_item``; keeping ``_inputs`` in the pickle would duplicate the list
|
|
155
|
+
once per process.
|
|
156
|
+
"""
|
|
157
|
+
state = self.__dict__.copy()
|
|
158
|
+
state["_inputs"] = None
|
|
159
|
+
return state
|
|
160
|
+
|
|
150
161
|
def prepare_structure(self, _: str | None) -> Any:
|
|
151
162
|
return self._inputs
|
|
152
163
|
|
|
@@ -210,6 +221,17 @@ class LambdaDataChunkRecipe(DataChunkRecipe):
|
|
|
210
221
|
|
|
211
222
|
self.prepare_item = self._prepare_item_generator if self.is_generator else self._prepare_item # type: ignore
|
|
212
223
|
|
|
224
|
+
def __getstate__(self) -> dict[str, Any]:
|
|
225
|
+
"""Drop the full input sequence when pickling into spawn workers.
|
|
226
|
+
|
|
227
|
+
The parent already shards items onto per-worker queues. Workers only need
|
|
228
|
+
``prepare_item``; keeping ``_inputs`` in the pickle would duplicate the list
|
|
229
|
+
once per process.
|
|
230
|
+
"""
|
|
231
|
+
state = self.__dict__.copy()
|
|
232
|
+
state["_inputs"] = None
|
|
233
|
+
return state
|
|
234
|
+
|
|
213
235
|
def check_fn(self) -> None:
|
|
214
236
|
if (
|
|
215
237
|
isinstance(self._fn, (partial, FunctionType))
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
|
+
import logging
|
|
15
16
|
import os
|
|
17
|
+
import random
|
|
16
18
|
import threading
|
|
17
|
-
from time import time
|
|
19
|
+
from time import sleep, time
|
|
18
20
|
from typing import Any
|
|
19
21
|
|
|
20
22
|
import boto3
|
|
@@ -27,14 +29,60 @@ from urllib3.util.retry import Retry
|
|
|
27
29
|
|
|
28
30
|
from litdata.constants import _IS_IN_STUDIO
|
|
29
31
|
|
|
32
|
+
logger = logging.getLogger("litdata.streaming.client")
|
|
33
|
+
|
|
30
34
|
# Constants for the retry adapter. Docs: https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html
|
|
31
|
-
#
|
|
32
|
-
|
|
35
|
+
# Retries per request. Deliberately small: a refresh runs inline on whichever DataLoader worker
|
|
36
|
+
# thread crossed the interval and holds the client lock for its whole duration, so it has to
|
|
37
|
+
# return in seconds, not hours. `S3Client._refresh_client` is what rides out a longer outage.
|
|
38
|
+
_CONNECTION_RETRY_TOTAL = 4
|
|
33
39
|
# Backoff factor for connection retries (wait time increases by this factor after each failure)
|
|
34
40
|
_CONNECTION_RETRY_BACKOFF_FACTOR = 0.5
|
|
35
41
|
# Default timeout for each HTTP request in seconds
|
|
36
42
|
_DEFAULT_REQUEST_TIMEOUT = 30 # seconds
|
|
37
43
|
|
|
44
|
+
# The control plane mints credentials with a 1 hour TTL for S3 project-role data connections, and
|
|
45
|
+
# the response carries no expiry for us to read, so refresh well inside it. The remaining time is
|
|
46
|
+
# the window in which a failed refresh can be retried while the credentials in hand still work.
|
|
47
|
+
_DEFAULT_REFETCH_INTERVAL = 2700 # seconds
|
|
48
|
+
# How long past the refetch interval we keep serving existing credentials while refreshes fail.
|
|
49
|
+
# Sized against the TTL, not comfort: 2700 + 600 leaves ~5 minutes before the 1 hour S3 expiry,
|
|
50
|
+
# so we stop before reads start failing as unexplained S3 403s.
|
|
51
|
+
_REFRESH_GRACE_PERIOD = 600 # seconds
|
|
52
|
+
# How long to wait for the control plane when there are no credentials yet. No TTL constrains
|
|
53
|
+
# this one — nothing is being served — so it can be more patient than the refresh grace.
|
|
54
|
+
_INITIAL_RETRY_BUDGET = 900 # seconds
|
|
55
|
+
# Spacing between refresh attempts once one has failed. Without it every subsequent read
|
|
56
|
+
# re-requests credentials, turning one control-plane blip into a request storm per worker.
|
|
57
|
+
_REFRESH_RETRY_INTERVAL = 60 # seconds
|
|
58
|
+
# Fraction of the interval by which each process refreshes early. DataLoader workers are forked
|
|
59
|
+
# together, so without jitter they all reach the interval — and stampede — in the same instant.
|
|
60
|
+
_REFETCH_JITTER_RATIO = 0.1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _CredentialsError(RuntimeError):
|
|
64
|
+
"""Raised when credentials could not be obtained from the control plane or IMDS.
|
|
65
|
+
|
|
66
|
+
Only these are retried. Anything else out of ``_create_client`` — a bad ``storage_options``
|
|
67
|
+
key, a malformed endpoint — is a local mistake that no amount of waiting fixes, and must
|
|
68
|
+
reach the caller immediately rather than stalling the job for the retry budget.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _CredentialsUnavailableError(_CredentialsError):
|
|
73
|
+
"""A credential failure that may clear on its own: unreachable, timed out, 5xx, 429."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _CredentialsConfigurationError(_CredentialsError):
|
|
77
|
+
"""A credential failure that retrying cannot fix: missing configuration, or rejected auth."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _credentials_error(status_code: int, message: str) -> _CredentialsError:
|
|
81
|
+
"""Classify an HTTP failure. A 4xx other than 408/429 will not fix itself on a retry."""
|
|
82
|
+
if 400 <= status_code < 500 and status_code not in (408, 429):
|
|
83
|
+
return _CredentialsConfigurationError(message)
|
|
84
|
+
return _CredentialsUnavailableError(message)
|
|
85
|
+
|
|
38
86
|
|
|
39
87
|
class _CustomRetryAdapter(HTTPAdapter):
|
|
40
88
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -42,7 +90,11 @@ class _CustomRetryAdapter(HTTPAdapter):
|
|
|
42
90
|
super().__init__(*args, **kwargs)
|
|
43
91
|
|
|
44
92
|
def send(self, request: Any, *args: Any, **kwargs: Any) -> Any:
|
|
45
|
-
|
|
93
|
+
# requests always passes `timeout` explicitly, as None when the caller gave none, so a
|
|
94
|
+
# `kwargs.get("timeout", self.timeout)` default never applied and every request without
|
|
95
|
+
# an explicit timeout could hang forever.
|
|
96
|
+
if kwargs.get("timeout") is None:
|
|
97
|
+
kwargs["timeout"] = self.timeout
|
|
46
98
|
return super().send(request, **kwargs)
|
|
47
99
|
|
|
48
100
|
|
|
@@ -57,6 +109,10 @@ def _login_and_get_temp_bucket_credentials(data_connection_id: str) -> dict[str,
|
|
|
57
109
|
retry_strategy = Retry(
|
|
58
110
|
total=_CONNECTION_RETRY_TOTAL,
|
|
59
111
|
backoff_factor=_CONNECTION_RETRY_BACKOFF_FACTOR,
|
|
112
|
+
# urllib3 leaves POST out of its default allowed_methods, which left the login below
|
|
113
|
+
# unretried: a single 429 or 502 there failed the whole refresh even though the
|
|
114
|
+
# credentials GET would have retried it. Minting a token is safe to repeat.
|
|
115
|
+
allowed_methods=Retry.DEFAULT_ALLOWED_METHODS | {"POST"},
|
|
60
116
|
status_forcelist=[
|
|
61
117
|
408, # Request Timeout
|
|
62
118
|
429, # Too Many Requests
|
|
@@ -77,17 +133,27 @@ def _login_and_get_temp_bucket_credentials(data_connection_id: str) -> dict[str,
|
|
|
77
133
|
project_id = os.getenv("LIGHTNING_CLOUD_PROJECT_ID")
|
|
78
134
|
|
|
79
135
|
if not all([api_key, username, project_id]):
|
|
80
|
-
raise
|
|
136
|
+
raise _CredentialsConfigurationError("Missing required environment variables")
|
|
81
137
|
|
|
82
138
|
# Login to get token
|
|
83
139
|
payload = {"apiKey": api_key, "username": username}
|
|
84
140
|
login_url = f"{cloud_url}/v1/auth/login"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
raise
|
|
141
|
+
try:
|
|
142
|
+
response = session.post(login_url, data=json.dumps(payload))
|
|
143
|
+
except requests.exceptions.RequestException as e:
|
|
144
|
+
raise _CredentialsUnavailableError(f"Could not reach the Lightning Cloud API to log in: {e}") from e
|
|
145
|
+
|
|
146
|
+
# Check the status before the body: a proxy in front of the API answers with HTML, which
|
|
147
|
+
# would otherwise surface as a JSONDecodeError with nothing pointing back at the login call.
|
|
148
|
+
if response.status_code != 200:
|
|
149
|
+
raise _credentials_error(
|
|
150
|
+
response.status_code, f"Failed to log in to the Lightning Cloud API: {response.status_code}"
|
|
151
|
+
)
|
|
89
152
|
|
|
90
|
-
|
|
153
|
+
try:
|
|
154
|
+
token = response.json()["token"]
|
|
155
|
+
except (ValueError, KeyError) as e:
|
|
156
|
+
raise RuntimeError("Failed to get authentication token") from e
|
|
91
157
|
|
|
92
158
|
# Get temporary bucket credentials
|
|
93
159
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
@@ -95,10 +161,15 @@ def _login_and_get_temp_bucket_credentials(data_connection_id: str) -> dict[str,
|
|
|
95
161
|
f"{cloud_url}/v1/projects/{project_id}/data-connections/{data_connection_id}/temp-bucket-credentials"
|
|
96
162
|
)
|
|
97
163
|
|
|
98
|
-
|
|
164
|
+
try:
|
|
165
|
+
credentials_response = session.get(credentials_url, headers=headers, timeout=10)
|
|
166
|
+
except requests.exceptions.RequestException as e:
|
|
167
|
+
raise _CredentialsUnavailableError(f"Could not reach the Lightning Cloud API for credentials: {e}") from e
|
|
99
168
|
|
|
100
169
|
if credentials_response.status_code != 200:
|
|
101
|
-
raise
|
|
170
|
+
raise _credentials_error(
|
|
171
|
+
credentials_response.status_code, f"Failed to get credentials: {credentials_response.status_code}"
|
|
172
|
+
)
|
|
102
173
|
|
|
103
174
|
return credentials_response.json()
|
|
104
175
|
|
|
@@ -108,17 +179,33 @@ class S3Client:
|
|
|
108
179
|
|
|
109
180
|
def __init__(
|
|
110
181
|
self,
|
|
111
|
-
refetch_interval: int =
|
|
182
|
+
refetch_interval: int = _DEFAULT_REFETCH_INTERVAL,
|
|
112
183
|
storage_options: dict | None = {},
|
|
113
184
|
session_options: dict | None = {},
|
|
114
185
|
) -> None:
|
|
115
186
|
self._refetch_interval = refetch_interval
|
|
116
187
|
self._last_time: float | None = None
|
|
117
|
-
self._client: Any | None = None
|
|
118
188
|
self._storage_options: dict = storage_options or {}
|
|
119
189
|
self._session_options: dict = session_options or {}
|
|
190
|
+
self._reset_process_state()
|
|
191
|
+
|
|
192
|
+
def _reset_process_state(self) -> None:
|
|
193
|
+
"""Reset the state that must not be shared across a fork.
|
|
194
|
+
|
|
195
|
+
Re-rolls the refresh jitter too: DataLoader workers inherit the parent's timings, so
|
|
196
|
+
without a fresh roll per process they would all refresh in the same instant.
|
|
197
|
+
"""
|
|
198
|
+
self._client: Any | None = None
|
|
120
199
|
# Guards lazy create + credential refresh (range GETs hit .client from many threads).
|
|
121
200
|
self._client_lock = threading.Lock()
|
|
201
|
+
self._owner_pid = os.getpid()
|
|
202
|
+
self._refetch_deadline = self._jittered_refetch_interval()
|
|
203
|
+
self._refresh_retry_time: float | None = None
|
|
204
|
+
|
|
205
|
+
def _jittered_refetch_interval(self) -> float:
|
|
206
|
+
# Only ever early, never late: callers set the interval as an upper bound on how long
|
|
207
|
+
# a set of credentials is held, and the grace period below is measured against it.
|
|
208
|
+
return self._refetch_interval * (1.0 - random.uniform(0.0, _REFETCH_JITTER_RATIO)) # noqa: S311
|
|
122
209
|
|
|
123
210
|
def __getstate__(self) -> dict[str, Any]:
|
|
124
211
|
state = self.__dict__.copy()
|
|
@@ -130,9 +217,7 @@ class S3Client:
|
|
|
130
217
|
|
|
131
218
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
|
132
219
|
self.__dict__.update(state)
|
|
133
|
-
self.
|
|
134
|
-
self._client_lock = threading.Lock()
|
|
135
|
-
self._owner_pid = os.getpid()
|
|
220
|
+
self._reset_process_state()
|
|
136
221
|
|
|
137
222
|
def _create_client(self) -> None:
|
|
138
223
|
# S3 data connections marked available on non-AWS providers can't reach the bucket via the
|
|
@@ -159,7 +244,10 @@ class S3Client:
|
|
|
159
244
|
)
|
|
160
245
|
else:
|
|
161
246
|
provider = InstanceMetadataProvider(iam_role_fetcher=InstanceMetadataFetcher(timeout=3600, num_attempts=5))
|
|
162
|
-
|
|
247
|
+
try:
|
|
248
|
+
credentials = provider.load()
|
|
249
|
+
except Exception as e:
|
|
250
|
+
raise _CredentialsUnavailableError(f"Could not load instance metadata credentials: {e}") from e
|
|
163
251
|
session = boto3.Session()
|
|
164
252
|
self._client = session.client(
|
|
165
253
|
"s3",
|
|
@@ -192,24 +280,91 @@ class S3Client:
|
|
|
192
280
|
},
|
|
193
281
|
)
|
|
194
282
|
|
|
283
|
+
def _mark_refreshed(self) -> None:
|
|
284
|
+
self._last_time = time()
|
|
285
|
+
self._refetch_deadline = self._jittered_refetch_interval()
|
|
286
|
+
self._refresh_retry_time = None
|
|
287
|
+
|
|
288
|
+
def _create_initial_client(self) -> None:
|
|
289
|
+
"""Create the first client, waiting out a control plane that is briefly unavailable.
|
|
290
|
+
|
|
291
|
+
Unlike a refresh there are no credentials to fall back on, so this has to keep trying
|
|
292
|
+
rather than hand the caller an error. Bounded by ``_INITIAL_RETRY_BUDGET``: a job that
|
|
293
|
+
cannot reach the control plane for that long should fail with a clear reason.
|
|
294
|
+
"""
|
|
295
|
+
started = time()
|
|
296
|
+
attempt = 0
|
|
297
|
+
while True:
|
|
298
|
+
attempt += 1
|
|
299
|
+
try:
|
|
300
|
+
self._create_client()
|
|
301
|
+
# Only a control plane that might come back is worth waiting for. A configuration
|
|
302
|
+
# error, or any local failure building the client, propagates on the first attempt.
|
|
303
|
+
except _CredentialsUnavailableError as e:
|
|
304
|
+
waited = time() - started
|
|
305
|
+
if waited + _REFRESH_RETRY_INTERVAL >= _INITIAL_RETRY_BUDGET:
|
|
306
|
+
raise RuntimeError(f"Could not get credentials after {waited:.0f}s of retrying: {e}") from e
|
|
307
|
+
logger.warning(
|
|
308
|
+
"Could not get credentials (attempt %d, %.0fs elapsed), so data loading is blocked; "
|
|
309
|
+
"retrying in %ds: %s",
|
|
310
|
+
attempt,
|
|
311
|
+
waited,
|
|
312
|
+
_REFRESH_RETRY_INTERVAL,
|
|
313
|
+
e,
|
|
314
|
+
)
|
|
315
|
+
sleep(_REFRESH_RETRY_INTERVAL)
|
|
316
|
+
else:
|
|
317
|
+
self._mark_refreshed()
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
def _refresh_client(self) -> None:
|
|
321
|
+
"""Re-mint credentials, tolerating a control plane that is briefly unavailable.
|
|
322
|
+
|
|
323
|
+
Credentials are refreshed before they expire, so a failed refresh is not immediately
|
|
324
|
+
fatal — the ones already in hand still work. Keep serving those and retry on a timer
|
|
325
|
+
until the grace period runs out, rather than failing the read on the first error, which
|
|
326
|
+
kills the DataLoader worker and with it the run.
|
|
327
|
+
"""
|
|
328
|
+
now = time()
|
|
329
|
+
if self._refresh_retry_time is not None and now < self._refresh_retry_time:
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
self._create_client()
|
|
334
|
+
# Both kinds get the grace period here, unlike initial creation. The credentials in hand
|
|
335
|
+
# still work, so there is nothing to gain by failing fast on a 403 that might be a proxy
|
|
336
|
+
# misbehaving mid-deploy — and if it is a real revocation, the deadline still catches it.
|
|
337
|
+
except _CredentialsError as e:
|
|
338
|
+
held_for = 0.0 if self._last_time is None else now - self._last_time
|
|
339
|
+
if held_for > self._refetch_deadline + _REFRESH_GRACE_PERIOD:
|
|
340
|
+
raise RuntimeError(
|
|
341
|
+
f"Failed to refresh credentials for {held_for:.0f}s, so they are assumed expired: {e}"
|
|
342
|
+
) from e
|
|
343
|
+
self._refresh_retry_time = now + _REFRESH_RETRY_INTERVAL
|
|
344
|
+
logger.warning(
|
|
345
|
+
"Could not refresh credentials (%.0fs since the last successful refresh); reusing the current "
|
|
346
|
+
"ones and retrying in %ds: %s",
|
|
347
|
+
held_for,
|
|
348
|
+
_REFRESH_RETRY_INTERVAL,
|
|
349
|
+
e,
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
self._mark_refreshed()
|
|
354
|
+
|
|
195
355
|
@property
|
|
196
356
|
def client(self) -> Any:
|
|
197
357
|
# boto3 clients are thread-safe for requests; construction/refresh is not.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
self._client_lock = threading.Lock()
|
|
203
|
-
self._owner_pid = pid
|
|
358
|
+
if getattr(self, "_owner_pid", None) != os.getpid():
|
|
359
|
+
# DataLoader fork: drop the inherited client, lock and refresh schedule.
|
|
360
|
+
self._reset_process_state()
|
|
361
|
+
|
|
204
362
|
with self._client_lock:
|
|
205
363
|
if self._client is None:
|
|
206
|
-
self.
|
|
207
|
-
self._last_time = time()
|
|
208
|
-
|
|
364
|
+
self._create_initial_client()
|
|
209
365
|
# Re-generate credentials for EC2 / temporary Studio creds
|
|
210
|
-
|
|
211
|
-
self.
|
|
212
|
-
self._last_time = time()
|
|
366
|
+
elif self._last_time is None or (time() - self._last_time) > self._refetch_deadline:
|
|
367
|
+
self._refresh_client()
|
|
213
368
|
|
|
214
369
|
return self._client
|
|
215
370
|
|
|
@@ -219,7 +374,7 @@ class R2Client(S3Client):
|
|
|
219
374
|
|
|
220
375
|
def __init__(
|
|
221
376
|
self,
|
|
222
|
-
refetch_interval: int =
|
|
377
|
+
refetch_interval: int = _DEFAULT_REFETCH_INTERVAL,
|
|
223
378
|
storage_options: dict | None = {},
|
|
224
379
|
session_options: dict | None = {},
|
|
225
380
|
) -> None:
|
|
@@ -248,17 +403,17 @@ class R2Client(S3Client):
|
|
|
248
403
|
"endpoint_url": endpoint_url,
|
|
249
404
|
}
|
|
250
405
|
|
|
406
|
+
except _CredentialsError:
|
|
407
|
+
raise
|
|
251
408
|
except Exception as e:
|
|
252
|
-
|
|
253
|
-
print(f"Failed to get R2 credentials from API: {e}. Using fallback credentials.")
|
|
254
|
-
raise RuntimeError(f"Failed to get R2 credentials and no fallback available: {e}")
|
|
409
|
+
raise RuntimeError(f"Failed to get R2 credentials: {e}") from e
|
|
255
410
|
|
|
256
411
|
def _create_client(self) -> None:
|
|
257
412
|
"""Create a new R2 client with fresh credentials."""
|
|
258
413
|
# Get data connection ID from storage options
|
|
259
414
|
data_connection_id = self._base_storage_options.get("data_connection_id")
|
|
260
415
|
if not data_connection_id:
|
|
261
|
-
raise
|
|
416
|
+
raise _CredentialsConfigurationError("data_connection_id is required in storage_options for R2 client")
|
|
262
417
|
|
|
263
418
|
# Get fresh R2 credentials
|
|
264
419
|
r2_credentials = self.get_r2_bucket_credentials(data_connection_id)
|
|
@@ -558,25 +558,30 @@ class _StreamingMultiProcessingDataLoaderIter(_MultiProcessingDataLoaderIter):
|
|
|
558
558
|
)
|
|
559
559
|
self._num_workers = loader.num_workers
|
|
560
560
|
self._cprofile: cProfile.Profile | None = None
|
|
561
|
-
self._orig_worker_loop: Any = None
|
|
562
561
|
|
|
563
562
|
distributed_env = _DistributedEnv.detect()
|
|
564
563
|
profile_cprofile = bool(getattr(self._loader, "_profile_cprofile", False))
|
|
564
|
+
from torch.utils.data._utils import worker
|
|
565
565
|
|
|
566
|
-
|
|
567
|
-
from torch.utils.data._utils import worker
|
|
566
|
+
original_worker_loop: Any = None
|
|
568
567
|
|
|
568
|
+
if distributed_env.global_rank == 0:
|
|
569
569
|
if self._loader._profile_batches and _VIZ_TRACKER_AVAILABLE:
|
|
570
|
+
original_worker_loop = worker._worker_loop
|
|
570
571
|
worker._worker_loop = _ProfileWorkerLoop(
|
|
571
572
|
self._loader._profile_batches, self._loader._profile_skip_batches, self._loader._profile_dir
|
|
572
573
|
)
|
|
573
574
|
elif profile_cprofile:
|
|
574
|
-
|
|
575
|
+
original_worker_loop = worker._worker_loop
|
|
575
576
|
worker._worker_loop = _CProfileWorkerLoop(_cprofile_output_dir(self._loader._profile_dir))
|
|
576
577
|
|
|
577
578
|
# Workers fork/spawn here. Enable the parent profiler only after that so
|
|
578
579
|
# the child does not inherit an active cProfile (one profiler per process).
|
|
579
|
-
|
|
580
|
+
try:
|
|
581
|
+
super().__init__(loader)
|
|
582
|
+
finally:
|
|
583
|
+
if original_worker_loop is not None:
|
|
584
|
+
worker._worker_loop = original_worker_loop
|
|
580
585
|
|
|
581
586
|
if profile_cprofile and distributed_env.global_rank == 0:
|
|
582
587
|
self._cprofile = cProfile.Profile()
|
|
@@ -587,11 +592,6 @@ class _StreamingMultiProcessingDataLoaderIter(_MultiProcessingDataLoaderIter):
|
|
|
587
592
|
stem = os.path.join(_cprofile_output_dir(self._loader._profile_dir), _CPROFILE_MAIN_STEM)
|
|
588
593
|
_dump_cprofile(self._cprofile, stem)
|
|
589
594
|
self._cprofile = None
|
|
590
|
-
if self._orig_worker_loop is not None:
|
|
591
|
-
from torch.utils.data._utils import worker
|
|
592
|
-
|
|
593
|
-
worker._worker_loop = self._orig_worker_loop
|
|
594
|
-
self._orig_worker_loop = None
|
|
595
595
|
super()._shutdown_workers()
|
|
596
596
|
|
|
597
597
|
def _try_put_index(self) -> None:
|
|
@@ -477,7 +477,7 @@ class NumpySerializer(Serializer):
|
|
|
477
477
|
shape.append(np.frombuffer(data[8 + 4 * shape_idx : 8 + 4 * (shape_idx + 1)], np.uint32).item())
|
|
478
478
|
|
|
479
479
|
# deserialize the numpy array bytes
|
|
480
|
-
tensor = np.frombuffer(data[8 + 4 * shape_size : len(data)], dtype=dtype)
|
|
480
|
+
tensor = np.frombuffer(data[8 + 4 * shape_size : len(data)], dtype=dtype).copy()
|
|
481
481
|
if tensor.shape == shape:
|
|
482
482
|
return tensor
|
|
483
483
|
return np.reshape(tensor, shape)
|
|
@@ -503,7 +503,7 @@ class NoHeaderNumpySerializer(Serializer):
|
|
|
503
503
|
|
|
504
504
|
def deserialize(self, data: bytes) -> np.ndarray:
|
|
505
505
|
assert self._dtype
|
|
506
|
-
return np.frombuffer(data, dtype=self._dtype)
|
|
506
|
+
return np.frombuffer(data, dtype=self._dtype).copy()
|
|
507
507
|
|
|
508
508
|
def can_serialize(self, item: np.ndarray) -> bool:
|
|
509
509
|
return isinstance(item, np.ndarray) and len(item.shape) == 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: litdata
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.73
|
|
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
|
|
@@ -34,6 +34,7 @@ Requires-Dist: filelock
|
|
|
34
34
|
Requires-Dist: numpy
|
|
35
35
|
Requires-Dist: boto3
|
|
36
36
|
Requires-Dist: requests
|
|
37
|
+
Requires-Dist: urllib3>=1.26
|
|
37
38
|
Requires-Dist: tifffile
|
|
38
39
|
Requires-Dist: obstore
|
|
39
40
|
Provides-Extra: extras
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|