loaderx 2.0.7__tar.gz → 2.3.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-2.0.7 → loaderx-2.3.3}/MANIFEST.in +1 -1
- {loaderx-2.0.7/loaderx.egg-info → loaderx-2.3.3}/PKG-INFO +151 -118
- {loaderx-2.0.7 → loaderx-2.3.3}/README.md +150 -117
- {loaderx-2.0.7 → loaderx-2.3.3}/build.zig +5 -3
- {loaderx-2.0.7 → loaderx-2.3.3}/build.zig.zon +1 -1
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/__init__.py +1 -1
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/_store.py +19 -13
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/utils.py +34 -3
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/zrecord.py +108 -67
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/zsampler.py +5 -4
- {loaderx-2.0.7 → loaderx-2.3.3/loaderx.egg-info}/PKG-INFO +151 -118
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/SOURCES.txt +3 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/_bench_common.py +5 -3
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/bench_dense.py +4 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/bench_ragged.py +33 -28
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/test_loaderx.py +213 -95
- {loaderx-2.0.7 → loaderx-2.3.3}/src/record/engine.zig +256 -296
- {loaderx-2.0.7 → loaderx-2.3.3}/src/record/executor.zig +12 -16
- loaderx-2.3.3/src/record/storage.zig +411 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/src/store.zig +18 -35
- {loaderx-2.0.7 → loaderx-2.3.3}/src/zsampler.zig +19 -28
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/bits.h +92 -87
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/bitstream.h +26 -29
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/compiler.h +36 -22
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/cpu.h +1 -1
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/debug.h +0 -9
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/error_private.c +1 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/error_private.h +0 -10
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/fse.h +2 -17
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/fse_decompress.c +2 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/huf.h +0 -9
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/mem.h +7 -11
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/pool.h +0 -9
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/portability_macros.h +22 -9
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/threading.h +0 -8
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/xxhash.h +93 -19
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_deps.h +12 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_internal.h +1 -69
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_trace.h +5 -12
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/hist.c +10 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/hist.h +7 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress.c +1057 -367
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_internal.h +227 -125
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_literals.c +1 -1
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_sequences.c +7 -7
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_sequences.h +7 -6
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_superblock.c +17 -17
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_cwksp.h +41 -24
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_double_fast.c +58 -50
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_double_fast.h +4 -12
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_fast.c +91 -74
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_fast.h +4 -12
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_lazy.c +64 -64
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_lazy.h +30 -39
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm.c +48 -33
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm.h +6 -14
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_opt.c +55 -51
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_opt.h +8 -16
- loaderx-2.3.3/vendor/zstd/lib/compress/zstd_preSplit.c +238 -0
- loaderx-2.3.3/vendor/zstd/lib/compress/zstd_preSplit.h +33 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstdmt_compress.c +134 -93
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstdmt_compress.h +4 -15
- loaderx-2.3.3/vendor/zstd/lib/decompress/huf_decompress_amd64.S +602 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress.c +14 -11
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_block.c +6 -12
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_internal.h +5 -5
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/cover.c +60 -19
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/divsufsort.h +0 -10
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/zdict.c +2 -2
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/zdict.h +15 -8
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/zstd.h +241 -132
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/zstd_errors.h +1 -8
- loaderx-2.0.7/src/record/storage.zig +0 -341
- {loaderx-2.0.7 → loaderx-2.3.3}/LICENSE +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/_lib.py +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/dataloader.py +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/dependency_links.txt +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/requires.txt +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/top_level.txt +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/pyproject.toml +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/bench.py +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/build_wheels.py +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/prepare_tokens.py +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/scripts/requirements-bench.txt +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/setup.cfg +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/setup.py +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/src/zstd/c.zig +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/COPYING +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/LICENSE +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/allocations.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/debug.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/entropy_common.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/pool.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/threading.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/xxhash.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_common.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/clevels.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/fse_compress.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/huf_compress.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_literals.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_superblock.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm_geartab.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/huf_decompress.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_ddict.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_ddict.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_block.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/cover.h +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/divsufsort.c +0 -0
- {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/fastcover.c +0 -0
|
@@ -8,7 +8,7 @@ recursive-include scripts *.py
|
|
|
8
8
|
include scripts/requirements-bench.txt
|
|
9
9
|
|
|
10
10
|
# Vendored zstd: the sdist rebuilds with Zig, so it needs the C sources.
|
|
11
|
-
recursive-include vendor *.c *.h
|
|
11
|
+
recursive-include vendor *.c *.h *.S
|
|
12
12
|
include vendor/zstd/LICENSE vendor/zstd/COPYING
|
|
13
13
|
|
|
14
14
|
exclude loaderx/lib/lib_here
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loaderx
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.3
|
|
4
4
|
Summary: Rebuildable high-performance ordered record containers
|
|
5
5
|
Author-email: Ben0i0d <ben0i0d@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -121,25 +121,26 @@ reader. Open the result with ``Dense.open``:
|
|
|
121
121
|
batch = ds[:]
|
|
122
122
|
ds.close()
|
|
123
123
|
|
|
124
|
-
Python defines the exact record schema: ``dtype``
|
|
125
|
-
|
|
124
|
+
Python defines the exact record schema: both geometries store ``dtype`` and
|
|
125
|
+
``ndim``; Dense additionally stores ``item_shape``. The MsgPack bytes live opaquely in the
|
|
126
126
|
static page at the front of ``meta.zr``; Zig persists them but never interprets
|
|
127
127
|
them. The schema accepts no user metadata. Python selects the geometry and gives
|
|
128
128
|
the private native engine only the runtime record boundaries it needs. Each
|
|
129
|
-
Ragged record carries
|
|
129
|
+
Ragged record carries exactly ``ndim`` inline little-endian u64 dimensions.
|
|
130
130
|
One native physical
|
|
131
131
|
engine consumes the trusted Dense stride or Ragged offsets. Append inputs are
|
|
132
132
|
strictly NumPy arrays: Dense takes one batched ndarray and Ragged takes an
|
|
133
133
|
iterable of ndarrays. Raw bytes and pre-encoded images are made explicit with
|
|
134
134
|
``np.frombuffer(raw, dtype=np.uint8)`` and stored in a
|
|
135
|
-
``Ragged.create(path, dtype=np.uint8)`` rather than creating a second
|
|
135
|
+
``Ragged.create(path, dtype=np.uint8, ndim=1)`` rather than creating a second
|
|
136
136
|
public storage API.
|
|
137
137
|
|
|
138
138
|
### Records
|
|
139
139
|
One persistent format, two native execution contracts. ``Dense`` is the **dense** contract
|
|
140
140
|
where every record is exactly one row of the recorded ``item_shape``; reads
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
allocate a fixed-stride destination whose batch shape follows from the schema.
|
|
142
|
+
Physical records still use the shared ``RecordLoc[logical_id] -> payload``
|
|
143
|
+
pipeline, so compressed completion order never becomes a second Dense layout:
|
|
143
144
|
|
|
144
145
|
```python
|
|
145
146
|
import numpy as np
|
|
@@ -160,11 +161,11 @@ the same way.
|
|
|
160
161
|
|
|
161
162
|
``Ragged`` is the **ragged** contract for variable-length records. It is a
|
|
162
163
|
separate contract: :class:`Ragged` hands back a list of arrays, so a
|
|
163
|
-
loader never has to carry ``row_splits`` around. ``dtype``
|
|
164
|
-
explicit; each record keeps its **own
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
loader never has to carry ``row_splits`` around. ``dtype`` and ``ndim`` are
|
|
165
|
+
unified and explicit; each record keeps its **own dimension lengths**, recorded
|
|
166
|
+
per record and restored exactly on read. Every axis may vary, but every record
|
|
167
|
+
has the schema rank; nothing is inferred from the source. Ragged requires at
|
|
168
|
+
least one axis; scalar records use Dense with ``item_shape=()``.
|
|
168
169
|
zero-byte arrays are rejected because physical records are nonempty. Densifying a list into a dense
|
|
169
170
|
batch is the model's call — a plain numpy loop, wherever you need it:
|
|
170
171
|
|
|
@@ -172,8 +173,8 @@ batch is the model's call — a plain numpy loop, wherever you need it:
|
|
|
172
173
|
from loaderx.zrecord import Ragged
|
|
173
174
|
|
|
174
175
|
seqs = [np.arange(L, dtype=np.int32) for L in (3, 1, 4, 1, 5)]
|
|
175
|
-
with Ragged.create('tokens', np.int32) as rs:
|
|
176
|
-
rs.append(seqs) # dtype
|
|
176
|
+
with Ragged.create('tokens', np.int32, ndim=1) as rs:
|
|
177
|
+
rs.append(seqs) # dtype/rank fixed; lengths remain per-record
|
|
177
178
|
rs = Ragged.open('tokens')
|
|
178
179
|
|
|
179
180
|
records = rs[0, 2, 4] # list of ndarray — one per record, exact shapes
|
|
@@ -268,12 +269,12 @@ buffer, then completes the native append before returning. Nothing is inferred.
|
|
|
268
269
|
```python
|
|
269
270
|
from loaderx.zrecord import Dense, Ragged
|
|
270
271
|
|
|
271
|
-
ds = Dense.create('mnist/x', dtype=np.uint8, item_shape=(28, 28))
|
|
272
|
+
ds = Dense.create('mnist/x', dtype=np.uint8, item_shape=(28, 28), data_shards=4)
|
|
272
273
|
ds.append(images[i:i + 1024]) # synchronous native batch; returns None
|
|
273
274
|
ds.append(single_image[None]) # one sample is batch_size 1 — add the axis yourself
|
|
274
275
|
ds.close() # publish before opening
|
|
275
276
|
|
|
276
|
-
tok = Ragged.create('tokens', dtype=np.int32)
|
|
277
|
+
tok = Ragged.create('tokens', dtype=np.int32, ndim=1)
|
|
277
278
|
tok.append([seq_a, seq_b, seq_c])
|
|
278
279
|
tok.close()
|
|
279
280
|
|
|
@@ -281,6 +282,11 @@ with Dense.open('mnist/x') as ds:
|
|
|
281
282
|
first_four = ds[:4] # opened containers are read-only
|
|
282
283
|
```
|
|
283
284
|
|
|
285
|
+
`data_shards` is the keyword-only write-parallelism setting. It is persisted in
|
|
286
|
+
the native Header, must be in `1..255`, and defaults to four. Values near the
|
|
287
|
+
writer lane count spread payload writes across independent files; opening a
|
|
288
|
+
completed container discovers the value automatically.
|
|
289
|
+
|
|
284
290
|
Dense and Ragged append both report native errors in the current call and return
|
|
285
291
|
only after accepting the batch. A writer cannot be read, and a reader cannot be
|
|
286
292
|
appended to. ``close()`` on a writer publishes the container Header;
|
|
@@ -288,12 +294,14 @@ appended to. ``close()`` on a writer publishes the container Header;
|
|
|
288
294
|
``with`` for scoped lifetimes. Content is never changed in place: rerun the
|
|
289
295
|
authoritative build at a new path, validate it, then switch consumers to it.
|
|
290
296
|
|
|
291
|
-
The exact schema is declared at creation and encoded by Python as MsgPack.
|
|
292
|
-
|
|
293
|
-
``
|
|
297
|
+
The exact schema is declared at creation and encoded by Python as MsgPack. Both
|
|
298
|
+
schemas contain ``dtype`` and ``ndim``; Dense additionally contains
|
|
299
|
+
``item_shape`` and requires its length to equal ``ndim``. Ragged requires every
|
|
300
|
+
appended record to have that rank, while every dimension length may vary.
|
|
301
|
+
Structured, subarray, object, metadata-bearing, and zero-itemsize dtypes are not
|
|
294
302
|
supported: their semantics do not round-trip through one canonical NumPy dtype
|
|
295
|
-
string. The encoded schema has
|
|
296
|
-
metadata page. Ragged schema size is
|
|
303
|
+
string. The encoded schema has 2040 bytes available in the fixed 4096-byte
|
|
304
|
+
metadata page. Ragged schema size is fixed; Dense schema size grows
|
|
297
305
|
only with the integer ``item_shape``, so the physical limit is far above any
|
|
298
306
|
practical NumPy array rank.
|
|
299
307
|
Unexpected fields are rejected. ``append`` validates dtype and shape in Python,
|
|
@@ -310,7 +318,7 @@ published without one.
|
|
|
310
318
|
for general-purpose compression — it is fast and the default.
|
|
311
319
|
|
|
312
320
|
`"zstd_dict"` trains a shared dictionary on a sample of the data before writing
|
|
313
|
-
any record, then compresses every record against it at level
|
|
321
|
+
any record, then compresses every record against it at level 15. The dictionary
|
|
314
322
|
captures structure shared across records that per-record compression cannot see —
|
|
315
323
|
a large win for many small, similar records (image tiles, token sequences).
|
|
316
324
|
|
|
@@ -353,7 +361,8 @@ from loaderx.zrecord import Dense, Ragged
|
|
|
353
361
|
d = train_dict(settled_array, tier="balanced")
|
|
354
362
|
|
|
355
363
|
# then any new store can install it and append explicitly
|
|
356
|
-
with Ragged.create('tokens', np.int32,
|
|
364
|
+
with Ragged.create('tokens', np.int32, ndim=1,
|
|
365
|
+
codec='zstd_dict', dict_bytes=d) as ds:
|
|
357
366
|
ds.append(token_generator)
|
|
358
367
|
with Dense.create('data', data.dtype, data.shape[1:],
|
|
359
368
|
codec='zstd_dict', dict_bytes=d) as ds:
|
|
@@ -377,7 +386,8 @@ with Dense.open("src") as s, \
|
|
|
377
386
|
```
|
|
378
387
|
|
|
379
388
|
``Ragged`` is the same shape: ``s[:]`` returns ``list[np.ndarray]``, which
|
|
380
|
-
is exactly its append input
|
|
389
|
+
is exactly its append input; a destination is created with ``s.dtype`` and
|
|
390
|
+
``s.ndim``. The
|
|
381
391
|
native compression path bounds its own working memory; there is no public chunk
|
|
382
392
|
parameter. ``dst`` must not already hold a store.
|
|
383
393
|
|
|
@@ -403,7 +413,7 @@ with Dense.create(root + "/joint", joint.dtype, joint.shape[1:]) as s:
|
|
|
403
413
|
s.append(joint)
|
|
404
414
|
with Dense.create(root + "/label", label.dtype, label.shape[1:]) as s:
|
|
405
415
|
s.append(label)
|
|
406
|
-
with Ragged.create(root + "/token", np.int32) as s:
|
|
416
|
+
with Ragged.create(root + "/token", np.int32, ndim=1) as s:
|
|
407
417
|
s.append(seqs)
|
|
408
418
|
|
|
409
419
|
streams = {
|
|
@@ -496,9 +506,12 @@ so CFFI, NumPy allocation, and Ragged list/shape reconstruction are timed.
|
|
|
496
506
|
|
|
497
507
|
### Methodology
|
|
498
508
|
|
|
499
|
-
The
|
|
500
|
-
|
|
501
|
-
|
|
509
|
+
The comparison matrix began as one complete run on a warm page cache. All
|
|
510
|
+
Zrecord Store rows and the two loaderx identity rows were refreshed on 2.3.0
|
|
511
|
+
with the default `data_shards=4`. Unchanged
|
|
512
|
+
optional Store backends, sampler rows and external loader rows remain from the
|
|
513
|
+
immediately preceding 2.0.9 run. These are not three-run medians: an unexpected result is traced
|
|
514
|
+
separately instead of being hidden by repeated aggregation. Within each store workload every
|
|
502
515
|
backend receives identical source records. Correctness and timing use independent
|
|
503
516
|
deterministic `zsampler` IID streams. Loader backends receive
|
|
504
517
|
the same source and seed but use their own shipped samplers, so their exact
|
|
@@ -556,8 +569,8 @@ threads):
|
|
|
556
569
|
| NUMA | 1 node |
|
|
557
570
|
| memory | 31 GiB (not limited by cgroup) |
|
|
558
571
|
| shared memory | 16 GiB `/dev/shm` |
|
|
559
|
-
| OS | Debian GNU/Linux forky/sid, kernel 7.1.
|
|
560
|
-
| python | CPython 3.14.7 (standard GIL build), numpy 2.
|
|
572
|
+
| OS | Debian GNU/Linux forky/sid, kernel 7.1.8+deb13-amd64, x86_64 |
|
|
573
|
+
| python | CPython 3.14.7 (standard GIL build), numpy 2.4.6 |
|
|
561
574
|
|
|
562
575
|
The benchmark process sees all 24 threads and is not memory-limited by cgroup.
|
|
563
576
|
The 16 GiB shared-memory mount accommodates the four-worker, 36.8 MiB-batch torch
|
|
@@ -575,9 +588,9 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
|
|
|
575
588
|
|
|
576
589
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
577
590
|
|---|---:|---:|---:|---:|---:|---:|
|
|
578
|
-
| zrecord-zstd |
|
|
579
|
-
| zrecord-zstdict |
|
|
580
|
-
| zrecord-raw |
|
|
591
|
+
| zrecord-zstd | 7172 MiB/s | 11081 MiB/s | 77.2 | 4.38 ms | 24.4 MiB | 14.69x |
|
|
592
|
+
| zrecord-zstdict | 104 MiB/s | 12042 MiB/s | 83.9 | 4.09 ms | 16.2 MiB | 22.10x |
|
|
593
|
+
| zrecord-raw | 5721 MiB/s | 14457 MiB/s | 100.7 | 3.16 ms | 358.9 MiB | 1.00x |
|
|
581
594
|
| npy-mmap-raw | 2004 MiB/s | 4749 MiB/s | 33.1 | 11.08 ms | 358.9 MiB | 1.00x |
|
|
582
595
|
| hdf5-raw | 2350 MiB/s | 1836 MiB/s | 12.8 | 29.68 ms | 359.0 MiB | 1.00x |
|
|
583
596
|
| hdf5-gzip | 277 MiB/s | 657 MiB/s | 4.6 | 62.53 ms | 26.1 MiB | 13.73x |
|
|
@@ -591,8 +604,8 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
|
|
|
591
604
|
| tiledb-raw | 743 MiB/s | 630 MiB/s | 4.4 | 66.01 ms | 359.0 MiB | 1.00x |
|
|
592
605
|
| tiledb-zstd | 1498 MiB/s | 1503 MiB/s | 10.5 | 27.82 ms | 26.9 MiB | 13.36x |
|
|
593
606
|
|
|
594
|
-
At 147 KiB per record, Zrecord-raw reaches 14.
|
|
595
|
-
plain zstd gathers at 10.
|
|
607
|
+
At 147 KiB per record, Zrecord-raw reaches 14.1 GiB/s and is 3.0x npy-mmap-raw;
|
|
608
|
+
plain zstd gathers at 10.8 GiB/s while reducing the corpus 14.69x. LMDB and Arrow
|
|
596
609
|
IPC are competitive raw record
|
|
597
610
|
stores, while codecs tied to whole IPC batches or Parquet row groups pay read
|
|
598
611
|
amplification on random gathers. Dense demonstrates that
|
|
@@ -610,9 +623,9 @@ list or a one-dimensional variable-length abstraction is not enough.
|
|
|
610
623
|
|
|
611
624
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
612
625
|
|---|---:|---:|---:|---:|---:|---:|
|
|
613
|
-
| zrecord-zstd |
|
|
614
|
-
| zrecord-zstdict |
|
|
615
|
-
| zrecord-raw |
|
|
626
|
+
| zrecord-zstd | 2220 MiB/s | 9444 MiB/s | 56.7 | 5.67 ms | 26.9 MiB | 15.48x |
|
|
627
|
+
| zrecord-zstdict | 102 MiB/s | 10345 MiB/s | 62.1 | 5.15 ms | 18.2 MiB | 22.97x |
|
|
628
|
+
| zrecord-raw | 2171 MiB/s | 12356 MiB/s | 74.2 | 4.22 ms | 417.2 MiB | 1.00x |
|
|
616
629
|
| hdf5-raw | 1451 MiB/s | 1059 MiB/s | 6.4 | 44.91 ms | 418.0 MiB | 1.00x |
|
|
617
630
|
| hdf5-gzip | 250 MiB/s | 126 MiB/s | 0.7 | 359.23 ms | 29.9 MiB | 13.94x |
|
|
618
631
|
| lmdb-raw | 1717 MiB/s | 7269 MiB/s | 43.6 | 7.93 ms | 422.2 MiB | 0.99x |
|
|
@@ -625,16 +638,16 @@ list or a one-dimensional variable-length abstraction is not enough.
|
|
|
625
638
|
| tiledb-raw | 392 MiB/s | 48 MiB/s | 0.3 | 941.09 ms | 417.2 MiB | 1.00x |
|
|
626
639
|
| tiledb-zstd | 675 MiB/s | 138 MiB/s | 0.8 | 328.05 ms | 27.0 MiB | 15.46x |
|
|
627
640
|
|
|
628
|
-
Zrecord-raw is 1.
|
|
629
|
-
delivers 9.
|
|
641
|
+
Zrecord-raw is 1.7x LMDB and 3.1x Arrow IPC in logical gather. Zrecord-zstd
|
|
642
|
+
delivers 9.2 GiB/s of
|
|
630
643
|
logical payload while reducing the corpus to 26.9 MiB. HDF5, Arrow IPC, Parquet,
|
|
631
644
|
ArrayRecord and TileDB
|
|
632
645
|
show the same framework/codec tradeoffs in both tables; compressed batch, chunk
|
|
633
646
|
and row-group formats pay read amplification on random records.
|
|
634
647
|
|
|
635
648
|
The shared generator makes compression ratios directly comparable across
|
|
636
|
-
contracts: Zrecord zstd is 14.69x Dense versus 15.
|
|
637
|
-
22.
|
|
649
|
+
contracts: Zrecord zstd is 14.69x Dense versus 15.48x Ragged, and zstdict is
|
|
650
|
+
22.10x versus 22.97x. The remaining difference comes from the H/W distribution
|
|
638
651
|
and Ragged shape metadata, not a different image entropy model.
|
|
639
652
|
|
|
640
653
|
### Small Token Records
|
|
@@ -654,19 +667,19 @@ accumulate at least two seconds.
|
|
|
654
667
|
|
|
655
668
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
656
669
|
|---|---:|---:|---:|---:|---:|---:|
|
|
657
|
-
| zrecord-zstd |
|
|
658
|
-
| zrecord-zstdict |
|
|
659
|
-
| zrecord-raw |
|
|
660
|
-
| npy-mmap-raw |
|
|
670
|
+
| zrecord-zstd | 989 MiB/s | 2012 MiB/s | 1030.3 | 0.34 ms | 193.2 MiB | 2.02x |
|
|
671
|
+
| zrecord-zstdict | 62 MiB/s | 2079 MiB/s | 1064.6 | 0.32 ms | 154.4 MiB | 2.53x |
|
|
672
|
+
| zrecord-raw | 4542 MiB/s | 6391 MiB/s | 3272.3 | 0.11 ms | 393.7 MiB | 0.99x |
|
|
673
|
+
| npy-mmap-raw | 1587 MiB/s | 9070 MiB/s | 4644.0 | 0.08 ms | 390.6 MiB | 1.00x |
|
|
661
674
|
| lmdb-raw | 639 MiB/s | 1104 MiB/s | 565.0 | 0.71 ms | 786.3 MiB | 0.50x |
|
|
662
675
|
| arrow-ipc-raw | 2068 MiB/s | 225 MiB/s | 115.0 | 2.81 ms | 390.8 MiB | 1.00x |
|
|
663
676
|
| arrayrecord-raw | 807 MiB/s | 150 MiB/s | 76.7 | 4.99 ms | 401.4 MiB | 0.97x |
|
|
664
677
|
| arrayrecord-zstd | 127 MiB/s | 135 MiB/s | 69.4 | 4.45 ms | 201.3 MiB | 1.94x |
|
|
665
678
|
|
|
666
679
|
The contiguous NumPy baseline is strongest when the whole corpus is one fixed
|
|
667
|
-
typed matrix. Zrecord-raw reaches 3.
|
|
680
|
+
typed matrix. Zrecord-raw reaches 3.27 Mrecords/s while retaining independent
|
|
668
681
|
record semantics; the per-record zstd codecs halve disk and still return
|
|
669
|
-
1.
|
|
682
|
+
1.03–1.06 Mrecords/s. LMDB's B-tree/page overhead is visible in both throughput
|
|
670
683
|
and disk.
|
|
671
684
|
|
|
672
685
|
#### Variable Token Sequences
|
|
@@ -678,16 +691,16 @@ with exact `int32` values and original one-dimensional shapes.
|
|
|
678
691
|
|
|
679
692
|
| backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
|
|
680
693
|
|---|---:|---:|---:|---:|---:|---:|
|
|
681
|
-
| zrecord-zstd |
|
|
682
|
-
| zrecord-zstdict |
|
|
683
|
-
| zrecord-raw |
|
|
694
|
+
| zrecord-zstd | 287 MiB/s | 315 MiB/s | 594.6 | 0.60 ms | 67.4 MiB | 1.57x |
|
|
695
|
+
| zrecord-zstdict | 54 MiB/s | 332 MiB/s | 627.6 | 0.58 ms | 49.1 MiB | 2.16x |
|
|
696
|
+
| zrecord-raw | 536 MiB/s | 389 MiB/s | 734.9 | 0.50 ms | 110.5 MiB | 0.96x |
|
|
684
697
|
| lmdb-raw | 318 MiB/s | 113 MiB/s | 214.3 | 1.48 ms | 153.0 MiB | 0.69x |
|
|
685
698
|
| arrow-ipc-raw | 562 MiB/s | 44 MiB/s | 83.1 | 4.00 ms | 109.9 MiB | 0.96x |
|
|
686
699
|
| arrayrecord-raw | 288 MiB/s | 26 MiB/s | 49.3 | 7.39 ms | 118.8 MiB | 0.89x |
|
|
687
700
|
| arrayrecord-zstd | 62 MiB/s | 33 MiB/s | 61.5 | 5.58 ms | 76.2 MiB | 1.39x |
|
|
688
701
|
|
|
689
702
|
Here the record contract, not bulk byte bandwidth, is the useful scale.
|
|
690
|
-
Zrecord's three codecs return
|
|
703
|
+
Zrecord's three codecs return 595–735 krecords/s with 0.50–0.60 ms p95;
|
|
691
704
|
the dictionary gives the best disk ratio and is slightly ahead of plain zstd in
|
|
692
705
|
this pass.
|
|
693
706
|
|
|
@@ -734,8 +747,8 @@ and Grain reads ArrayRecord.
|
|
|
734
747
|
|
|
735
748
|
| loader | model | storage | batches/s | p95 | steady PSS | peak PSS | peak RSS |
|
|
736
749
|
|---|---|---|---:|---:|---:|---:|---:|
|
|
737
|
-
| **loaderx** | threads | zrecord-zstd |
|
|
738
|
-
| loaderx-raw | threads | zrecord-raw |
|
|
750
|
+
| **loaderx** | threads | zrecord-zstd | 171.5 | 12.69 ms | 984 MiB | 985 MiB | 988 MiB |
|
|
751
|
+
| loaderx-raw | threads | zrecord-raw | 200.0 | 12.50 ms | 993 MiB | 993 MiB | 996 MiB |
|
|
739
752
|
| torch | fork | npy-mmap-raw | 109.4 | 32.70 ms | 1783 MiB | 1889 MiB | 6396 MiB |
|
|
740
753
|
| torch-spawn | spawn | npy-mmap-raw | 111.6 | 30.69 ms | 2835 MiB | 2913 MiB | 4880 MiB |
|
|
741
754
|
| grain | processes | arrayrecord-zstd | 46.6 | 93.48 ms | 1847 MiB | 1946 MiB | 2065 MiB |
|
|
@@ -745,8 +758,8 @@ the transform threads overlap Python-side collation with the next gather. The
|
|
|
745
758
|
memory is the source, Zrecord container and bounded in-flight batches. loaderx prefetches in
|
|
746
759
|
threads inside one process, so workers share one interpreter, one NumPy runtime
|
|
747
760
|
and one set of gather buffers. With source geometry and entropy held constant,
|
|
748
|
-
raw is 1.
|
|
749
|
-
Torch spawn and 3.
|
|
761
|
+
raw is 1.17x compressed loaderx; compressed loaderx is 1.57x Torch fork, 1.54x
|
|
762
|
+
Torch spawn and 3.68x Grain, while raw is 1.83x, 1.79x and 4.29x faster.
|
|
750
763
|
Torch's aggregate RSS is high because
|
|
751
764
|
Linux fork mappings are counted repeatedly; it is not a total-memory ratio
|
|
752
765
|
against Zrecord's unaccounted page cache. The explicit `torch-spawn` row removes
|
|
@@ -815,13 +828,13 @@ specialization for compression and a complete variable-shape persistence model.
|
|
|
815
828
|
copy are one path, so turning compression on costs part of a margin, not an
|
|
816
829
|
order of magnitude. The ratio is the data, not the
|
|
817
830
|
format: in the current Dense structured-vision workload, plain zstd reaches
|
|
818
|
-
14.69x and the balanced dictionary reaches 22.
|
|
831
|
+
14.69x and the balanced dictionary reaches 22.10x.
|
|
819
832
|
|
|
820
833
|
**Loader results combine architecture and storage.** loaderx uses threads and
|
|
821
834
|
never ends an epoch, so a step pays no IPC and never waits on an epoch boundary;
|
|
822
835
|
torch uses finite shuffled epochs, worker processes and shared-memory handoff.
|
|
823
|
-
Here compressed loaderx is 1.
|
|
824
|
-
raw loaderx is 1.
|
|
836
|
+
Here compressed loaderx is 1.57x Torch fork, 1.54x Torch spawn and 3.68x Grain;
|
|
837
|
+
raw loaderx is 1.83x, 1.79x and 4.29x faster, respectively.
|
|
825
838
|
Storage also differs per loader — each reads from what it was
|
|
826
839
|
built for — so the loader table is a different comparison from either store
|
|
827
840
|
table, not a rerun.
|
|
@@ -1047,9 +1060,9 @@ license to trust storage or the operating system: native code still validates
|
|
|
1047
1060
|
normal I/O behavior, basic malformed-store rejection and native memory safety; checks that only
|
|
1048
1061
|
defend against bypassing the public Python API do not belong in zrecord.
|
|
1049
1062
|
|
|
1050
|
-
1. `RecordEngine` stores N logically ordered records.
|
|
1051
|
-
|
|
1052
|
-
|
|
1063
|
+
1. `RecordEngine` stores N logically ordered records. Payload record `i` belongs
|
|
1064
|
+
to `data_{i % data_shards}.zr`; logical ID remains the stable append position
|
|
1065
|
+
and `RecordLoc[ID]` preserves its shard-local offset. Index and slice
|
|
1053
1066
|
operations are implemented as ordered gathers over those positions.
|
|
1054
1067
|
2. It hands the container layer a dense sequence space: records are exactly
|
|
1055
1068
|
`0..N-1`. Named streams are composed dynamically by a plain Python dict;
|
|
@@ -1059,7 +1072,7 @@ defend against bypassing the public Python API do not belong in zrecord.
|
|
|
1059
1072
|
byte geometry. Dense stores persist
|
|
1060
1073
|
one fixed-width physical record per logical record. Ragged stores
|
|
1061
1074
|
persist one variable-width physical record per logical record:
|
|
1062
|
-
`[u64le
|
|
1075
|
+
`[u64le dim] * schema.ndim + [payload]`. Shape and payload therefore share one
|
|
1063
1076
|
location, codec frame, and append publication.
|
|
1064
1077
|
4. The IO model (`append | read`) is batch-oriented and
|
|
1065
1078
|
shape-agnostic. Per-call adapters expose record boundaries through a compile-time
|
|
@@ -1076,7 +1089,7 @@ defend against bypassing the public Python API do not belong in zrecord.
|
|
|
1076
1089
|
|-------|-----------|----------------------------------------|
|
|
1077
1090
|
| 0 | raw | none |
|
|
1078
1091
|
| 1 | zstd | zstd (plain, level 3) |
|
|
1079
|
-
| 2 | zstdict | zstd with a trained dictionary (level
|
|
1092
|
+
| 2 | zstdict | zstd with a trained dictionary (level 15) |
|
|
1080
1093
|
```
|
|
1081
1094
|
|
|
1082
1095
|
7. Compression is transparent to the client:
|
|
@@ -1096,40 +1109,51 @@ defend against bypassing the public Python API do not belong in zrecord.
|
|
|
1096
1109
|
dictionary carries the structure shared across records, which per-record
|
|
1097
1110
|
compression cannot see. On many small, similar records (image tiles, token
|
|
1098
1111
|
sequences) this is a large win: the current Dense structured-vision set
|
|
1099
|
-
is 14.69x with plain zstd and 22.
|
|
1112
|
+
is 14.69x with plain zstd and 22.10x with the balanced dictionary. The dictionary is loaded once on
|
|
1100
1113
|
open and shared, lock-free, across all reader threads. The dictionary size
|
|
1101
1114
|
is chosen from the `DICT_TIERS` presets (see Codec notes).
|
|
1102
1115
|
* A `zstd_dict` store needs its dictionary to read every record; a `raw` or
|
|
1103
1116
|
`zstd` store rejects an unexpected dictionary as malformed state.
|
|
1104
1117
|
|
|
1105
1118
|
## Persistence format
|
|
1106
|
-
The
|
|
1107
|
-
the
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1119
|
+
The current format is the settled internal baseline for implementation work: optimizations keep
|
|
1120
|
+
the fixed files, Header/schema page, contiguous ``RecordLoc`` table and independent
|
|
1121
|
+
record payloads unless the product boundary is deliberately reopened. "Settled"
|
|
1122
|
+
does not promise cross-version persistence compatibility: there is no compatibility
|
|
1123
|
+
layer, migration, version dispatch, checksum, or recovery facility. Zrecord is not
|
|
1124
|
+
the authority for irreplaceable data. Keep authoritative source data and reproducible
|
|
1125
|
+
build scripts; after an interrupted build, storage failure, incompatible implementation
|
|
1126
|
+
change, or content change, rebuild a complete container at a new path.
|
|
1127
|
+
|
|
1128
|
+
Native storage uses one metadata file and a create-time-fixed payload file set:
|
|
1114
1129
|
```
|
|
1115
1130
|
store/
|
|
1116
|
-
├── meta.zr 4096-byte
|
|
1117
|
-
├──
|
|
1131
|
+
├── meta.zr 4096-byte Header/schema/tails page + RecordLoc table
|
|
1132
|
+
├── data_0.zr payload records where ID % data_shards == 0
|
|
1133
|
+
├── ...
|
|
1134
|
+
├── data_{N-1}.zr final static payload shard
|
|
1118
1135
|
└── dict.zr zstd dictionary (only in dict stores)
|
|
1119
1136
|
```
|
|
1120
1137
|
|
|
1138
|
+
`data_shards` is a write-performance parameter in `1..255`, fixed by `create`
|
|
1139
|
+
and recovered automatically by `open`. The default is four; practical values are
|
|
1140
|
+
usually 2, 4, 8 or 16, near the writer lane count. More files spread positional
|
|
1141
|
+
writes across payload inodes but consume one descriptor each. This physical
|
|
1142
|
+
striping does not change record IDs, order, codec, or read results.
|
|
1143
|
+
|
|
1121
1144
|
### Metadata (meta.zr)
|
|
1122
1145
|
|
|
1123
1146
|
Files are read and written **positionally** — pread/pwrite at computed offsets,
|
|
1124
1147
|
no mmap. `meta.zr` starts with one fixed 4096-byte static page: a naturally
|
|
1125
|
-
aligned
|
|
1148
|
+
aligned 16-byte Header, 255 shard-local u64 tails at bytes 16..2055, then up to
|
|
1149
|
+
2040 bytes of opaque MsgPack schema at bytes 2056..4095.
|
|
1126
1150
|
An array of 16-byte `RecordLoc`s starts at offset 4096. Record `i` is one
|
|
1127
1151
|
pread/pwrite at `4096 + i * 16`; there is no variable table base, segment
|
|
1128
1152
|
mapping, or rollover fd table.
|
|
1129
1153
|
|
|
1130
|
-
**1. Python schema** — bytes `
|
|
1131
|
-
MsgPack object.
|
|
1132
|
-
|
|
1154
|
+
**1. Python schema** — bytes `2056..2056+schema_length` are exactly one immutable
|
|
1155
|
+
MsgPack object. Both stores contain ``dtype`` and ``ndim``; Dense additionally
|
|
1156
|
+
contains ``item_shape``. Native create persists these bytes together with
|
|
1133
1157
|
the physical container but does not decode them. Open acquires the native lifetime
|
|
1134
1158
|
lock before copying the schema to Python for validation, so schema and physical
|
|
1135
1159
|
metadata are one locked snapshot. Dense record width is derived once from
|
|
@@ -1137,30 +1161,36 @@ dtype/item_shape and passed to the native handle as runtime geometry; it is not
|
|
|
1137
1161
|
independently persisted as a second authority. There is no format version or
|
|
1138
1162
|
legacy kind dispatch.
|
|
1139
1163
|
|
|
1140
|
-
**2. Physical header** — the first
|
|
1164
|
+
**2. Physical header** — the first 16 bytes of `meta.zr`. The format
|
|
1141
1165
|
deliberately carries no payload or metadata checksum.
|
|
1142
1166
|
|
|
1143
1167
|
* `codec` is the store's one compression method, stamped at creation and
|
|
1144
1168
|
immutable — there is no per-record tag anywhere.
|
|
1145
1169
|
* `length` (u64) is the physical record count; it equals logical length for both
|
|
1146
1170
|
dense stores and inline ragged stores.
|
|
1147
|
-
* `
|
|
1148
|
-
|
|
1149
|
-
|
|
1171
|
+
* `schema_length` (u16) is the occupied prefix of the static schema area and
|
|
1172
|
+
must be in `1..2040`.
|
|
1173
|
+
* `data_shards` (u8) is the static payload file count and must be in `1..255`.
|
|
1150
1174
|
|
|
1151
1175
|
```zig
|
|
1152
1176
|
const Codec = enum(u8) { raw = 0, zstd = 1, zstdict = 2, _ };
|
|
1153
1177
|
const Header = extern struct {
|
|
1154
1178
|
length: u64,
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
reserved: [11]u8,
|
|
1179
|
+
schema_length: u16,
|
|
1180
|
+
data_shards: u8,
|
|
1158
1181
|
codec: u8,
|
|
1182
|
+
reserved: [4]u8,
|
|
1159
1183
|
};
|
|
1160
1184
|
```
|
|
1161
1185
|
|
|
1162
|
-
**3.
|
|
1163
|
-
`
|
|
1186
|
+
**3. Shard frontiers** — tail slot `s` at `16 + s * 8` is the committed
|
|
1187
|
+
end of `data_s.zr`. Unused slots among the 255 fixed u64 entries are zero.
|
|
1188
|
+
Open requires every data file to be at least its persisted tail; locations may
|
|
1189
|
+
not cross that shard-local frontier.
|
|
1190
|
+
|
|
1191
|
+
**4. Record table** — contiguous 16-byte entries start at offset 4096 in
|
|
1192
|
+
`meta.zr` and grow as location windows are written. `offset` is local to
|
|
1193
|
+
`data_{ID % data_shards}.zr`;
|
|
1164
1194
|
`phys_length`/`logic_length` are the stored and original sizes. The
|
|
1165
1195
|
codec is not here: it is the header's, so a record is stored exactly the way the
|
|
1166
1196
|
store is declared.
|
|
@@ -1175,9 +1205,9 @@ const RecordLoc = extern struct {
|
|
|
1175
1205
|
|
|
1176
1206
|
There is no liveness flag. Every entry below `length` is a record.
|
|
1177
1207
|
|
|
1178
|
-
**
|
|
1179
|
-
|
|
1180
|
-
|
|
1208
|
+
**5. No fixed record-count cap.** The table and payload streams grow naturally.
|
|
1209
|
+
The practical bounds are the u64 count, supported positional file offsets,
|
|
1210
|
+
2 GiB per record, descriptor budget, and disk.
|
|
1181
1211
|
|
|
1182
1212
|
## Executor
|
|
1183
1213
|
|
|
@@ -1192,34 +1222,36 @@ while `Ragged` supplies offsets for its shape-prefixed records.
|
|
|
1192
1222
|
The private ABI turns those inputs into compile-time record sources and
|
|
1193
1223
|
destinations; the engine has one append and one gather operation. Its shared
|
|
1194
1224
|
opaque handle remains private and carries no typed-store geometry.
|
|
1195
|
-
*
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
the
|
|
1203
|
-
|
|
1204
|
-
|
|
1225
|
+
* Append validates the complete call before physical I/O, then processes bounded
|
|
1226
|
+
logical windows. Within a window, record positions are divided by
|
|
1227
|
+
`ID % data_shards` and planned as bounded shard-local chunks. Fixed Executor
|
|
1228
|
+
lanes dynamically claim those chunks, encode or pack them, briefly lock only
|
|
1229
|
+
the selected shard's tail reservation, then issue positional payload writes
|
|
1230
|
+
directly. Multiple lanes may write non-overlapping ranges of one shard; the
|
|
1231
|
+
static file set spreads that pressure across inodes without limiting codec
|
|
1232
|
+
concurrency to the shard count. Workers fill disjoint entries in one
|
|
1233
|
+
contiguous location buffer. A payload barrier precedes one contiguous
|
|
1234
|
+
`meta.zr` location write. All windows must succeed before the in-process
|
|
1235
|
+
length and per-shard tails advance. Writer `close()` truncates each data file
|
|
1236
|
+
to its committed tail and publishes the 4096-byte static page.
|
|
1237
|
+
* Compressed shard tasks lease process-bounded `ExecutionSlot` scratch and write
|
|
1238
|
+
independent frames in bounded subchunks. Python budgets the process-wide executor at three quarters
|
|
1205
1239
|
of the logical CPUs available to the process, leaving headroom for packing,
|
|
1206
1240
|
transforms, and the caller without encoding a platform-specific thread count.
|
|
1207
1241
|
Each producer configures its CCtx or shared immutable CDict once, then starts
|
|
1208
1242
|
every independent record frame with `ZSTD_compress2`.
|
|
1209
|
-
* Raw
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
`std.Io` handles short writes and maps the same positional operation to POSIX
|
|
1216
|
-
and Windows implementations.
|
|
1243
|
+
* Raw records for one shard are strided in the source. Each shard task packs a
|
|
1244
|
+
bounded subchunk into its reusable scratch and performs one contiguous
|
|
1245
|
+
positional write; a single record larger than the normal subchunk budget is
|
|
1246
|
+
written directly. There is no per-record syscall or platform-specific vectored
|
|
1247
|
+
path. The extra memory copy is the deliberate cost paid to remove concentrated
|
|
1248
|
+
single-inode writes.
|
|
1217
1249
|
**2. Read.** Fill the destination memory concurrently, in place from the Python
|
|
1218
1250
|
side (executed on async threads).
|
|
1219
1251
|
* Committed records are immutable and `length` is published through an atomic.
|
|
1220
|
-
The fixed
|
|
1221
|
-
|
|
1222
|
-
|
|
1252
|
+
The fixed metadata and static data handles require no rollover synchronization.
|
|
1253
|
+
* Every record first selects `data_{ID % data_shards}.zr`, then reads the
|
|
1254
|
+
shard-local offset its table entry records — the record table
|
|
1223
1255
|
is addressed by pure arithmetic, so random access is one pread for the
|
|
1224
1256
|
location and one for the bytes, with no batching assumptions about layout.
|
|
1225
1257
|
Each lane reads one location and immediately reads/decompresses that record;
|
|
@@ -1231,8 +1263,9 @@ budget; lanes for which the runtime cannot reserve concurrency run inline on the
|
|
|
1231
1263
|
calling thread. Python configures the process-level budget as
|
|
1232
1264
|
`max(physical cores, logical cores * 3 / 4)`, using platform topology where
|
|
1233
1265
|
available.
|
|
1234
|
-
*
|
|
1235
|
-
|
|
1266
|
+
* Gather lanes receive contiguous request blocks. Append lanes dynamically claim
|
|
1267
|
+
bounded chunks of strided shard ranges; short per-shard reservation locks keep
|
|
1268
|
+
offsets disjoint while positional writes remain concurrent.
|
|
1236
1269
|
* Each lane creates one zstd context (`ZSTD_CCtx` to write, `ZSTD_DCtx` to
|
|
1237
1270
|
read) and reuses it across every record it handles, rather than paying that
|
|
1238
1271
|
setup per record. The dictionary (`ZSTD_CDict`/`ZSTD_DDict`) is immutable, so
|
|
@@ -1242,10 +1275,10 @@ available.
|
|
|
1242
1275
|
|
|
1243
1276
|
**4. File access.**
|
|
1244
1277
|
* Metadata: one naturally growing `meta.zr`, containing the fixed Header/schema
|
|
1245
|
-
page and loc table.
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
sparse
|
|
1278
|
+
/tails page and loc table. It is intentionally not sharded because measured
|
|
1279
|
+
write pressure is in payload I/O; one coordinator writes each loc window.
|
|
1280
|
+
* Payload: a static list of naturally growing `data_<shard>.zr` files, accessed through
|
|
1281
|
+
`readPositionalAll`/`writePositionalAll`. No path depends on sparse files.
|
|
1249
1282
|
|
|
1250
1283
|
**Execution model.** Opened readers are immutable, so calls on the same reader may
|
|
1251
1284
|
gather concurrently. Creator appends are synchronous and native Storage
|