loaderx 0.5.4__tar.gz → 0.7.3__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.
- {loaderx-0.5.4 → loaderx-0.7.3}/MANIFEST.in +4 -0
- {loaderx-0.5.4/loaderx.egg-info → loaderx-0.7.3}/PKG-INFO +182 -82
- {loaderx-0.5.4 → loaderx-0.7.3}/README.md +181 -81
- {loaderx-0.5.4 → loaderx-0.7.3}/build.zig +80 -21
- loaderx-0.7.3/loaderx/__init__.py +5 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/loaderx/_sampler.py +12 -5
- {loaderx-0.5.4 → loaderx-0.7.3}/loaderx/_zrecord.py +243 -3
- {loaderx-0.5.4 → loaderx-0.7.3}/loaderx/dataloader.py +35 -21
- loaderx-0.7.3/loaderx/dataset.py +226 -0
- {loaderx-0.5.4 → loaderx-0.7.3/loaderx.egg-info}/PKG-INFO +182 -82
- loaderx-0.7.3/loaderx.egg-info/SOURCES.txt +94 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/scripts/bench.py +28 -20
- {loaderx-0.5.4 → loaderx-0.7.3}/src/zrecord.zig +242 -69
- loaderx-0.7.3/src/zsampler.zig +325 -0
- loaderx-0.7.3/src/zstd/c.zig +735 -0
- loaderx-0.7.3/tests/test_loaderx.py +430 -0
- loaderx-0.7.3/vendor/zstd/COPYING +339 -0
- loaderx-0.7.3/vendor/zstd/LICENSE +30 -0
- loaderx-0.7.3/vendor/zstd/lib/common/allocations.h +55 -0
- loaderx-0.7.3/vendor/zstd/lib/common/bits.h +200 -0
- loaderx-0.7.3/vendor/zstd/lib/common/bitstream.h +457 -0
- loaderx-0.7.3/vendor/zstd/lib/common/compiler.h +450 -0
- loaderx-0.7.3/vendor/zstd/lib/common/cpu.h +249 -0
- loaderx-0.7.3/vendor/zstd/lib/common/debug.c +30 -0
- loaderx-0.7.3/vendor/zstd/lib/common/debug.h +116 -0
- loaderx-0.7.3/vendor/zstd/lib/common/entropy_common.c +340 -0
- loaderx-0.7.3/vendor/zstd/lib/common/error_private.c +63 -0
- loaderx-0.7.3/vendor/zstd/lib/common/error_private.h +168 -0
- loaderx-0.7.3/vendor/zstd/lib/common/fse.h +640 -0
- loaderx-0.7.3/vendor/zstd/lib/common/fse_decompress.c +313 -0
- loaderx-0.7.3/vendor/zstd/lib/common/huf.h +286 -0
- loaderx-0.7.3/vendor/zstd/lib/common/mem.h +426 -0
- loaderx-0.7.3/vendor/zstd/lib/common/pool.c +371 -0
- loaderx-0.7.3/vendor/zstd/lib/common/pool.h +90 -0
- loaderx-0.7.3/vendor/zstd/lib/common/portability_macros.h +158 -0
- loaderx-0.7.3/vendor/zstd/lib/common/threading.c +182 -0
- loaderx-0.7.3/vendor/zstd/lib/common/threading.h +150 -0
- loaderx-0.7.3/vendor/zstd/lib/common/xxhash.c +18 -0
- loaderx-0.7.3/vendor/zstd/lib/common/xxhash.h +7020 -0
- loaderx-0.7.3/vendor/zstd/lib/common/zstd_common.c +48 -0
- loaderx-0.7.3/vendor/zstd/lib/common/zstd_deps.h +111 -0
- loaderx-0.7.3/vendor/zstd/lib/common/zstd_internal.h +392 -0
- loaderx-0.7.3/vendor/zstd/lib/common/zstd_trace.h +163 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/clevels.h +134 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/fse_compress.c +625 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/hist.c +181 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/hist.h +75 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/huf_compress.c +1464 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress.c +7153 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress_internal.h +1534 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress_literals.c +235 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress_literals.h +39 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress_sequences.c +442 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress_sequences.h +54 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress_superblock.c +688 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_compress_superblock.h +32 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_cwksp.h +748 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_double_fast.c +770 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_double_fast.h +50 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_fast.c +968 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_fast.h +38 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_lazy.c +2199 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_lazy.h +202 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_ldm.c +730 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_ldm.h +117 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_ldm_geartab.h +106 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_opt.c +1576 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstd_opt.h +80 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstdmt_compress.c +1882 -0
- loaderx-0.7.3/vendor/zstd/lib/compress/zstdmt_compress.h +113 -0
- loaderx-0.7.3/vendor/zstd/lib/decompress/huf_decompress.c +1944 -0
- loaderx-0.7.3/vendor/zstd/lib/decompress/zstd_ddict.c +244 -0
- loaderx-0.7.3/vendor/zstd/lib/decompress/zstd_ddict.h +44 -0
- loaderx-0.7.3/vendor/zstd/lib/decompress/zstd_decompress.c +2407 -0
- loaderx-0.7.3/vendor/zstd/lib/decompress/zstd_decompress_block.c +2215 -0
- loaderx-0.7.3/vendor/zstd/lib/decompress/zstd_decompress_block.h +73 -0
- loaderx-0.7.3/vendor/zstd/lib/decompress/zstd_decompress_internal.h +240 -0
- loaderx-0.7.3/vendor/zstd/lib/dictBuilder/cover.c +1261 -0
- loaderx-0.7.3/vendor/zstd/lib/dictBuilder/cover.h +152 -0
- loaderx-0.7.3/vendor/zstd/lib/dictBuilder/divsufsort.c +1913 -0
- loaderx-0.7.3/vendor/zstd/lib/dictBuilder/divsufsort.h +67 -0
- loaderx-0.7.3/vendor/zstd/lib/dictBuilder/fastcover.c +766 -0
- loaderx-0.7.3/vendor/zstd/lib/dictBuilder/zdict.c +1133 -0
- loaderx-0.7.3/vendor/zstd/lib/zdict.h +474 -0
- loaderx-0.7.3/vendor/zstd/lib/zstd.h +3089 -0
- loaderx-0.7.3/vendor/zstd/lib/zstd_errors.h +114 -0
- loaderx-0.5.4/loaderx/__init__.py +0 -5
- loaderx-0.5.4/loaderx/dataset.py +0 -222
- loaderx-0.5.4/loaderx.egg-info/SOURCES.txt +0 -23
- loaderx-0.5.4/src/zsampler.zig +0 -94
- loaderx-0.5.4/tests/test_loaderx.py +0 -301
- {loaderx-0.5.4 → loaderx-0.7.3}/LICENSE +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/build.zig.zon +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/loaderx/_lib.py +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/loaderx.egg-info/dependency_links.txt +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/loaderx.egg-info/requires.txt +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/loaderx.egg-info/top_level.txt +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/pyproject.toml +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/scripts/build_wheels.py +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/setup.cfg +0 -0
- {loaderx-0.5.4 → loaderx-0.7.3}/setup.py +0 -0
|
@@ -7,5 +7,9 @@ recursive-include src *.zig
|
|
|
7
7
|
recursive-include tests *.py
|
|
8
8
|
recursive-include scripts *.py
|
|
9
9
|
|
|
10
|
+
# Vendored zstd: the sdist rebuilds with Zig, so it needs the C sources.
|
|
11
|
+
recursive-include vendor *.c *.h
|
|
12
|
+
include vendor/zstd/LICENSE vendor/zstd/COPYING
|
|
13
|
+
|
|
10
14
|
exclude loaderx/lib/lib_here
|
|
11
15
|
recursive-exclude loaderx/lib *.so *.dylib *.dll *.pdb *.lib
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loaderx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: A compact, high-performance persistent record store with zero-copy batch gathering and transparent per-record compression, designed for single-machine AI training and serving pipelines
|
|
5
5
|
Author-email: Ben0i0d <ben0i0d@foxmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -67,12 +67,17 @@ loaderx is built around several core principles:
|
|
|
67
67
|
|
|
68
68
|
## Quick Start
|
|
69
69
|
```python
|
|
70
|
-
from loaderx import Dataset,
|
|
70
|
+
from loaderx import DataLoader, Dataset, write_dataset
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
# Write (zrecord layer — one‑time conversion from NumPy)
|
|
73
|
+
write_dataset('train_data.zr', np.load('data.npy', mmap_mode='r'))
|
|
74
|
+
write_dataset('train_label.zr', np.load('label.npy', mmap_mode='r'))
|
|
74
75
|
|
|
75
|
-
loader
|
|
76
|
+
# Read (loader layer — pure loader, no mutations)
|
|
77
|
+
loader = DataLoader({
|
|
78
|
+
'data': Dataset('train_data.zr'),
|
|
79
|
+
'label': Dataset('train_label.zr'),
|
|
80
|
+
})
|
|
76
81
|
|
|
77
82
|
for i, batch in enumerate(loader):
|
|
78
83
|
if i >= 256:
|
|
@@ -82,34 +87,91 @@ print(batch['data'].shape)
|
|
|
82
87
|
print(batch['label'].shape)
|
|
83
88
|
```
|
|
84
89
|
|
|
90
|
+
A batch is a dict with one entry per stream. Every stream is gathered at the same
|
|
91
|
+
indices, so record `i` lines up across them — a multi-field sample is just more
|
|
92
|
+
streams sharing a length: `DataLoader({'image': ..., 'label': ..., 'meta': ...})`.
|
|
93
|
+
|
|
85
94
|
### Converting a NumPy tensor
|
|
86
95
|
```python
|
|
87
96
|
import numpy as np
|
|
88
|
-
from loaderx import Dataset
|
|
97
|
+
from loaderx import Dataset, write_dataset
|
|
98
|
+
|
|
99
|
+
write_dataset('train_data.zr', np.load('data.npy', mmap_mode='r'))
|
|
100
|
+
write_dataset('train_label.zr', np.load('label.npy', mmap_mode='r'))
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
# many small, similar records compress far better against a trained dictionary:
|
|
103
|
+
write_dataset('train_data.zr', images, codec='zstd_dict')
|
|
92
104
|
```
|
|
93
105
|
One record per slice along axis 0; a 1-D array (the usual shape of a label set)
|
|
94
106
|
becomes a dataset of scalars. The conversion streams in bounded chunks, so an
|
|
95
|
-
mmapped array is never fully materialized.
|
|
107
|
+
mmapped array is never fully materialized. `codec` selects compression —
|
|
108
|
+
`"zstd"` (default), `"raw"`, or `"zstd_dict"` (see [Zrecord](#zrecord)). Open
|
|
109
|
+
the result with :class:`Dataset`::
|
|
110
|
+
|
|
111
|
+
ds = Dataset('train_data.zr')
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
The store carries a single ``schema.json`` recording the per-sample dtype and
|
|
114
|
+
shape — that is the only loaderx-level metadata. Everything beneath it is
|
|
115
|
+
plain Zrecord, reachable through ``loaderx.Zrecord`` when you want raw byte
|
|
100
116
|
records (ragged samples, pre-encoded images) instead of tensors.
|
|
101
117
|
|
|
102
|
-
###
|
|
118
|
+
### Variable-length records
|
|
119
|
+
When records share a dtype but not a length — token sequences, waveforms, point
|
|
120
|
+
sets — use `RaggedDataset`. It writes an iterable of variable-length arrays and
|
|
121
|
+
reads a batch back in the packed layout Zrecord stores natively: one flat buffer
|
|
122
|
+
of every row plus `row_splits`, gathered in place across cores with nothing
|
|
123
|
+
padded. The store stays a plain blob of bytes; how a batch becomes dense is a
|
|
124
|
+
separate, visible step.
|
|
125
|
+
|
|
103
126
|
```python
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
import numpy as np
|
|
128
|
+
from loaderx import RaggedDataset, pad, write_ragged
|
|
129
|
+
|
|
130
|
+
seqs = [np.arange(L, dtype=np.int32) for L in (3, 1, 4, 1, 5)]
|
|
131
|
+
write_ragged('tokens.zr', seqs) # dtype and per-row shape inferred
|
|
132
|
+
rds = RaggedDataset('tokens.zr') # open for reading
|
|
133
|
+
|
|
134
|
+
values, row_splits = rds[[0, 2, 4]] # packed: values is flat, row_splits marks edges
|
|
135
|
+
first = values[row_splits[0]:row_splits[1]] # record 0's rows — the client slices
|
|
136
|
+
|
|
137
|
+
padded, lengths = pad(values, row_splits) # dense (B, max_len, *tail) + row counts
|
|
138
|
+
padded, lengths = rds.getitems_padded([0, 2, 4]) # or fuse the gather and the pad in one pass
|
|
108
139
|
```
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
|
|
141
|
+
Records may carry a fixed trailing shape (`item_shape=(3,)` for sequences of
|
|
142
|
+
3-vectors); a plain 1-D array is a sequence of scalar rows. In a `DataLoader`,
|
|
143
|
+
padding is just the collate `transform` (a batch dict in, a batch dict out), so
|
|
144
|
+
ragged streams and fixed-shape streams flow through the same loader:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
def collate(batch):
|
|
148
|
+
values, row_splits = batch['tokens']
|
|
149
|
+
padded, lengths = pad(values, row_splits)
|
|
150
|
+
return {'input_ids': padded, 'lengths': lengths, 'label': batch['label']}
|
|
151
|
+
|
|
152
|
+
loader = DataLoader({'tokens': rds, 'label': labelset}, batch_size=32, transform=collate)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Codec notes
|
|
156
|
+
|
|
157
|
+
`"zstd"` compresses each record independently with plain zstd (level 3). Use it
|
|
158
|
+
for general-purpose compression — it is fast and the default.
|
|
159
|
+
|
|
160
|
+
`"zstd_dict"` trains a shared dictionary on a sample of the data before writing
|
|
161
|
+
any record, then compresses every record against it at level 19. The dictionary
|
|
162
|
+
captures structure shared across records that per-record compression cannot see —
|
|
163
|
+
a large win for many small, similar records (image tiles, token sequences).
|
|
164
|
+
Dictionary size adapts to the training data automatically; pass `dict_size` to
|
|
165
|
+
cap it.
|
|
166
|
+
|
|
167
|
+
`"zstd_dict"` records can only be read from a store that has the dictionary
|
|
168
|
+
(`dict.zr`). The dictionary is loaded on open and shared, lock-free, across all
|
|
169
|
+
reader threads.
|
|
170
|
+
|
|
171
|
+
**Important:** Do not use `"zstd_dict"` while data is still changing (through
|
|
172
|
+
deletes or compaction below the Python layer). The dictionary captures a snapshot
|
|
173
|
+
of the data; training it before the data settles wastes compression. Train the
|
|
174
|
+
dictionary once preprocessing is complete and the content is final.
|
|
113
175
|
|
|
114
176
|
### Integrating with JAX/Flax
|
|
115
177
|
|
|
@@ -117,7 +179,7 @@ For practical integration examples, please refer to the **[Data2Latent](https://
|
|
|
117
179
|
|
|
118
180
|
## Benchmarks
|
|
119
181
|
|
|
120
|
-
`scripts/bench.py` measures loaderx against the alternatives in
|
|
182
|
+
`scripts/bench.py` measures zrecord and loaderx against the alternatives in
|
|
121
183
|
the index sampler, the record store, and the full data loader — each through the
|
|
122
184
|
binding a client actually uses, so cffi, the GIL and the NumPy allocation are all
|
|
123
185
|
inside the timings. The numbers below are one run on a 24-core machine, Python
|
|
@@ -141,40 +203,44 @@ space, against both NumPy random APIs.
|
|
|
141
203
|
Zsampler draws indices about 2x faster than NumPy's faster API — more against the
|
|
142
204
|
legacy `randint` most code still calls — and the lead is widest at the small
|
|
143
205
|
batches a loader actually draws, where a batch is one cffi call either way and
|
|
144
|
-
NumPy's vectorised fill has nothing to amortise. The
|
|
145
|
-
|
|
146
|
-
|
|
206
|
+
NumPy's vectorised fill has nothing to amortise. The IID draw is unbiased like
|
|
207
|
+
NumPy's: it uses Lemire's method with rejection, which over any real index space
|
|
208
|
+
effectively never redraws, so uniformity costs nothing here.
|
|
147
209
|
|
|
148
|
-
**2. Store** — random batch gather, 20000
|
|
210
|
+
**2. Store** — Zrecord against the alternatives: random batch gather, 20000
|
|
149
211
|
batch 256 × 200, structured (mildly compressible) data.
|
|
150
212
|
|
|
151
|
-
| store
|
|
152
|
-
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
| hdf5
|
|
159
|
-
|
|
|
160
|
-
| lmdb
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
|
|
165
|
-
|
|
213
|
+
| store | write | gather | per batch | on disk | ratio |
|
|
214
|
+
|-------------------|------------|-------------|------------|---------|-------|
|
|
215
|
+
| zrecord-zstd | 531 MiB/s | 10157 MiB/s | 0.30 ms | 216 MiB | 1.08x |
|
|
216
|
+
| zrecord-zstdict | 40 MiB/s | 4387 MiB/s | 0.68 ms | 201 MiB | 1.17x |
|
|
217
|
+
| zrecord-raw | 794 MiB/s | 21317 MiB/s | 0.14 ms | 235 MiB | 1.00x |
|
|
218
|
+
| npy-mmap | 2633 MiB/s | 10057 MiB/s | 0.30 ms | 234 MiB | 1.00x |
|
|
219
|
+
| arrayrecord | 329 MiB/s | 744 MiB/s | 4.03 ms | 217 MiB | 1.08x |
|
|
220
|
+
| hdf5 | 1105 MiB/s | 486 MiB/s | 6.18 ms | 236 MiB | 0.99x |
|
|
221
|
+
| hdf5-gzip | 78 MiB/s | 193 MiB/s | 15.51 ms | 211 MiB | 1.11x |
|
|
222
|
+
| lmdb | 666 MiB/s | 4655 MiB/s | 0.64 ms | 313 MiB | 0.75x |
|
|
223
|
+
| lmdb-zlib | 70 MiB/s | 306 MiB/s | 9.81 ms | 235 MiB | 1.00x |
|
|
224
|
+
| blosc2 | 30 MiB/s | 217 MiB/s | 13.81 ms | 216 MiB | 1.09x |
|
|
225
|
+
| zarr | 78 MiB/s | 23 MiB/s | 131.13 ms | 217 MiB | 1.08x |
|
|
226
|
+
| tensorstore | 545 MiB/s | 89 MiB/s | 33.76 ms | 214 MiB | 1.09x |
|
|
227
|
+
|
|
228
|
+
The gather column is what the store is built for. zrecord-raw returns a scattered
|
|
166
229
|
batch faster than everything here, a memory-mapped `.npy` included, because it
|
|
167
|
-
copies straight into the destination across all cores with the GIL released;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
230
|
+
copies straight into the destination across all cores with the GIL released; and
|
|
231
|
+
zrecord-zstd keeps most of that speed *while* decompressing — 10 GiB/s, several
|
|
232
|
+
times faster than any other compressed store (hdf5-gzip, lmdb-zlib, blosc2, zarr,
|
|
233
|
+
tensorstore) and faster even than the uncompressed `.npy`. That is the point of
|
|
234
|
+
one fast codec: turning compression on barely costs a read. The array stores lag
|
|
235
|
+
on gather because a scattered single-record read fights a layout meant for
|
|
236
|
+
contiguous slabs — exactly the access pattern a training loader has. Where zrecord
|
|
237
|
+
does not lead is write: it pays compression and its own layout up front, a
|
|
238
|
+
one-time cost it does not chase (`zrecord-zstdict` most of all, at level 19).
|
|
239
|
+
Compression is modest here (1.08–1.17x) only because this data is barely
|
|
240
|
+
compressible; on the smooth-image set of the [Zrecord](#zrecord) notes plain zstd
|
|
241
|
+
reaches 7.6x and the dictionary 16.5x.
|
|
242
|
+
|
|
243
|
+
The reads are not all single-threaded: zrecord and blosc2 decompress across cores,
|
|
178
244
|
the rest one record at a time. Zarr and TensorStore are sharded so `on disk`
|
|
179
245
|
reflects compression rather than per-file block rounding, and TensorStore is given
|
|
180
246
|
gzip (its zarr3 default is uncompressed). `on disk` is allocated blocks, which is
|
|
@@ -187,20 +253,21 @@ for: loaderx from Zrecord, Grain from ArrayRecord, torch from a memory-mapped
|
|
|
187
253
|
|
|
188
254
|
| loader | batches/s | per batch | samples/s | throughput |
|
|
189
255
|
|----------------|-----------|-----------|-----------|-------------|
|
|
190
|
-
| **loaderx** |
|
|
191
|
-
| loaderx-raw |
|
|
192
|
-
| grain |
|
|
193
|
-
| torch |
|
|
194
|
-
| jax-dataloader |
|
|
195
|
-
|
|
196
|
-
End to end, loaderx
|
|
197
|
-
|
|
198
|
-
jax-dataloader's number is an in-memory ceiling with no storage path at all.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
torch and Grain use worker processes that escape the
|
|
202
|
-
loaderx never ends an epoch, so no step waits on a
|
|
203
|
-
per epoch, a cost that is in their numbers as it
|
|
256
|
+
| **loaderx** | 4113.9 | 0.24 ms | 1,053,168 | 12342 MiB/s |
|
|
257
|
+
| loaderx-raw | 4838.9 | 0.21 ms | 1,238,752 | 14517 MiB/s |
|
|
258
|
+
| grain | 62.9 | 15.90 ms | 16,097 | 189 MiB/s |
|
|
259
|
+
| torch | 961.0 | 1.04 ms | 246,005 | 2883 MiB/s |
|
|
260
|
+
| jax-dataloader | 3176.8 | 0.31 ms | 813,270 | 9531 MiB/s |
|
|
261
|
+
|
|
262
|
+
End to end, loaderx serves batches several times faster than torch or Grain and
|
|
263
|
+
ahead of jax-dataloader — and it does so from disk-backed *compressed* storage,
|
|
264
|
+
where jax-dataloader's number is an in-memory ceiling with no storage path at all.
|
|
265
|
+
zstd decompresses fast enough that the compressed loader nearly matches the raw
|
|
266
|
+
one (4114 vs 4839 batches/s), so on-disk compression is close to free here.
|
|
267
|
+
loaderx and jax use threads; torch and Grain use worker processes that escape the
|
|
268
|
+
GIL but pay IPC per batch. loaderx never ends an epoch, so no step waits on a
|
|
269
|
+
boundary; the others restart per epoch, a cost that is in their numbers as it
|
|
270
|
+
would be in training.
|
|
204
271
|
|
|
205
272
|
## Current Limitations
|
|
206
273
|
* Single-host only; multi-host training is not supported.
|
|
@@ -244,12 +311,26 @@ The sdist ships sources only. Installing from it runs `zig build` through
|
|
|
244
311
|
---
|
|
245
312
|
|
|
246
313
|
# Zsampler
|
|
247
|
-
Index Generator: a high-performance sampler implemented in Zig
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
314
|
+
Index Generator: a high-performance sampler implemented in Zig. Every mode is a
|
|
315
|
+
pure function of `(seed, step)`, so a run resumes exactly by seeking to a step —
|
|
316
|
+
there is no epoch to track, in keeping with the endless step-based loader.
|
|
317
|
+
|
|
318
|
+
1. **Sequential** — traverse the index space in order through a fixed-size sliding
|
|
319
|
+
window, treating the space as a circular queue so the tail never truncates.
|
|
320
|
+
2. **IID** — draw each index uniformly at random with replacement. Unbiased
|
|
321
|
+
(Lemire with rejection), matching NumPy. Simplest, but coverage is uneven over
|
|
322
|
+
any short run.
|
|
323
|
+
3. **Cyclic** — without replacement, round-robin. Each cycle traverses a fresh
|
|
324
|
+
permutation of the whole index space, so within a cycle every record appears
|
|
325
|
+
exactly once and no batch repeats an index — coverage is even by construction,
|
|
326
|
+
which keeps how often each sample is seen uniform. The permutation is a
|
|
327
|
+
stateless bijection (a small Feistel network over the index space, brought
|
|
328
|
+
into range by cycle-walking), so a million-record shuffle materializes nothing
|
|
329
|
+
the size of the dataset and reshuffling each cycle is free. Every batch is
|
|
330
|
+
exactly `batch_size`: an endless step-based loader has no final partial batch
|
|
331
|
+
to special-case, so when `batch_size` does not divide the length the cycle's
|
|
332
|
+
remainder is dropped — a different remainder each cycle, since the permutation
|
|
333
|
+
changes, so every record is still reached over time.
|
|
253
334
|
|
|
254
335
|
---
|
|
255
336
|
|
|
@@ -271,10 +352,11 @@ A record-based data runtime, focused on delivering extreme throughput and low la
|
|
|
271
352
|
6. Each record is compressed and decompressed independently:
|
|
272
353
|
|
|
273
354
|
```
|
|
274
|
-
| tag | name
|
|
275
|
-
|
|
276
|
-
| 0 |
|
|
277
|
-
| 1 |
|
|
355
|
+
| tag | name | algorithm |
|
|
356
|
+
|-------|-----------|----------------------------------------|
|
|
357
|
+
| 0 | raw | none |
|
|
358
|
+
| 1 | zstd | zstd (plain, level 3) |
|
|
359
|
+
| 2 | zstdict | zstd with a trained dictionary (level 19) |
|
|
278
360
|
```
|
|
279
361
|
|
|
280
362
|
7. Compression is transparent to the client:
|
|
@@ -283,6 +365,22 @@ A record-based data runtime, focused on delivering extreme throughput and low la
|
|
|
283
365
|
data itself.
|
|
284
366
|
* Decompression writes straight into the caller's destination memory
|
|
285
367
|
(`gather`), with no intermediate buffer and no extra copy.
|
|
368
|
+
* zstd is the one transparent codec — faster than Deflate at both ends and a
|
|
369
|
+
better ratio, so there is no reason to carry a second. It is vendored C,
|
|
370
|
+
built for every platform by Zig, so the one-wheel-per-platform story is
|
|
371
|
+
unchanged.
|
|
372
|
+
* `zstd_dict` additionally trains one dictionary on a sample of the data
|
|
373
|
+
(stored as `dict.zr`) and compresses every record against it. Because each
|
|
374
|
+
record is still independent, random access is unchanged — but the
|
|
375
|
+
dictionary carries the structure shared across records, which per-record
|
|
376
|
+
compression cannot see. On many small, similar records (image tiles, token
|
|
377
|
+
sequences) this is a large win: a smooth-image set that plain zstd takes to
|
|
378
|
+
7.6x compresses 16.5x with a dictionary. The dictionary is loaded once on
|
|
379
|
+
open and shared, lock-free, across all reader threads. The dictionary size
|
|
380
|
+
adapts to the training data automatically.
|
|
381
|
+
* Records stored with `zstd_dict` are explicitly tagged; they cannot be read
|
|
382
|
+
by a store that lacks the dictionary. Plain `zstd` records never use a
|
|
383
|
+
dictionary even if one is present.
|
|
286
384
|
|
|
287
385
|
## Persistence format
|
|
288
386
|
Zrecord storage is metadata plus chunked data:
|
|
@@ -329,7 +427,7 @@ a physical address is what makes random access efficient.
|
|
|
329
427
|
the algorithm.
|
|
330
428
|
|
|
331
429
|
```zig
|
|
332
|
-
const Codec = enum(u8) { raw = 0,
|
|
430
|
+
const Codec = enum(u8) { raw = 0, zstd = 1, zstdict = 2, _ };
|
|
333
431
|
const RecordLoc = extern struct {
|
|
334
432
|
offset: u32, phys_length: u32, logic_length: u32,
|
|
335
433
|
chunk_id: u16, compress: Codec, _reserved: u8,
|
|
@@ -376,10 +474,12 @@ side (executed on async threads).
|
|
|
376
474
|
beyond the limit run inline on the calling thread.
|
|
377
475
|
* Shards receive contiguous blocks rather than a strided subset, keeping each
|
|
378
476
|
worker's reads and writes sequential.
|
|
379
|
-
* Each shard
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
477
|
+
* Each shard creates one zstd context (`ZSTD_CCtx` to write, `ZSTD_DCtx` to
|
|
478
|
+
read) and reuses it across every record it handles, rather than paying that
|
|
479
|
+
setup per record. The dictionary (`ZSTD_CDict`/`ZSTD_DDict`) is immutable, so
|
|
480
|
+
all shards share one, lock-free.
|
|
481
|
+
* Decompression writes straight into the caller's destination buffer, so there
|
|
482
|
+
is no intermediate copy.
|
|
383
483
|
|
|
384
484
|
**4. Garbage collection.** Deletion leaves the record's bytes stranded, so space
|
|
385
485
|
is reclaimed by an offline `compact` that rewrites the live records in place.
|
|
@@ -400,7 +500,7 @@ is reclaimed by an offline `compact` that rewrites the live records in place.
|
|
|
400
500
|
that compaction cannot remove.
|
|
401
501
|
|
|
402
502
|
**5. File access.**
|
|
403
|
-
* Metadata: a
|
|
503
|
+
* Metadata: a 256 MiB file created at init, accessed by mmap thereafter.
|
|
404
504
|
* Chunk data: 4 GiB files created at init, accessed concurrently through
|
|
405
505
|
`readPositionalAll`/`writePositionalAll`.
|
|
406
506
|
|