loaderx 2.2.1__tar.gz → 2.3.5__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-2.2.1/loaderx.egg-info → loaderx-2.3.5}/PKG-INFO +184 -143
- {loaderx-2.2.1 → loaderx-2.3.5}/README.md +181 -142
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx/__init__.py +4 -2
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx/_store.py +8 -4
- loaderx-2.3.5/loaderx/converter.py +204 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx/zrecord.py +23 -7
- {loaderx-2.2.1 → loaderx-2.3.5/loaderx.egg-info}/PKG-INFO +184 -143
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx.egg-info/SOURCES.txt +1 -0
- loaderx-2.3.5/loaderx.egg-info/requires.txt +6 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/pyproject.toml +3 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/_bench_common.py +5 -3
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/bench_dense.py +4 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/bench_ragged.py +9 -3
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/test_loaderx.py +283 -68
- {loaderx-2.2.1 → loaderx-2.3.5}/src/record/engine.zig +202 -241
- {loaderx-2.2.1 → loaderx-2.3.5}/src/record/executor.zig +0 -10
- {loaderx-2.2.1 → loaderx-2.3.5}/src/record/storage.zig +125 -55
- {loaderx-2.2.1 → loaderx-2.3.5}/src/store.zig +5 -10
- loaderx-2.2.1/loaderx.egg-info/requires.txt +0 -3
- {loaderx-2.2.1 → loaderx-2.3.5}/LICENSE +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/MANIFEST.in +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/build.zig +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/build.zig.zon +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx/_lib.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx/dataloader.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx/utils.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx/zsampler.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx.egg-info/dependency_links.txt +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/loaderx.egg-info/top_level.txt +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/bench.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/build_wheels.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/prepare_tokens.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/scripts/requirements-bench.txt +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/setup.cfg +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/setup.py +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/src/zsampler.zig +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/src/zstd/c.zig +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/COPYING +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/LICENSE +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/allocations.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/bits.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/bitstream.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/compiler.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/cpu.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/debug.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/debug.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/entropy_common.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/error_private.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/error_private.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/fse.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/fse_decompress.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/huf.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/mem.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/pool.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/pool.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/portability_macros.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/threading.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/threading.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/xxhash.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/xxhash.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/zstd_common.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/zstd_deps.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/zstd_internal.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/common/zstd_trace.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/clevels.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/fse_compress.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/hist.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/hist.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/huf_compress.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress_internal.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress_literals.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress_literals.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress_sequences.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress_sequences.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress_superblock.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_compress_superblock.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_cwksp.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_double_fast.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_double_fast.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_fast.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_fast.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_lazy.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_lazy.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_ldm.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_ldm.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_ldm_geartab.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_opt.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_opt.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_preSplit.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstd_preSplit.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstdmt_compress.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/compress/zstdmt_compress.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/huf_decompress.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/huf_decompress_amd64.S +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/zstd_ddict.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/zstd_ddict.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/zstd_decompress.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/zstd_decompress_block.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/zstd_decompress_block.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/decompress/zstd_decompress_internal.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/dictBuilder/cover.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/dictBuilder/cover.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/dictBuilder/divsufsort.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/dictBuilder/divsufsort.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/dictBuilder/fastcover.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/dictBuilder/zdict.c +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/zdict.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/zstd.h +0 -0
- {loaderx-2.2.1 → loaderx-2.3.5}/vendor/zstd/lib/zstd_errors.h +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loaderx
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.5
|
|
4
4
|
Summary: Rebuildable high-performance ordered record containers
|
|
5
5
|
Author-email: Ben0i0d <ben0i0d@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,6 +21,8 @@ License-File: LICENSE
|
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: cffi
|
|
23
23
|
Requires-Dist: msgpack
|
|
24
|
+
Provides-Extra: converter
|
|
25
|
+
Requires-Dist: datasets[vision]>=2.19; extra == "converter"
|
|
24
26
|
Dynamic: license-file
|
|
25
27
|
|
|
26
28
|
# Loaderx
|
|
@@ -73,9 +75,9 @@ from loaderx.dataloader import DataLoader
|
|
|
73
75
|
|
|
74
76
|
data = np.load('data.npy', mmap_mode='r')
|
|
75
77
|
label = np.load('label.npy', mmap_mode='r')
|
|
76
|
-
with Dense.create('train_data', data.dtype, data.shape[1:]) as ds:
|
|
78
|
+
with Dense.create('train_data', data.dtype, data.shape[1:], codec='zstd') as ds:
|
|
77
79
|
ds.append(data)
|
|
78
|
-
with Dense.create('train_label', label.dtype, label.shape[1:]) as ds:
|
|
80
|
+
with Dense.create('train_label', label.dtype, label.shape[1:], codec='zstd') as ds:
|
|
79
81
|
ds.append(label)
|
|
80
82
|
|
|
81
83
|
data_store = Dense.open('train_data')
|
|
@@ -100,13 +102,70 @@ at the same indices, so record ``i`` lines up across them. The ``transform``
|
|
|
100
102
|
callback is the collate step — reshape, cast, stack — where ``values`` is the
|
|
101
103
|
plain dense batch ready for the model.
|
|
102
104
|
|
|
105
|
+
### Offline Hugging Face conversion
|
|
106
|
+
|
|
107
|
+
The optional converter uses Hugging Face Datasets for remote discovery,
|
|
108
|
+
download, caching, revision handling and source-format decoding. It then writes
|
|
109
|
+
explicit typed Zrecord streams, so training needs neither `datasets` nor Arrow:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install 'loaderx[converter]'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from loaderx.converter import convert, huggingface
|
|
117
|
+
|
|
118
|
+
dataset = huggingface("ylecun/mnist", revision="main", token=None)
|
|
119
|
+
convert(
|
|
120
|
+
dataset,
|
|
121
|
+
"mnist",
|
|
122
|
+
codec="zstd",
|
|
123
|
+
batch_size=256,
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`huggingface` resolves the requested branch, tag or commit to an immutable
|
|
128
|
+
snapshot, then downloads and returns the repository's DatasetDict. Repositories
|
|
129
|
+
without one default config require `config=...`. `convert` is the independent
|
|
130
|
+
persistence stage: it accepts that DatasetDict or one already normalized
|
|
131
|
+
Dataset. `batch_size` bounds records per Python batch; actual bytes also depend
|
|
132
|
+
on record sizes and Ragged packing.
|
|
133
|
+
For private or gated repositories, pass `token="hf_..."`; `None` uses the
|
|
134
|
+
standard HF environment, local login, or anonymous access.
|
|
135
|
+
Static or regularly batched numeric arrays become Dense stores, while dynamic
|
|
136
|
+
arrays become Ragged stores; source dtype and record shape are preserved.
|
|
137
|
+
|
|
138
|
+
Zrecord persists numeric tensors rather than Python objects. A standard HF
|
|
139
|
+
feature that cannot be represented losslessly fails with its column name;
|
|
140
|
+
conversion does not filter, pad, tokenize, reshape, or run a transform stage.
|
|
141
|
+
Arrow dtypes, including float64 and uint64, are preserved. Only the current
|
|
142
|
+
batch is formatted; `with_format` never materializes the complete dataset.
|
|
143
|
+
|
|
144
|
+
The result groups aligned streams under one published root:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
mnist/
|
|
148
|
+
train/
|
|
149
|
+
image/
|
|
150
|
+
label/
|
|
151
|
+
test/
|
|
152
|
+
image/
|
|
153
|
+
label/
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Conversion happens under a temporary sibling directory. The root is renamed
|
|
157
|
+
into place only after all stores close and their record counts agree; a failed
|
|
158
|
+
build that raises a Python exception is discarded. A process kill may leave the
|
|
159
|
+
hidden temporary sibling for manual cleanup. No collection manifest is added;
|
|
160
|
+
the output is only split directories containing ordinary column Zrecord stores.
|
|
161
|
+
|
|
103
162
|
### Creating a dense store
|
|
104
163
|
```python
|
|
105
164
|
import numpy as np
|
|
106
165
|
from loaderx.zrecord import Dense
|
|
107
166
|
|
|
108
167
|
data = np.load('data.npy', mmap_mode='r')
|
|
109
|
-
with Dense.create('train_data', data.dtype, data.shape[1:]) as ds:
|
|
168
|
+
with Dense.create('train_data', data.dtype, data.shape[1:], codec='zstd') as ds:
|
|
110
169
|
ds.append(data)
|
|
111
170
|
```
|
|
112
171
|
One record per slice along axis 0; a 1-D array (the usual shape of a label set)
|
|
@@ -132,7 +191,7 @@ engine consumes the trusted Dense stride or Ragged offsets. Append inputs are
|
|
|
132
191
|
strictly NumPy arrays: Dense takes one batched ndarray and Ragged takes an
|
|
133
192
|
iterable of ndarrays. Raw bytes and pre-encoded images are made explicit with
|
|
134
193
|
``np.frombuffer(raw, dtype=np.uint8)`` and stored in a
|
|
135
|
-
``Ragged.create(path, dtype=np.uint8, ndim=1)`` rather than creating a second
|
|
194
|
+
``Ragged.create(path, dtype=np.uint8, ndim=1, codec="zstd")`` rather than creating a second
|
|
136
195
|
public storage API.
|
|
137
196
|
|
|
138
197
|
### Records
|
|
@@ -147,7 +206,7 @@ import numpy as np
|
|
|
147
206
|
from loaderx.zrecord import Dense
|
|
148
207
|
|
|
149
208
|
data = np.arange(64, dtype=np.float32).reshape(8, 2, 4)
|
|
150
|
-
with Dense.create('data', data.dtype, data.shape[1:]) as ds:
|
|
209
|
+
with Dense.create('data', data.dtype, data.shape[1:], codec='zstd') as ds:
|
|
151
210
|
ds.append(data)
|
|
152
211
|
ds = Dense.open('data')
|
|
153
212
|
ds[0, 5, 2] # (3, 2, 4) — shape from the persisted schema
|
|
@@ -173,7 +232,7 @@ batch is the model's call — a plain numpy loop, wherever you need it:
|
|
|
173
232
|
from loaderx.zrecord import Ragged
|
|
174
233
|
|
|
175
234
|
seqs = [np.arange(L, dtype=np.int32) for L in (3, 1, 4, 1, 5)]
|
|
176
|
-
with Ragged.create('tokens', np.int32, ndim=1) as rs:
|
|
235
|
+
with Ragged.create('tokens', np.int32, ndim=1, codec='zstd') as rs:
|
|
177
236
|
rs.append(seqs) # dtype/rank fixed; lengths remain per-record
|
|
178
237
|
rs = Ragged.open('tokens')
|
|
179
238
|
|
|
@@ -260,8 +319,10 @@ warmup belongs outside build or loader benchmark timing.
|
|
|
260
319
|
### Creating containers
|
|
261
320
|
|
|
262
321
|
``Dense.create`` and ``Ragged.create`` return append-only ordered-sequence
|
|
263
|
-
builders. ``
|
|
264
|
-
|
|
322
|
+
builders. The ``codec`` keyword is required: callers must explicitly select
|
|
323
|
+
``"raw"``, ``"zstd"``, or ``"zstd_dict"``. ``append`` is explicit — one input
|
|
324
|
+
batch extends the logical sequence without exposing native physical completion
|
|
325
|
+
order.
|
|
265
326
|
Dense append is synchronous and borrows an already-contiguous ndarray without a
|
|
266
327
|
snapshot copy. Ragged append consumes its iterable once into one owned packed
|
|
267
328
|
buffer, then completes the native append before returning. Nothing is inferred.
|
|
@@ -269,12 +330,13 @@ buffer, then completes the native append before returning. Nothing is inferred.
|
|
|
269
330
|
```python
|
|
270
331
|
from loaderx.zrecord import Dense, Ragged
|
|
271
332
|
|
|
272
|
-
ds = Dense.create('mnist/x', dtype=np.uint8, item_shape=(28, 28)
|
|
333
|
+
ds = Dense.create('mnist/x', dtype=np.uint8, item_shape=(28, 28),
|
|
334
|
+
codec='zstd', data_shards=4)
|
|
273
335
|
ds.append(images[i:i + 1024]) # synchronous native batch; returns None
|
|
274
336
|
ds.append(single_image[None]) # one sample is batch_size 1 — add the axis yourself
|
|
275
337
|
ds.close() # publish before opening
|
|
276
338
|
|
|
277
|
-
tok = Ragged.create('tokens', dtype=np.int32, ndim=1)
|
|
339
|
+
tok = Ragged.create('tokens', dtype=np.int32, ndim=1, codec='zstd')
|
|
278
340
|
tok.append([seq_a, seq_b, seq_c])
|
|
279
341
|
tok.close()
|
|
280
342
|
|
|
@@ -282,6 +344,11 @@ with Dense.open('mnist/x') as ds:
|
|
|
282
344
|
first_four = ds[:4] # opened containers are read-only
|
|
283
345
|
```
|
|
284
346
|
|
|
347
|
+
`data_shards` is the keyword-only write-parallelism setting. It is persisted in
|
|
348
|
+
the native Header, must be in `1..255`, and defaults to four. Values near the
|
|
349
|
+
writer lane count spread payload writes across independent files; opening a
|
|
350
|
+
completed container discovers the value automatically.
|
|
351
|
+
|
|
285
352
|
Dense and Ragged append both report native errors in the current call and return
|
|
286
353
|
only after accepting the batch. A writer cannot be read, and a reader cannot be
|
|
287
354
|
appended to. ``close()`` on a writer publishes the container Header;
|
|
@@ -295,7 +362,7 @@ schemas contain ``dtype`` and ``ndim``; Dense additionally contains
|
|
|
295
362
|
appended record to have that rank, while every dimension length may vary.
|
|
296
363
|
Structured, subarray, object, metadata-bearing, and zero-itemsize dtypes are not
|
|
297
364
|
supported: their semantics do not round-trip through one canonical NumPy dtype
|
|
298
|
-
string. The encoded schema has
|
|
365
|
+
string. The encoded schema has 2040 bytes available in the fixed 4096-byte
|
|
299
366
|
metadata page. Ragged schema size is fixed; Dense schema size grows
|
|
300
367
|
only with the integer ``item_shape``, so the physical limit is far above any
|
|
301
368
|
practical NumPy array rank.
|
|
@@ -310,7 +377,7 @@ published without one.
|
|
|
310
377
|
### Codec notes
|
|
311
378
|
|
|
312
379
|
`"zstd"` compresses each record independently with plain zstd (level 3). Use it
|
|
313
|
-
for general-purpose compression
|
|
380
|
+
for general-purpose compression; it is fast and must be selected explicitly.
|
|
314
381
|
|
|
315
382
|
`"zstd_dict"` trains a shared dictionary on a sample of the data before writing
|
|
316
383
|
any record, then compresses every record against it at level 15. The dictionary
|
|
@@ -404,11 +471,11 @@ from loaderx.zrecord import Dense, Ragged
|
|
|
404
471
|
from loaderx.dataloader import DataLoader
|
|
405
472
|
|
|
406
473
|
root = "xsub/train"
|
|
407
|
-
with Dense.create(root + "/joint", joint.dtype, joint.shape[1:]) as s:
|
|
474
|
+
with Dense.create(root + "/joint", joint.dtype, joint.shape[1:], codec="zstd") as s:
|
|
408
475
|
s.append(joint)
|
|
409
|
-
with Dense.create(root + "/label", label.dtype, label.shape[1:]) as s:
|
|
476
|
+
with Dense.create(root + "/label", label.dtype, label.shape[1:], codec="zstd") as s:
|
|
410
477
|
s.append(label)
|
|
411
|
-
with Ragged.create(root + "/token", np.int32, ndim=1) as s:
|
|
478
|
+
with Ragged.create(root + "/token", np.int32, ndim=1, codec="zstd") as s:
|
|
412
479
|
s.append(seqs)
|
|
413
480
|
|
|
414
481
|
streams = {
|
|
@@ -502,8 +569,8 @@ so CFFI, NumPy allocation, and Ragged list/shape reconstruction are timed.
|
|
|
502
569
|
### Methodology
|
|
503
570
|
|
|
504
571
|
The comparison matrix began as one complete run on a warm page cache. All
|
|
505
|
-
Zrecord Store rows and the two loaderx identity rows were refreshed on 2.
|
|
506
|
-
|
|
572
|
+
Zrecord Store rows and the two loaderx identity rows were refreshed on 2.3.0
|
|
573
|
+
with the default `data_shards=4`. Unchanged
|
|
507
574
|
optional Store backends, sampler rows and external loader rows remain from the
|
|
508
575
|
immediately preceding 2.0.9 run. These are not three-run medians: an unexpected result is traced
|
|
509
576
|
separately instead of being hidden by repeated aggregation. Within each store workload every
|
|
@@ -583,9 +650,9 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
|
|
|
583
650
|
|
|
584
651
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
585
652
|
|---|---:|---:|---:|---:|---:|---:|
|
|
586
|
-
| zrecord-zstd |
|
|
587
|
-
| zrecord-zstdict |
|
|
588
|
-
| zrecord-raw |
|
|
653
|
+
| zrecord-zstd | 7172 MiB/s | 11081 MiB/s | 77.2 | 4.38 ms | 24.4 MiB | 14.69x |
|
|
654
|
+
| zrecord-zstdict | 104 MiB/s | 12042 MiB/s | 83.9 | 4.09 ms | 16.2 MiB | 22.10x |
|
|
655
|
+
| zrecord-raw | 5721 MiB/s | 14457 MiB/s | 100.7 | 3.16 ms | 358.9 MiB | 1.00x |
|
|
589
656
|
| npy-mmap-raw | 2004 MiB/s | 4749 MiB/s | 33.1 | 11.08 ms | 358.9 MiB | 1.00x |
|
|
590
657
|
| hdf5-raw | 2350 MiB/s | 1836 MiB/s | 12.8 | 29.68 ms | 359.0 MiB | 1.00x |
|
|
591
658
|
| hdf5-gzip | 277 MiB/s | 657 MiB/s | 4.6 | 62.53 ms | 26.1 MiB | 13.73x |
|
|
@@ -599,8 +666,8 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
|
|
|
599
666
|
| tiledb-raw | 743 MiB/s | 630 MiB/s | 4.4 | 66.01 ms | 359.0 MiB | 1.00x |
|
|
600
667
|
| tiledb-zstd | 1498 MiB/s | 1503 MiB/s | 10.5 | 27.82 ms | 26.9 MiB | 13.36x |
|
|
601
668
|
|
|
602
|
-
At 147 KiB per record, Zrecord-raw reaches 14.
|
|
603
|
-
plain zstd gathers at 10.
|
|
669
|
+
At 147 KiB per record, Zrecord-raw reaches 14.1 GiB/s and is 3.0x npy-mmap-raw;
|
|
670
|
+
plain zstd gathers at 10.8 GiB/s while reducing the corpus 14.69x. LMDB and Arrow
|
|
604
671
|
IPC are competitive raw record
|
|
605
672
|
stores, while codecs tied to whole IPC batches or Parquet row groups pay read
|
|
606
673
|
amplification on random gathers. Dense demonstrates that
|
|
@@ -618,9 +685,9 @@ list or a one-dimensional variable-length abstraction is not enough.
|
|
|
618
685
|
|
|
619
686
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
620
687
|
|---|---:|---:|---:|---:|---:|---:|
|
|
621
|
-
| zrecord-zstd |
|
|
622
|
-
| zrecord-zstdict | 102 MiB/s |
|
|
623
|
-
| zrecord-raw |
|
|
688
|
+
| zrecord-zstd | 2220 MiB/s | 9444 MiB/s | 56.7 | 5.67 ms | 26.9 MiB | 15.48x |
|
|
689
|
+
| zrecord-zstdict | 102 MiB/s | 10345 MiB/s | 62.1 | 5.15 ms | 18.2 MiB | 22.97x |
|
|
690
|
+
| zrecord-raw | 2171 MiB/s | 12356 MiB/s | 74.2 | 4.22 ms | 417.2 MiB | 1.00x |
|
|
624
691
|
| hdf5-raw | 1451 MiB/s | 1059 MiB/s | 6.4 | 44.91 ms | 418.0 MiB | 1.00x |
|
|
625
692
|
| hdf5-gzip | 250 MiB/s | 126 MiB/s | 0.7 | 359.23 ms | 29.9 MiB | 13.94x |
|
|
626
693
|
| lmdb-raw | 1717 MiB/s | 7269 MiB/s | 43.6 | 7.93 ms | 422.2 MiB | 0.99x |
|
|
@@ -633,16 +700,16 @@ list or a one-dimensional variable-length abstraction is not enough.
|
|
|
633
700
|
| tiledb-raw | 392 MiB/s | 48 MiB/s | 0.3 | 941.09 ms | 417.2 MiB | 1.00x |
|
|
634
701
|
| tiledb-zstd | 675 MiB/s | 138 MiB/s | 0.8 | 328.05 ms | 27.0 MiB | 15.46x |
|
|
635
702
|
|
|
636
|
-
Zrecord-raw is 1.
|
|
637
|
-
delivers 9.
|
|
703
|
+
Zrecord-raw is 1.7x LMDB and 3.1x Arrow IPC in logical gather. Zrecord-zstd
|
|
704
|
+
delivers 9.2 GiB/s of
|
|
638
705
|
logical payload while reducing the corpus to 26.9 MiB. HDF5, Arrow IPC, Parquet,
|
|
639
706
|
ArrayRecord and TileDB
|
|
640
707
|
show the same framework/codec tradeoffs in both tables; compressed batch, chunk
|
|
641
708
|
and row-group formats pay read amplification on random records.
|
|
642
709
|
|
|
643
710
|
The shared generator makes compression ratios directly comparable across
|
|
644
|
-
contracts: Zrecord zstd is 14.69x Dense versus 15.
|
|
645
|
-
22.10x versus 22.
|
|
711
|
+
contracts: Zrecord zstd is 14.69x Dense versus 15.48x Ragged, and zstdict is
|
|
712
|
+
22.10x versus 22.97x. The remaining difference comes from the H/W distribution
|
|
646
713
|
and Ragged shape metadata, not a different image entropy model.
|
|
647
714
|
|
|
648
715
|
### Small Token Records
|
|
@@ -662,9 +729,9 @@ accumulate at least two seconds.
|
|
|
662
729
|
|
|
663
730
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
664
731
|
|---|---:|---:|---:|---:|---:|---:|
|
|
665
|
-
| zrecord-zstd |
|
|
666
|
-
| zrecord-zstdict |
|
|
667
|
-
| zrecord-raw |
|
|
732
|
+
| zrecord-zstd | 989 MiB/s | 2012 MiB/s | 1030.3 | 0.34 ms | 193.2 MiB | 2.02x |
|
|
733
|
+
| zrecord-zstdict | 62 MiB/s | 2079 MiB/s | 1064.6 | 0.32 ms | 154.4 MiB | 2.53x |
|
|
734
|
+
| zrecord-raw | 4542 MiB/s | 6391 MiB/s | 3272.3 | 0.11 ms | 393.7 MiB | 0.99x |
|
|
668
735
|
| npy-mmap-raw | 1587 MiB/s | 9070 MiB/s | 4644.0 | 0.08 ms | 390.6 MiB | 1.00x |
|
|
669
736
|
| lmdb-raw | 639 MiB/s | 1104 MiB/s | 565.0 | 0.71 ms | 786.3 MiB | 0.50x |
|
|
670
737
|
| arrow-ipc-raw | 2068 MiB/s | 225 MiB/s | 115.0 | 2.81 ms | 390.8 MiB | 1.00x |
|
|
@@ -672,9 +739,9 @@ accumulate at least two seconds.
|
|
|
672
739
|
| arrayrecord-zstd | 127 MiB/s | 135 MiB/s | 69.4 | 4.45 ms | 201.3 MiB | 1.94x |
|
|
673
740
|
|
|
674
741
|
The contiguous NumPy baseline is strongest when the whole corpus is one fixed
|
|
675
|
-
typed matrix. Zrecord-raw reaches 3.
|
|
742
|
+
typed matrix. Zrecord-raw reaches 3.27 Mrecords/s while retaining independent
|
|
676
743
|
record semantics; the per-record zstd codecs halve disk and still return
|
|
677
|
-
1.
|
|
744
|
+
1.03–1.06 Mrecords/s. LMDB's B-tree/page overhead is visible in both throughput
|
|
678
745
|
and disk.
|
|
679
746
|
|
|
680
747
|
#### Variable Token Sequences
|
|
@@ -686,16 +753,16 @@ with exact `int32` values and original one-dimensional shapes.
|
|
|
686
753
|
|
|
687
754
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
688
755
|
|---|---:|---:|---:|---:|---:|---:|
|
|
689
|
-
| zrecord-zstd |
|
|
690
|
-
| zrecord-zstdict |
|
|
691
|
-
| zrecord-raw |
|
|
756
|
+
| zrecord-zstd | 287 MiB/s | 315 MiB/s | 594.6 | 0.60 ms | 67.4 MiB | 1.57x |
|
|
757
|
+
| zrecord-zstdict | 54 MiB/s | 332 MiB/s | 627.6 | 0.58 ms | 49.1 MiB | 2.16x |
|
|
758
|
+
| zrecord-raw | 536 MiB/s | 389 MiB/s | 734.9 | 0.50 ms | 110.5 MiB | 0.96x |
|
|
692
759
|
| lmdb-raw | 318 MiB/s | 113 MiB/s | 214.3 | 1.48 ms | 153.0 MiB | 0.69x |
|
|
693
760
|
| arrow-ipc-raw | 562 MiB/s | 44 MiB/s | 83.1 | 4.00 ms | 109.9 MiB | 0.96x |
|
|
694
761
|
| arrayrecord-raw | 288 MiB/s | 26 MiB/s | 49.3 | 7.39 ms | 118.8 MiB | 0.89x |
|
|
695
762
|
| arrayrecord-zstd | 62 MiB/s | 33 MiB/s | 61.5 | 5.58 ms | 76.2 MiB | 1.39x |
|
|
696
763
|
|
|
697
764
|
Here the record contract, not bulk byte bandwidth, is the useful scale.
|
|
698
|
-
Zrecord's three codecs return
|
|
765
|
+
Zrecord's three codecs return 595–735 krecords/s with 0.50–0.60 ms p95;
|
|
699
766
|
the dictionary gives the best disk ratio and is slightly ahead of plain zstd in
|
|
700
767
|
this pass.
|
|
701
768
|
|
|
@@ -742,8 +809,8 @@ and Grain reads ArrayRecord.
|
|
|
742
809
|
|
|
743
810
|
| loader | model | storage | batches/s | p95 | steady PSS | peak PSS | peak RSS |
|
|
744
811
|
|---|---|---|---:|---:|---:|---:|---:|
|
|
745
|
-
| **loaderx** | threads | zrecord-zstd |
|
|
746
|
-
| loaderx-raw | threads | zrecord-raw |
|
|
812
|
+
| **loaderx** | threads | zrecord-zstd | 171.5 | 12.69 ms | 984 MiB | 985 MiB | 988 MiB |
|
|
813
|
+
| loaderx-raw | threads | zrecord-raw | 200.0 | 12.50 ms | 993 MiB | 993 MiB | 996 MiB |
|
|
747
814
|
| torch | fork | npy-mmap-raw | 109.4 | 32.70 ms | 1783 MiB | 1889 MiB | 6396 MiB |
|
|
748
815
|
| torch-spawn | spawn | npy-mmap-raw | 111.6 | 30.69 ms | 2835 MiB | 2913 MiB | 4880 MiB |
|
|
749
816
|
| grain | processes | arrayrecord-zstd | 46.6 | 93.48 ms | 1847 MiB | 1946 MiB | 2065 MiB |
|
|
@@ -753,8 +820,8 @@ the transform threads overlap Python-side collation with the next gather. The
|
|
|
753
820
|
memory is the source, Zrecord container and bounded in-flight batches. loaderx prefetches in
|
|
754
821
|
threads inside one process, so workers share one interpreter, one NumPy runtime
|
|
755
822
|
and one set of gather buffers. With source geometry and entropy held constant,
|
|
756
|
-
raw is 1.
|
|
757
|
-
Torch spawn and 3.
|
|
823
|
+
raw is 1.17x compressed loaderx; compressed loaderx is 1.57x Torch fork, 1.54x
|
|
824
|
+
Torch spawn and 3.68x Grain, while raw is 1.83x, 1.79x and 4.29x faster.
|
|
758
825
|
Torch's aggregate RSS is high because
|
|
759
826
|
Linux fork mappings are counted repeatedly; it is not a total-memory ratio
|
|
760
827
|
against Zrecord's unaccounted page cache. The explicit `torch-spawn` row removes
|
|
@@ -828,8 +895,8 @@ format: in the current Dense structured-vision workload, plain zstd reaches
|
|
|
828
895
|
**Loader results combine architecture and storage.** loaderx uses threads and
|
|
829
896
|
never ends an epoch, so a step pays no IPC and never waits on an epoch boundary;
|
|
830
897
|
torch uses finite shuffled epochs, worker processes and shared-memory handoff.
|
|
831
|
-
Here compressed loaderx is 1.
|
|
832
|
-
raw loaderx is 1.
|
|
898
|
+
Here compressed loaderx is 1.57x Torch fork, 1.54x Torch spawn and 3.68x Grain;
|
|
899
|
+
raw loaderx is 1.83x, 1.79x and 4.29x faster, respectively.
|
|
833
900
|
Storage also differs per loader — each reads from what it was
|
|
834
901
|
built for — so the loader table is a different comparison from either store
|
|
835
902
|
table, not a rerun.
|
|
@@ -890,57 +957,13 @@ The loader rows used Numba 0.67.0, Torch 2.13.0 and Grain 0.2.18; all benchmark
|
|
|
890
957
|
are pinned in `scripts/requirements-bench.txt`. Temporary stores used the ordinary
|
|
891
958
|
disk-backed `/tmp` filesystem, not `/dev/shm`.
|
|
892
959
|
|
|
893
|
-
## Real-data verification
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
`.npy` outputs of the standard preprocessing pipeline were treated as ground
|
|
901
|
-
truth. This verification was not rerun with the synthetic benchmarks above;
|
|
902
|
-
its throughput is retained as a separate historical 12-core result.
|
|
903
|
-
|
|
904
|
-
**Correctness — the read path is bit-exact against the ground truth:**
|
|
905
|
-
|
|
906
|
-
* Full scan of all **228,356 records** (joint float32 + label int64, all four
|
|
907
|
-
splits) through `Dense`: byte-for-byte identical to the reference npy.
|
|
908
|
-
* A `DataLoader` over joint + label + an index stream, run under all three
|
|
909
|
-
sampler modes (`sequential`, `iid`, `cyclic`): every received batch is
|
|
910
|
-
bit-exact to the ground truth at its own declared indices, and the streams
|
|
911
|
-
stay index-aligned.
|
|
912
|
-
* Sampler semantics hold on real index spaces: `sequential` walks in order,
|
|
913
|
-
`cyclic` draws a full cycle without replacement, `iid` is deterministic per
|
|
914
|
-
seed.
|
|
915
|
-
|
|
916
|
-
**Storage — zstd on this data:**
|
|
917
|
-
|
|
918
|
-
| store | on disk | ratio |
|
|
919
|
-
|-----------------------|---------|-------|
|
|
920
|
-
| npy (raw float32) | 6.4 GB | 1.00x |
|
|
921
|
-
| zrecord raw | 6.86 GB | 1.00x |
|
|
922
|
-
| zrecord zstd | 0.79 GB | 8.66x |
|
|
923
|
-
| zrecord zstd_dict | 0.75 GB | 9.13x |
|
|
924
|
-
|
|
925
|
-
Sizes above are for one split (`xview/val`, 38,132 records); across all four
|
|
926
|
-
splits the zstd `joint` stores total 4.97 GB against 41 GB of raw npy (~8x).
|
|
927
|
-
|
|
928
|
-
**Throughput** (180 KB per record, warm page cache, 12 physical cores):
|
|
929
|
-
|
|
930
|
-
| path | throughput |
|
|
931
|
-
|-------------------------------------|-----------|
|
|
932
|
-
| random-batch gather, zstd store | 4.1–4.5 GiB/s |
|
|
933
|
-
| same, npy-mmap fancy indexing | 0.6–1.3 GiB/s |
|
|
934
|
-
| DataLoader, 4 prefetch threads | 5.7–6.6 GiB/s (123–144 batches/s) |
|
|
935
|
-
|
|
936
|
-
zstd decompression reads ~8x fewer bytes than raw storage, so the compressed
|
|
937
|
-
store gathers *faster* than the raw one (zstd 4587 MiB/s vs raw 1792 MiB/s on
|
|
938
|
-
the same split).
|
|
939
|
-
|
|
940
|
-
**The npy intermediate is optional.** Parse the skeleton files in parallel and
|
|
941
|
-
feed each fixed-shape ndarray produced by the parser directly to
|
|
942
|
-
`Dense.append`. This writes the store in one pass with no npy staging or
|
|
943
|
-
second read; zrecord bounds its compression working memory independently.
|
|
960
|
+
## Real-data verification
|
|
961
|
+
|
|
962
|
+
Dataset-specific preprocessing and verification remain in the
|
|
963
|
+
**[DataPipe](https://codeberg.org/eoelab/DataPipe)** repository. The built-in
|
|
964
|
+
converter covers standardized Hugging Face datasets; DataPipe handles sources
|
|
965
|
+
without a common remote protocol and implements derived modalities as loader
|
|
966
|
+
transforms without NumPy dump intermediates.
|
|
944
967
|
|
|
945
968
|
## Current Limitations
|
|
946
969
|
* Single-host only; multi-host training is not supported.
|
|
@@ -1055,9 +1078,9 @@ license to trust storage or the operating system: native code still validates
|
|
|
1055
1078
|
normal I/O behavior, basic malformed-store rejection and native memory safety; checks that only
|
|
1056
1079
|
defend against bypassing the public Python API do not belong in zrecord.
|
|
1057
1080
|
|
|
1058
|
-
1. `RecordEngine` stores N logically ordered records.
|
|
1059
|
-
|
|
1060
|
-
|
|
1081
|
+
1. `RecordEngine` stores N logically ordered records. Payload record `i` belongs
|
|
1082
|
+
to `data_{i % data_shards}.zr`; logical ID remains the stable append position
|
|
1083
|
+
and `RecordLoc[ID]` preserves its shard-local offset. Index and slice
|
|
1061
1084
|
operations are implemented as ordered gathers over those positions.
|
|
1062
1085
|
2. It hands the container layer a dense sequence space: records are exactly
|
|
1063
1086
|
`0..N-1`. Named streams are composed dynamically by a plain Python dict;
|
|
@@ -1120,24 +1143,33 @@ the authority for irreplaceable data. Keep authoritative source data and reprodu
|
|
|
1120
1143
|
build scripts; after an interrupted build, storage failure, incompatible implementation
|
|
1121
1144
|
change, or content change, rebuild a complete container at a new path.
|
|
1122
1145
|
|
|
1123
|
-
Native storage uses a fixed file set:
|
|
1146
|
+
Native storage uses one metadata file and a create-time-fixed payload file set:
|
|
1124
1147
|
```
|
|
1125
1148
|
store/
|
|
1126
|
-
├── meta.zr 4096-byte
|
|
1127
|
-
├──
|
|
1149
|
+
├── meta.zr 4096-byte Header/schema/tails page + RecordLoc table
|
|
1150
|
+
├── data_0.zr payload records where ID % data_shards == 0
|
|
1151
|
+
├── ...
|
|
1152
|
+
├── data_{N-1}.zr final static payload shard
|
|
1128
1153
|
└── dict.zr zstd dictionary (only in dict stores)
|
|
1129
1154
|
```
|
|
1130
1155
|
|
|
1156
|
+
`data_shards` is a write-performance parameter in `1..255`, fixed by `create`
|
|
1157
|
+
and recovered automatically by `open`. The default is four; practical values are
|
|
1158
|
+
usually 2, 4, 8 or 16, near the writer lane count. More files spread positional
|
|
1159
|
+
writes across payload inodes but consume one descriptor each. This physical
|
|
1160
|
+
striping does not change record IDs, order, codec, or read results.
|
|
1161
|
+
|
|
1131
1162
|
### Metadata (meta.zr)
|
|
1132
1163
|
|
|
1133
1164
|
Files are read and written **positionally** — pread/pwrite at computed offsets,
|
|
1134
1165
|
no mmap. `meta.zr` starts with one fixed 4096-byte static page: a naturally
|
|
1135
|
-
aligned
|
|
1166
|
+
aligned 16-byte Header, 255 shard-local u64 tails at bytes 16..2055, then up to
|
|
1167
|
+
2040 bytes of opaque MsgPack schema at bytes 2056..4095.
|
|
1136
1168
|
An array of 16-byte `RecordLoc`s starts at offset 4096. Record `i` is one
|
|
1137
1169
|
pread/pwrite at `4096 + i * 16`; there is no variable table base, segment
|
|
1138
1170
|
mapping, or rollover fd table.
|
|
1139
1171
|
|
|
1140
|
-
**1. Python schema** — bytes `
|
|
1172
|
+
**1. Python schema** — bytes `2056..2056+schema_length` are exactly one immutable
|
|
1141
1173
|
MsgPack object. Both stores contain ``dtype`` and ``ndim``; Dense additionally
|
|
1142
1174
|
contains ``item_shape``. Native create persists these bytes together with
|
|
1143
1175
|
the physical container but does not decode them. Open acquires the native lifetime
|
|
@@ -1147,30 +1179,36 @@ dtype/item_shape and passed to the native handle as runtime geometry; it is not
|
|
|
1147
1179
|
independently persisted as a second authority. There is no format version or
|
|
1148
1180
|
legacy kind dispatch.
|
|
1149
1181
|
|
|
1150
|
-
**2. Physical header** — the first
|
|
1182
|
+
**2. Physical header** — the first 16 bytes of `meta.zr`. The format
|
|
1151
1183
|
deliberately carries no payload or metadata checksum.
|
|
1152
1184
|
|
|
1153
1185
|
* `codec` is the store's one compression method, stamped at creation and
|
|
1154
1186
|
immutable — there is no per-record tag anywhere.
|
|
1155
1187
|
* `length` (u64) is the physical record count; it equals logical length for both
|
|
1156
1188
|
dense stores and inline ragged stores.
|
|
1157
|
-
* `
|
|
1158
|
-
|
|
1159
|
-
|
|
1189
|
+
* `schema_length` (u16) is the occupied prefix of the static schema area and
|
|
1190
|
+
must be in `1..2040`.
|
|
1191
|
+
* `data_shards` (u8) is the static payload file count and must be in `1..255`.
|
|
1160
1192
|
|
|
1161
1193
|
```zig
|
|
1162
1194
|
const Codec = enum(u8) { raw = 0, zstd = 1, zstdict = 2, _ };
|
|
1163
1195
|
const Header = extern struct {
|
|
1164
1196
|
length: u64,
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
reserved: [11]u8,
|
|
1197
|
+
schema_length: u16,
|
|
1198
|
+
data_shards: u8,
|
|
1168
1199
|
codec: u8,
|
|
1200
|
+
reserved: [4]u8,
|
|
1169
1201
|
};
|
|
1170
1202
|
```
|
|
1171
1203
|
|
|
1172
|
-
**3.
|
|
1173
|
-
`
|
|
1204
|
+
**3. Shard frontiers** — tail slot `s` at `16 + s * 8` is the committed
|
|
1205
|
+
end of `data_s.zr`. Unused slots among the 255 fixed u64 entries are zero.
|
|
1206
|
+
Open requires every data file to be at least its persisted tail; locations may
|
|
1207
|
+
not cross that shard-local frontier.
|
|
1208
|
+
|
|
1209
|
+
**4. Record table** — contiguous 16-byte entries start at offset 4096 in
|
|
1210
|
+
`meta.zr` and grow as location windows are written. `offset` is local to
|
|
1211
|
+
`data_{ID % data_shards}.zr`;
|
|
1174
1212
|
`phys_length`/`logic_length` are the stored and original sizes. The
|
|
1175
1213
|
codec is not here: it is the header's, so a record is stored exactly the way the
|
|
1176
1214
|
store is declared.
|
|
@@ -1185,9 +1223,9 @@ const RecordLoc = extern struct {
|
|
|
1185
1223
|
|
|
1186
1224
|
There is no liveness flag. Every entry below `length` is a record.
|
|
1187
1225
|
|
|
1188
|
-
**
|
|
1189
|
-
|
|
1190
|
-
|
|
1226
|
+
**5. No fixed record-count cap.** The table and payload streams grow naturally.
|
|
1227
|
+
The practical bounds are the u64 count, supported positional file offsets,
|
|
1228
|
+
2 GiB per record, descriptor budget, and disk.
|
|
1191
1229
|
|
|
1192
1230
|
## Executor
|
|
1193
1231
|
|
|
@@ -1202,34 +1240,36 @@ while `Ragged` supplies offsets for its shape-prefixed records.
|
|
|
1202
1240
|
The private ABI turns those inputs into compile-time record sources and
|
|
1203
1241
|
destinations; the engine has one append and one gather operation. Its shared
|
|
1204
1242
|
opaque handle remains private and carries no typed-store geometry.
|
|
1205
|
-
*
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
the
|
|
1213
|
-
|
|
1214
|
-
|
|
1243
|
+
* Append validates the complete call before physical I/O, then processes bounded
|
|
1244
|
+
logical windows. Within a window, record positions are divided by
|
|
1245
|
+
`ID % data_shards` and planned as bounded shard-local chunks. Fixed Executor
|
|
1246
|
+
lanes dynamically claim those chunks, encode or pack them, briefly lock only
|
|
1247
|
+
the selected shard's tail reservation, then issue positional payload writes
|
|
1248
|
+
directly. Multiple lanes may write non-overlapping ranges of one shard; the
|
|
1249
|
+
static file set spreads that pressure across inodes without limiting codec
|
|
1250
|
+
concurrency to the shard count. Workers fill disjoint entries in one
|
|
1251
|
+
contiguous location buffer. A payload barrier precedes one contiguous
|
|
1252
|
+
`meta.zr` location write. All windows must succeed before the in-process
|
|
1253
|
+
length and per-shard tails advance. Writer `close()` truncates each data file
|
|
1254
|
+
to its committed tail and publishes the 4096-byte static page.
|
|
1255
|
+
* Compressed shard tasks lease process-bounded `ExecutionSlot` scratch and write
|
|
1256
|
+
independent frames in bounded subchunks. Python budgets the process-wide executor at three quarters
|
|
1215
1257
|
of the logical CPUs available to the process, leaving headroom for packing,
|
|
1216
1258
|
transforms, and the caller without encoding a platform-specific thread count.
|
|
1217
1259
|
Each producer configures its CCtx or shared immutable CDict once, then starts
|
|
1218
1260
|
every independent record frame with `ZSTD_compress2`.
|
|
1219
|
-
* Raw
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
`std.Io` handles short writes and maps the same positional operation to POSIX
|
|
1226
|
-
and Windows implementations.
|
|
1261
|
+
* Raw records for one shard are strided in the source. Each shard task packs a
|
|
1262
|
+
bounded subchunk into its reusable scratch and performs one contiguous
|
|
1263
|
+
positional write; a single record larger than the normal subchunk budget is
|
|
1264
|
+
written directly. There is no per-record syscall or platform-specific vectored
|
|
1265
|
+
path. The extra memory copy is the deliberate cost paid to remove concentrated
|
|
1266
|
+
single-inode writes.
|
|
1227
1267
|
**2. Read.** Fill the destination memory concurrently, in place from the Python
|
|
1228
1268
|
side (executed on async threads).
|
|
1229
1269
|
* Committed records are immutable and `length` is published through an atomic.
|
|
1230
|
-
The fixed
|
|
1231
|
-
|
|
1232
|
-
|
|
1270
|
+
The fixed metadata and static data handles require no rollover synchronization.
|
|
1271
|
+
* Every record first selects `data_{ID % data_shards}.zr`, then reads the
|
|
1272
|
+
shard-local offset its table entry records — the record table
|
|
1233
1273
|
is addressed by pure arithmetic, so random access is one pread for the
|
|
1234
1274
|
location and one for the bytes, with no batching assumptions about layout.
|
|
1235
1275
|
Each lane reads one location and immediately reads/decompresses that record;
|
|
@@ -1241,8 +1281,9 @@ budget; lanes for which the runtime cannot reserve concurrency run inline on the
|
|
|
1241
1281
|
calling thread. Python configures the process-level budget as
|
|
1242
1282
|
`max(physical cores, logical cores * 3 / 4)`, using platform topology where
|
|
1243
1283
|
available.
|
|
1244
|
-
*
|
|
1245
|
-
|
|
1284
|
+
* Gather lanes receive contiguous request blocks. Append lanes dynamically claim
|
|
1285
|
+
bounded chunks of strided shard ranges; short per-shard reservation locks keep
|
|
1286
|
+
offsets disjoint while positional writes remain concurrent.
|
|
1246
1287
|
* Each lane creates one zstd context (`ZSTD_CCtx` to write, `ZSTD_DCtx` to
|
|
1247
1288
|
read) and reuses it across every record it handles, rather than paying that
|
|
1248
1289
|
setup per record. The dictionary (`ZSTD_CDict`/`ZSTD_DDict`) is immutable, so
|
|
@@ -1252,10 +1293,10 @@ available.
|
|
|
1252
1293
|
|
|
1253
1294
|
**4. File access.**
|
|
1254
1295
|
* Metadata: one naturally growing `meta.zr`, containing the fixed Header/schema
|
|
1255
|
-
page and loc table.
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
sparse
|
|
1296
|
+
/tails page and loc table. It is intentionally not sharded because measured
|
|
1297
|
+
write pressure is in payload I/O; one coordinator writes each loc window.
|
|
1298
|
+
* Payload: a static list of naturally growing `data_<shard>.zr` files, accessed through
|
|
1299
|
+
`readPositionalAll`/`writePositionalAll`. No path depends on sparse files.
|
|
1259
1300
|
|
|
1260
1301
|
**Execution model.** Opened readers are immutable, so calls on the same reader may
|
|
1261
1302
|
gather concurrently. Creator appends are synchronous and native Storage
|