litdata 0.2.61__tar.gz → 0.2.64__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.
Files changed (72) hide show
  1. {litdata-0.2.61/src/litdata.egg-info → litdata-0.2.64}/PKG-INFO +3 -3
  2. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/__about__.py +1 -1
  3. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/cli/commands.py +1 -1
  4. litdata-0.2.64/src/litdata/cli/handler/__init__.py +21 -0
  5. litdata-0.2.64/src/litdata/cli/handler/cache.py +32 -0
  6. litdata-0.2.64/src/litdata/cli/handler/optimize.py +19 -0
  7. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/constants.py +1 -0
  8. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/processing/data_processor.py +1 -1
  9. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/client.py +90 -49
  10. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/config.py +5 -0
  11. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/dataloader.py +45 -0
  12. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/downloader.py +1 -1
  13. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/fs_provider.py +9 -7
  14. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/item_loader.py +24 -8
  15. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/reader.py +29 -15
  16. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/resolver.py +13 -1
  17. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/serializers.py +32 -0
  18. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/writer.py +5 -1
  19. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/parquet.py +11 -3
  20. {litdata-0.2.61 → litdata-0.2.64/src/litdata.egg-info}/PKG-INFO +3 -3
  21. {litdata-0.2.61 → litdata-0.2.64}/src/litdata.egg-info/SOURCES.txt +3 -0
  22. {litdata-0.2.61 → litdata-0.2.64}/src/litdata.egg-info/requires.txt +2 -2
  23. {litdata-0.2.61 → litdata-0.2.64}/CONTRIBUTING.md +0 -0
  24. {litdata-0.2.61 → litdata-0.2.64}/LICENSE +0 -0
  25. {litdata-0.2.61 → litdata-0.2.64}/MANIFEST.in +0 -0
  26. {litdata-0.2.61 → litdata-0.2.64}/README.md +0 -0
  27. {litdata-0.2.61 → litdata-0.2.64}/requirements.txt +0 -0
  28. {litdata-0.2.61 → litdata-0.2.64}/setup.cfg +0 -0
  29. {litdata-0.2.61 → litdata-0.2.64}/setup.py +0 -0
  30. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/__init__.py +0 -0
  31. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/__main__.py +0 -0
  32. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/cli/__init__.py +0 -0
  33. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/cli/parser.py +0 -0
  34. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/debugger.py +0 -0
  35. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/helpers.py +0 -0
  36. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/imports.py +0 -0
  37. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/processing/__init__.py +0 -0
  38. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/processing/functions.py +0 -0
  39. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/processing/readers.py +0 -0
  40. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/processing/utilities.py +0 -0
  41. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/raw/__init__.py +0 -0
  42. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/raw/dataset.py +0 -0
  43. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/raw/indexer.py +0 -0
  44. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/raw/types.py +0 -0
  45. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/requirements.py +0 -0
  46. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/__init__.py +0 -0
  47. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/cache.py +0 -0
  48. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/combined.py +0 -0
  49. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/compression.py +0 -0
  50. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/dataset.py +0 -0
  51. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/parallel.py +0 -0
  52. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/sampler.py +0 -0
  53. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/streaming/shuffle.py +0 -0
  54. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/__init__.py +0 -0
  55. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/_pytree.py +0 -0
  56. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/base.py +0 -0
  57. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/breakpoint.py +0 -0
  58. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/broadcast.py +0 -0
  59. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/dataset_utilities.py +0 -0
  60. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/encryption.py +0 -0
  61. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/env.py +0 -0
  62. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/format.py +0 -0
  63. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/hf_dataset.py +0 -0
  64. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/packing.py +0 -0
  65. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/shuffle.py +0 -0
  66. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/subsample.py +0 -0
  67. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/torch_utils.py +0 -0
  68. {litdata-0.2.61 → litdata-0.2.64}/src/litdata/utilities/train_test_split.py +0 -0
  69. {litdata-0.2.61 → litdata-0.2.64}/src/litdata.egg-info/dependency_links.txt +0 -0
  70. {litdata-0.2.61 → litdata-0.2.64}/src/litdata.egg-info/entry_points.txt +0 -0
  71. {litdata-0.2.61 → litdata-0.2.64}/src/litdata.egg-info/not-zip-safe +0 -0
  72. {litdata-0.2.61 → litdata-0.2.64}/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.61
3
+ Version: 0.2.64
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==2025.12.17; extra == "extras"
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
@@ -14,7 +14,7 @@
14
14
 
15
15
  import time
16
16
 
17
- __version__ = "0.2.61"
17
+ __version__ = "0.2.64"
18
18
  __author__ = "Lightning AI et al."
19
19
  __author_email__ = "pytorch@lightning.ai"
20
20
  __license__ = "Apache-2.0"
@@ -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
 
@@ -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)
@@ -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
- # Get Lightning Cloud API token
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
 
@@ -302,6 +302,11 @@ class ChunksConfig:
302
302
 
303
303
  return local_chunkpath, begin, filesize_bytes
304
304
 
305
+ def download_filepath(self, chunk_index: int) -> str:
306
+ """The raw on-disk path that the chunk is downloaded to before any decompression."""
307
+ assert self._chunks is not None
308
+ return os.path.join(self._cache_dir, self._chunks[chunk_index]["filename"])
309
+
305
310
  def _get_chunk_index_from_filename(self, chunk_filename: str) -> int:
306
311
  """Retrieves the associated chunk_index for a given chunk filename."""
307
312
  assert self._chunks is not None
@@ -519,6 +519,43 @@ class StreamingDataLoaderCollateFn:
519
519
  return self.collate_fn(items)
520
520
 
521
521
 
522
+ def _is_fork_context(multiprocessing_context: Any) -> bool:
523
+ import multiprocessing as mp
524
+
525
+ if multiprocessing_context is None:
526
+ try:
527
+ return mp.get_start_method(allow_none=False) == "fork"
528
+ except Exception:
529
+ return False
530
+
531
+ if isinstance(multiprocessing_context, str):
532
+ return multiprocessing_context == "fork"
533
+
534
+ if hasattr(multiprocessing_context, "get_start_method"):
535
+ try:
536
+ return multiprocessing_context.get_start_method() == "fork"
537
+ except Exception:
538
+ return False
539
+
540
+ if hasattr(multiprocessing_context, "start_method"):
541
+ return multiprocessing_context.start_method == "fork"
542
+
543
+ return False
544
+
545
+
546
+ def _has_parquet_loader(dataset: Any) -> bool:
547
+ from litdata.streaming.dataset import StreamingDataset
548
+ from litdata.streaming.item_loader import ParquetLoader
549
+
550
+ if isinstance(dataset, StreamingDataset):
551
+ return isinstance(dataset.item_loader, ParquetLoader)
552
+
553
+ if hasattr(dataset, "_datasets") and isinstance(dataset._datasets, (list, tuple)):
554
+ return any(_has_parquet_loader(d) for d in dataset._datasets)
555
+
556
+ return False
557
+
558
+
522
559
  class StreamingDataLoader(DataLoader):
523
560
  r"""The StreamingDataLoader combines a dataset and a sampler, and provides an iterable over the given dataset.
524
561
 
@@ -589,6 +626,14 @@ class StreamingDataLoader(DataLoader):
589
626
  collate_fn: Callable | None = None,
590
627
  **kwargs: Any,
591
628
  ) -> None: # pyright: ignore
629
+ if num_workers > 0 and _is_fork_context(kwargs.get("multiprocessing_context")) and _has_parquet_loader(dataset):
630
+ raise RuntimeError(
631
+ "The `ParquetLoader` uses Polars, which is not compatible with the `fork` multiprocessing context "
632
+ "used by PyTorch's DataLoader on Linux. Using `fork` will cause deadlocks due to Polars' "
633
+ "internal thread pool. Please pass `multiprocessing_context='spawn'` (or 'forkserver') to "
634
+ "`StreamingDataLoader`. Check thread: https://github.com/Lightning-AI/litData/issues/823"
635
+ )
636
+
592
637
  if not isinstance(dataset, (StreamingDataset, _BaseStreamingDatasetWrapper)):
593
638
  raise RuntimeError(
594
639
  "The provided dataset should be either an instance of StreamingDataset, CombinedStreamingDataset or "
@@ -268,7 +268,7 @@ class R2Downloader(Downloader):
268
268
  if obj.scheme != "r2":
269
269
  raise ValueError(f"Expected obj.scheme to be `r2`, instead, got {obj.scheme} for remote={remote_filepath}")
270
270
 
271
- if not hasattr(self, "client"):
271
+ if not hasattr(self, "_client"):
272
272
  self._client = R2Client(storage_options=self._storage_options, session_options=self.session_options)
273
273
 
274
274
  bucket = obj.netloc
@@ -20,7 +20,9 @@ from litdata.streaming.client import R2Client, S3Client
20
20
 
21
21
 
22
22
  class FsProvider(ABC):
23
- def __init__(self, storage_options: dict[str, Any] | None = {}):
23
+ def __init__(self, storage_options: dict[str, Any] | None = None):
24
+ if storage_options is None:
25
+ storage_options = {}
24
26
  self.storage_options = storage_options
25
27
 
26
28
  @abstractmethod
@@ -50,7 +52,7 @@ class FsProvider(ABC):
50
52
 
51
53
 
52
54
  class GCPFsProvider(FsProvider):
53
- def __init__(self, storage_options: dict[str, Any] | None = {}):
55
+ def __init__(self, storage_options: dict[str, Any] | None = None):
54
56
  if not _GOOGLE_STORAGE_AVAILABLE:
55
57
  raise ModuleNotFoundError(str(_GOOGLE_STORAGE_AVAILABLE))
56
58
  from google.cloud import storage
@@ -133,9 +135,9 @@ class GCPFsProvider(FsProvider):
133
135
 
134
136
 
135
137
  class S3FsProvider(FsProvider):
136
- def __init__(self, storage_options: dict[str, Any] | None = {}):
138
+ def __init__(self, storage_options: dict[str, Any] | None = None):
137
139
  super().__init__(storage_options=storage_options)
138
- self.client = S3Client(storage_options=storage_options)
140
+ self.client = S3Client(storage_options=self.storage_options)
139
141
 
140
142
  def upload_file(self, local_path: str, remote_path: str) -> None:
141
143
  bucket_name, blob_path = get_bucket_and_path(remote_path, "s3")
@@ -225,11 +227,11 @@ class S3FsProvider(FsProvider):
225
227
 
226
228
 
227
229
  class R2FsProvider(S3FsProvider):
228
- def __init__(self, storage_options: dict[str, Any] | None = {}):
230
+ def __init__(self, storage_options: dict[str, Any] | None = None):
229
231
  super().__init__(storage_options=storage_options)
230
232
 
231
233
  # Create R2Client with refreshable credentials
232
- self.client = R2Client(storage_options=storage_options)
234
+ self.client = R2Client(storage_options=self.storage_options)
233
235
 
234
236
  def upload_file(self, local_path: str, remote_path: str) -> None:
235
237
  bucket_name, blob_path = get_bucket_and_path(remote_path, "r2")
@@ -324,7 +326,7 @@ def get_bucket_and_path(remote_filepath: str, expected_scheme: str = "s3") -> tu
324
326
  return bucket_name, blob_path
325
327
 
326
328
 
327
- def _get_fs_provider(remote_filepath: str, storage_options: dict[str, Any] | None = {}) -> FsProvider:
329
+ def _get_fs_provider(remote_filepath: str, storage_options: dict[str, Any] | None = None) -> FsProvider:
328
330
  obj = parse.urlparse(remote_filepath)
329
331
  if obj.scheme == "gs":
330
332
  return GCPFsProvider(storage_options=storage_options)
@@ -642,6 +642,7 @@ class ParquetLoader(BaseItemLoader):
642
642
  self._df: dict[int, Any] = {}
643
643
  self._chunk_row_groups: dict[int, Any] = {}
644
644
  self._chunk_row_group_item_read_count: dict[int, Any] = {}
645
+ self._chunk_row_group_offsets: dict[int, list[int]] = {}
645
646
 
646
647
  def generate_intervals(self) -> list[Interval]:
647
648
  intervals = []
@@ -712,18 +713,28 @@ class ParquetLoader(BaseItemLoader):
712
713
  Returns:
713
714
  Any: The dataframe row corresponding to the specified index.
714
715
  """
716
+ import bisect
717
+
715
718
  import polars as pl
716
719
  import pyarrow.parquet as pq
717
720
 
718
721
  # Load the Parquet file metadata if not already loaded
719
722
  if chunk_index not in self._df:
720
- self._df[chunk_index] = pq.ParquetFile(chunk_filepath)
721
-
722
- # Determine the row group and the row index within the row group
723
- parquet_file = self._df[chunk_index]
724
- num_rows_per_row_group = parquet_file.metadata.row_group(0).num_rows
725
- row_group_index = row_index // num_rows_per_row_group
726
- row_index_within_group = row_index % num_rows_per_row_group
723
+ parquet_file = pq.ParquetFile(chunk_filepath)
724
+ self._df[chunk_index] = parquet_file
725
+ # Precompute cumulative row offsets as a prefix-sum so lookup works for row groups of any size.
726
+ offsets = [0]
727
+ num_row_groups = parquet_file.metadata.num_row_groups
728
+ for i in range(num_row_groups):
729
+ num_rows = parquet_file.metadata.row_group(i).num_rows
730
+ offsets.append(offsets[-1] + num_rows)
731
+ self._chunk_row_group_offsets[chunk_index] = offsets
732
+
733
+ # Locate the row group containing row_index and the offset inside it.
734
+ offsets = self._chunk_row_group_offsets[chunk_index]
735
+ row_group_index = bisect.bisect_right(offsets, row_index) - 1
736
+ row_index_within_group = row_index - offsets[row_group_index]
737
+ row_group_size = offsets[row_group_index + 1] - offsets[row_group_index]
727
738
 
728
739
  # Check if the row group is already loaded
729
740
  if chunk_index in self._chunk_row_groups and row_group_index in self._chunk_row_groups[chunk_index]:
@@ -746,7 +757,7 @@ class ParquetLoader(BaseItemLoader):
746
757
 
747
758
  # Check if the row group has been fully read and release memory if necessary
748
759
  read_count = self._chunk_row_group_item_read_count[chunk_index][row_group_index]
749
- if read_count >= num_rows_per_row_group:
760
+ if read_count >= row_group_size:
750
761
  # Release memory for the fully read row group
751
762
  del self._chunk_row_groups[chunk_index][row_group_index]
752
763
  del self._chunk_row_group_item_read_count[chunk_index][row_group_index]
@@ -797,6 +808,8 @@ class ParquetLoader(BaseItemLoader):
797
808
 
798
809
  if chunk_index in self._chunk_row_group_item_read_count:
799
810
  del self._chunk_row_group_item_read_count[chunk_index]
811
+ if chunk_index in self._chunk_row_group_offsets:
812
+ del self._chunk_row_group_offsets[chunk_index]
800
813
  if os.path.exists(chunk_filepath):
801
814
  os.remove(chunk_filepath)
802
815
  logger.debug(
@@ -820,5 +833,8 @@ class ParquetLoader(BaseItemLoader):
820
833
  if chunk_index in self._chunk_row_group_item_read_count:
821
834
  del self._chunk_row_group_item_read_count[chunk_index]
822
835
 
836
+ if chunk_index in self._chunk_row_group_offsets:
837
+ del self._chunk_row_group_offsets[chunk_index]
838
+
823
839
  def encode_data(self, data: list[bytes], sizes: list[int], flattened: list[Any]) -> Any:
824
840
  pass
@@ -239,25 +239,39 @@ class PrepareChunksThread(Thread):
239
239
 
240
240
  def _force_download(self) -> None:
241
241
  chunk_index = _get_from_queue(self._force_download_queue)
242
- if chunk_index is not None:
243
- # force apply deletion before redownload
244
- self._apply_delete(chunk_index, skip_lock=True)
245
- if _DEBUG:
246
- chunk_filepath, _, _ = self._config[ChunkedIndex(index=-1, chunk_index=chunk_index)]
247
- print(
248
- f"[Reader] Requested force download for {chunk_filepath} "
249
- f"by {self._rank} at {datetime.now().isoformat()}"
250
- )
242
+ if chunk_index is None:
243
+ return
244
+
245
+ chunk_filepath, _, filesize_bytes = self._config[ChunkedIndex(index=-1, chunk_index=chunk_index)]
246
+ download_lock_path = self._config.download_filepath(chunk_index) + ".lock"
247
+ try:
248
+ with FileLock(download_lock_path, timeout=0):
249
+ # The chunk may have been fully downloaded by the time this
250
+ # request was processed, so double check that it still needs
251
+ # downloading before we delete it.
252
+ if os.path.exists(chunk_filepath) and os.stat(chunk_filepath).st_size >= filesize_bytes:
253
+ return
254
+
255
+ # force apply deletion before redownload
256
+ self._apply_delete(chunk_index, skip_lock=True)
257
+ if _DEBUG:
258
+ print(
259
+ f"[Reader] Requested force download for {chunk_filepath} "
260
+ f"by {self._rank} at {datetime.now().isoformat()}"
261
+ )
251
262
 
252
263
  self._config.download_chunk_from_index(chunk_index, skip_lock=True)
264
+ except Timeout:
265
+ # Another worker is actively downloading this chunk. Defer to them.
266
+ return
253
267
 
254
- # Preload item if possible to gain some time but only
255
- # if this is one of the pre-downloaded chunk
256
- if self._pre_download_counter > 0:
257
- self._pre_load_chunk(chunk_index)
268
+ # Preload item if possible to gain some time but only
269
+ # if this is one of the pre-downloaded chunk
270
+ if self._pre_download_counter > 0:
271
+ self._pre_load_chunk(chunk_index)
258
272
 
259
- # Avoid downloading too many chunks in advance at the risk of over using the disk space
260
- self._pre_download_counter += 1
273
+ # Avoid downloading too many chunks in advance at the risk of over using the disk space
274
+ self._pre_download_counter += 1
261
275
 
262
276
  def run(self) -> None:
263
277
  while True:
@@ -217,7 +217,19 @@ def _resolve_data_connection(dir_path: str) -> "V1DataConnection":
217
217
  def _resolve_s3_connections(dir_path: str) -> Dir:
218
218
  data_connection = _resolve_data_connection(dir_path)
219
219
 
220
- return Dir(path=dir_path, url=os.path.join(data_connection.aws.source, *dir_path.split("/")[4:]))
220
+ # S3 connections flagged available on non-AWS providers can't rely on the FUSE mount or an
221
+ # instance profile off AWS, so thread the connection id through. That makes the S3 client mint
222
+ # temporary project-role credentials (same mechanism as R2 / lightning_storage below). Plain
223
+ # AWS-only S3 connections leave this unset and keep using the ambient credentials.
224
+ data_connection_id = (
225
+ data_connection.id if getattr(data_connection.aws, "available_in_non_aws_providers", False) else None
226
+ )
227
+
228
+ return Dir(
229
+ path=dir_path,
230
+ url=os.path.join(data_connection.aws.source, *dir_path.split("/")[4:]),
231
+ data_connection_id=data_connection_id,
232
+ )
221
233
 
222
234
 
223
235
  def _resolve_gcs_connections(dir_path: str) -> Dir:
@@ -20,6 +20,7 @@ from abc import ABC, abstractmethod
20
20
  from collections import OrderedDict
21
21
  from contextlib import suppress
22
22
  from copy import deepcopy
23
+ from dataclasses import asdict
23
24
  from itertools import chain
24
25
  from typing import Any
25
26
 
@@ -32,6 +33,7 @@ from litdata.constants import (
32
33
  _NUMPY_DTYPES_MAPPING,
33
34
  _PIL_AVAILABLE,
34
35
  _TORCH_DTYPES_MAPPING,
36
+ _TORCH_VISION_LESS_THAN_0_26,
35
37
  )
36
38
 
37
39
 
@@ -403,6 +405,13 @@ class VideoSerializer(Serializer):
403
405
  return f.read(), f"video:{file_extension}"
404
406
 
405
407
  def deserialize(self, data: bytes) -> Any:
408
+ # if using torchvision <=0.25, we will use torchvision.io to decode the video
409
+ # otherwise, we will use torchcodec to decode the video, which is faster and more robust
410
+ if _TORCH_VISION_LESS_THAN_0_26:
411
+ return self._deserialize_with_torchvision_io(data)
412
+ return self._deserialize_with_torchcodec(data)
413
+
414
+ def _deserialize_with_torchvision_io(self, data: bytes) -> Any:
406
415
  if not _AV_AVAILABLE:
407
416
  raise ModuleNotFoundError("av is required. Run `pip install av`")
408
417
 
@@ -416,6 +425,29 @@ class VideoSerializer(Serializer):
416
425
  stream.write(data)
417
426
  return torchvision.io.read_video(fname, pts_unit="sec")
418
427
 
428
+ def _deserialize_with_torchcodec(self, data: bytes) -> Any:
429
+ try:
430
+ import torch
431
+ from torchcodec.decoders import AudioDecoder, VideoDecoder
432
+ except ImportError:
433
+ raise ModuleNotFoundError("torchcodec is required. Run `pip install torchcodec>0.11`")
434
+
435
+ dec = VideoDecoder(data, dimension_order="NHWC") # NHWC → T,H,W,C after stacking
436
+ metadata = asdict(dec.metadata) if dec.metadata is not None else {}
437
+
438
+ # get_all_frames() returns a FrameBatch; .data is (N, C, H, W) or (N, H, W, C)
439
+ # depending on dimension_order above
440
+ frame_batch = dec.get_all_frames()
441
+ video = frame_batch.data # shape: (T, H, W, C) with NHWC
442
+
443
+ try:
444
+ audio_dec = AudioDecoder(data)
445
+ audio = audio_dec.get_all_samples().data # (num_channels, num_samples)
446
+ except ValueError:
447
+ audio = torch.zeros(1, 0) # old torchvision path returns aframes with shape (1, 0) for no-audio videos.
448
+
449
+ return video, audio, metadata
450
+
419
451
  def can_serialize(self, data: Any) -> bool:
420
452
  return isinstance(data, str) and os.path.isfile(data) and any(data.endswith(ext) for ext in self._EXTENSIONS)
421
453
 
@@ -13,6 +13,7 @@
13
13
 
14
14
  import json
15
15
  import os
16
+ import re
16
17
  import uuid
17
18
  import warnings
18
19
  from dataclasses import dataclass
@@ -499,7 +500,10 @@ class BinaryWriter:
499
500
  chunks_info.extend(existing_index["chunks"])
500
501
  config = existing_index["config"]
501
502
 
502
- for index_filename in sorted(index_files):
503
+ def _natural_key(s: str) -> list:
504
+ return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", s)]
505
+
506
+ for index_filename in sorted(index_files, key=_natural_key):
503
507
  chunk_path = os.path.join(self._cache_dir, index_filename)
504
508
  with open(chunk_path) as f:
505
509
  data = json.load(f)
@@ -21,9 +21,12 @@ class ParquetDir(ABC):
21
21
  self,
22
22
  dir_path: str | Dir | None,
23
23
  cache_path: str | None = None,
24
- storage_options: dict | None = {},
24
+ storage_options: dict | None = None,
25
25
  num_workers: int = 4,
26
26
  ):
27
+ if storage_options is None:
28
+ storage_options = {}
29
+
27
30
  self.dir = _resolve_dir(dir_path)
28
31
  self.cache_path = cache_path
29
32
  self.storage_options = storage_options
@@ -147,7 +150,7 @@ class CloudParquetDir(ParquetDir):
147
150
  for provider in _CLOUD_PROVIDER:
148
151
  if self.dir.url.startswith(provider):
149
152
  # Initialize the cloud filesystem
150
- self.fs = fsspec.filesystem(provider, *self.storage_options)
153
+ self.fs = fsspec.filesystem(provider, **self.storage_options)
151
154
  print(f"using provider: {provider}")
152
155
  break
153
156
 
@@ -323,7 +326,12 @@ def get_parquet_indexer_cls(
323
326
 
324
327
  obj = parse.urlparse(dir_path)
325
328
 
326
- if obj.scheme in ("local", ""):
329
+ # On Windows, paths like "C:\\Users\\..." parse with scheme='c'. A single-letter
330
+ # scheme is never a valid URI scheme (RFC 3986 requires >=2 chars) so treat it
331
+ # as a Windows drive letter and dispatch to LocalParquetDir.
332
+ is_windows_drive = len(obj.scheme) == 1 and obj.scheme.isalpha()
333
+
334
+ if obj.scheme in ("local", "") or is_windows_drive:
327
335
  return LocalParquetDir(*args)
328
336
  if obj.scheme in ("gs", "s3"):
329
337
  return CloudParquetDir(*args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: litdata
3
- Version: 0.2.61
3
+ Version: 0.2.64
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==2025.12.17; extra == "extras"
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
@@ -22,6 +22,9 @@ src/litdata.egg-info/top_level.txt
22
22
  src/litdata/cli/__init__.py
23
23
  src/litdata/cli/commands.py
24
24
  src/litdata/cli/parser.py
25
+ src/litdata/cli/handler/__init__.py
26
+ src/litdata/cli/handler/cache.py
27
+ src/litdata/cli/handler/optimize.py
25
28
  src/litdata/processing/__init__.py
26
29
  src/litdata/processing/data_processor.py
27
30
  src/litdata/processing/functions.py
@@ -11,9 +11,9 @@ obstore
11
11
  [extras]
12
12
  fsspec
13
13
  google-cloud-storage
14
- lightning-sdk==2025.12.17
14
+ lightning-sdk==2026.2.6
15
15
  pillow
16
16
  polars
17
- pyarrow
17
+ pyarrow<25.0.0
18
18
  tqdm
19
19
  viztracer
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes