loaderx 2.0.7__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {loaderx-2.0.7 → loaderx-2.2.1}/MANIFEST.in +1 -1
  2. {loaderx-2.0.7/loaderx.egg-info → loaderx-2.2.1}/PKG-INFO +79 -69
  3. {loaderx-2.0.7 → loaderx-2.2.1}/README.md +78 -68
  4. {loaderx-2.0.7 → loaderx-2.2.1}/build.zig +5 -3
  5. {loaderx-2.0.7 → loaderx-2.2.1}/build.zig.zon +1 -1
  6. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx/__init__.py +1 -1
  7. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx/_store.py +11 -9
  8. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx/utils.py +34 -3
  9. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx/zrecord.py +89 -64
  10. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx/zsampler.py +5 -4
  11. {loaderx-2.0.7 → loaderx-2.2.1/loaderx.egg-info}/PKG-INFO +79 -69
  12. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx.egg-info/SOURCES.txt +3 -0
  13. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/bench_ragged.py +24 -25
  14. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/test_loaderx.py +124 -42
  15. {loaderx-2.0.7 → loaderx-2.2.1}/src/record/engine.zig +110 -111
  16. {loaderx-2.0.7 → loaderx-2.2.1}/src/record/executor.zig +14 -8
  17. {loaderx-2.0.7 → loaderx-2.2.1}/src/record/storage.zig +105 -105
  18. {loaderx-2.0.7 → loaderx-2.2.1}/src/store.zig +14 -26
  19. {loaderx-2.0.7 → loaderx-2.2.1}/src/zsampler.zig +19 -28
  20. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/bits.h +92 -87
  21. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/bitstream.h +26 -29
  22. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/compiler.h +36 -22
  23. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/cpu.h +1 -1
  24. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/debug.h +0 -9
  25. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/error_private.c +1 -0
  26. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/error_private.h +0 -10
  27. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/fse.h +2 -17
  28. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/fse_decompress.c +2 -0
  29. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/huf.h +0 -9
  30. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/mem.h +7 -11
  31. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/pool.h +0 -9
  32. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/portability_macros.h +22 -9
  33. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/threading.h +0 -8
  34. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/xxhash.h +93 -19
  35. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/zstd_deps.h +12 -0
  36. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/zstd_internal.h +1 -69
  37. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/zstd_trace.h +5 -12
  38. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/hist.c +10 -0
  39. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/hist.h +7 -0
  40. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress.c +1057 -367
  41. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress_internal.h +227 -125
  42. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress_literals.c +1 -1
  43. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress_sequences.c +7 -7
  44. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress_sequences.h +7 -6
  45. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress_superblock.c +17 -17
  46. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_cwksp.h +41 -24
  47. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_double_fast.c +58 -50
  48. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_double_fast.h +4 -12
  49. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_fast.c +91 -74
  50. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_fast.h +4 -12
  51. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_lazy.c +64 -64
  52. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_lazy.h +30 -39
  53. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_ldm.c +48 -33
  54. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_ldm.h +6 -14
  55. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_opt.c +55 -51
  56. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_opt.h +8 -16
  57. loaderx-2.2.1/vendor/zstd/lib/compress/zstd_preSplit.c +238 -0
  58. loaderx-2.2.1/vendor/zstd/lib/compress/zstd_preSplit.h +33 -0
  59. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstdmt_compress.c +134 -93
  60. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstdmt_compress.h +4 -15
  61. loaderx-2.2.1/vendor/zstd/lib/decompress/huf_decompress_amd64.S +602 -0
  62. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/decompress/zstd_decompress.c +14 -11
  63. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/decompress/zstd_decompress_block.c +6 -12
  64. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/decompress/zstd_decompress_internal.h +5 -5
  65. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/dictBuilder/cover.c +60 -19
  66. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/dictBuilder/divsufsort.h +0 -10
  67. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/dictBuilder/zdict.c +2 -2
  68. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/zdict.h +15 -8
  69. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/zstd.h +241 -132
  70. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/zstd_errors.h +1 -8
  71. {loaderx-2.0.7 → loaderx-2.2.1}/LICENSE +0 -0
  72. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx/_lib.py +0 -0
  73. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx/dataloader.py +0 -0
  74. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx.egg-info/dependency_links.txt +0 -0
  75. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx.egg-info/requires.txt +0 -0
  76. {loaderx-2.0.7 → loaderx-2.2.1}/loaderx.egg-info/top_level.txt +0 -0
  77. {loaderx-2.0.7 → loaderx-2.2.1}/pyproject.toml +0 -0
  78. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/_bench_common.py +0 -0
  79. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/bench.py +0 -0
  80. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/bench_dense.py +0 -0
  81. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/build_wheels.py +0 -0
  82. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/prepare_tokens.py +0 -0
  83. {loaderx-2.0.7 → loaderx-2.2.1}/scripts/requirements-bench.txt +0 -0
  84. {loaderx-2.0.7 → loaderx-2.2.1}/setup.cfg +0 -0
  85. {loaderx-2.0.7 → loaderx-2.2.1}/setup.py +0 -0
  86. {loaderx-2.0.7 → loaderx-2.2.1}/src/zstd/c.zig +0 -0
  87. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/COPYING +0 -0
  88. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/LICENSE +0 -0
  89. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/allocations.h +0 -0
  90. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/debug.c +0 -0
  91. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/entropy_common.c +0 -0
  92. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/pool.c +0 -0
  93. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/threading.c +0 -0
  94. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/xxhash.c +0 -0
  95. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/common/zstd_common.c +0 -0
  96. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/clevels.h +0 -0
  97. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/fse_compress.c +0 -0
  98. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/huf_compress.c +0 -0
  99. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress_literals.h +0 -0
  100. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_compress_superblock.h +0 -0
  101. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/compress/zstd_ldm_geartab.h +0 -0
  102. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/decompress/huf_decompress.c +0 -0
  103. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/decompress/zstd_ddict.c +0 -0
  104. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/decompress/zstd_ddict.h +0 -0
  105. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/decompress/zstd_decompress_block.h +0 -0
  106. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/dictBuilder/cover.h +0 -0
  107. {loaderx-2.0.7 → loaderx-2.2.1}/vendor/zstd/lib/dictBuilder/divsufsort.c +0 -0
  108. {loaderx-2.0.7 → loaderx-2.2.1}/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.0.7
3
+ Version: 2.2.1
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`` plus the dense ``item_shape``,
125
- or only ``dtype`` for ragged stores. The MsgPack bytes live opaquely in the
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 a u8 rank followed by inline little-endian u64 dimensions.
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
- are fixed-stride gathers and the batch shape follows from the schema, so no
142
- per-record metadata is touched:
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`` is unified and
164
- explicit; each record keeps its **own shape**, recorded per record as it is
165
- written and restored exactly on read so records may differ in shape
166
- arbitrarily, and nothing is ever inferred from the source (an iterator can't
167
- tell you what its later records look like). Scalar ``shape == ()`` is preserved;
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 explicit; each record keeps its shape
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
@@ -273,7 +274,7 @@ 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
 
@@ -288,12 +289,14 @@ appended to. ``close()`` on a writer publishes the container Header;
288
289
  ``with`` for scoped lifetimes. Content is never changed in place: rerun the
289
290
  authoritative build at a new path, validate it, then switch consumers to it.
290
291
 
291
- The exact schema is declared at creation and encoded by Python as MsgPack. Dense
292
- schema contains only ``dtype`` and ``item_shape``; Ragged schema contains only
293
- ``dtype``. Structured, subarray, object, and metadata-bearing dtypes are not
292
+ The exact schema is declared at creation and encoded by Python as MsgPack. Both
293
+ schemas contain ``dtype`` and ``ndim``; Dense additionally contains
294
+ ``item_shape`` and requires its length to equal ``ndim``. Ragged requires every
295
+ appended record to have that rank, while every dimension length may vary.
296
+ Structured, subarray, object, metadata-bearing, and zero-itemsize dtypes are not
294
297
  supported: their semantics do not round-trip through one canonical NumPy dtype
295
298
  string. The encoded schema has 4064 bytes available in the fixed 4096-byte
296
- metadata page. Ragged schema size is effectively fixed; Dense schema size grows
299
+ metadata page. Ragged schema size is fixed; Dense schema size grows
297
300
  only with the integer ``item_shape``, so the physical limit is far above any
298
301
  practical NumPy array rank.
299
302
  Unexpected fields are rejected. ``append`` validates dtype and shape in Python,
@@ -310,7 +313,7 @@ published without one.
310
313
  for general-purpose compression — it is fast and the default.
311
314
 
312
315
  `"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 19. The dictionary
316
+ any record, then compresses every record against it at level 15. The dictionary
314
317
  captures structure shared across records that per-record compression cannot see —
315
318
  a large win for many small, similar records (image tiles, token sequences).
316
319
 
@@ -353,7 +356,8 @@ from loaderx.zrecord import Dense, Ragged
353
356
  d = train_dict(settled_array, tier="balanced")
354
357
 
355
358
  # then any new store can install it and append explicitly
356
- with Ragged.create('tokens', np.int32, codec='zstd_dict', dict_bytes=d) as ds:
359
+ with Ragged.create('tokens', np.int32, ndim=1,
360
+ codec='zstd_dict', dict_bytes=d) as ds:
357
361
  ds.append(token_generator)
358
362
  with Dense.create('data', data.dtype, data.shape[1:],
359
363
  codec='zstd_dict', dict_bytes=d) as ds:
@@ -377,7 +381,8 @@ with Dense.open("src") as s, \
377
381
  ```
378
382
 
379
383
  ``Ragged`` is the same shape: ``s[:]`` returns ``list[np.ndarray]``, which
380
- is exactly its append input, and ``dtype`` is all the destination needs. The
384
+ is exactly its append input; a destination is created with ``s.dtype`` and
385
+ ``s.ndim``. The
381
386
  native compression path bounds its own working memory; there is no public chunk
382
387
  parameter. ``dst`` must not already hold a store.
383
388
 
@@ -403,7 +408,7 @@ with Dense.create(root + "/joint", joint.dtype, joint.shape[1:]) as s:
403
408
  s.append(joint)
404
409
  with Dense.create(root + "/label", label.dtype, label.shape[1:]) as s:
405
410
  s.append(label)
406
- with Ragged.create(root + "/token", np.int32) as s:
411
+ with Ragged.create(root + "/token", np.int32, ndim=1) as s:
407
412
  s.append(seqs)
408
413
 
409
414
  streams = {
@@ -496,9 +501,12 @@ so CFFI, NumPy allocation, and Ragged list/shape reconstruction are timed.
496
501
 
497
502
  ### Methodology
498
503
 
499
- The results below are one complete run from the same checkout on a warm page
500
- cache. They are not three-run medians: an unexpected result is traced separately
501
- instead of being hidden by repeated aggregation. Within each store workload every
504
+ 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.2.0
506
+ after the libzstd 1.5.7 and runtime-dispatched Huffman kernel update. Unchanged
507
+ optional Store backends, sampler rows and external loader rows remain from the
508
+ immediately preceding 2.0.9 run. These are not three-run medians: an unexpected result is traced
509
+ separately instead of being hidden by repeated aggregation. Within each store workload every
502
510
  backend receives identical source records. Correctness and timing use independent
503
511
  deterministic `zsampler` IID streams. Loader backends receive
504
512
  the same source and seed but use their own shipped samplers, so their exact
@@ -556,8 +564,8 @@ threads):
556
564
  | NUMA | 1 node |
557
565
  | memory | 31 GiB (not limited by cgroup) |
558
566
  | shared memory | 16 GiB `/dev/shm` |
559
- | OS | Debian GNU/Linux forky/sid, kernel 7.1.3+deb13-amd64, x86_64 |
560
- | python | CPython 3.14.7 (standard GIL build), numpy 2.5.2 |
567
+ | OS | Debian GNU/Linux forky/sid, kernel 7.1.8+deb13-amd64, x86_64 |
568
+ | python | CPython 3.14.7 (standard GIL build), numpy 2.4.6 |
561
569
 
562
570
  The benchmark process sees all 24 threads and is not memory-limited by cgroup.
563
571
  The 16 GiB shared-memory mount accommodates the four-worker, 36.8 MiB-batch torch
@@ -575,9 +583,9 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
575
583
 
576
584
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
577
585
  |---|---:|---:|---:|---:|---:|---:|
578
- | zrecord-zstd | 7998 MiB/s | 10937 MiB/s | 76.2 | 4.37 ms | 24.4 MiB | 14.69x |
579
- | zrecord-zstdict | 35 MiB/s | 12173 MiB/s | 84.8 | 4.06 ms | 15.8 MiB | 22.74x |
580
- | zrecord-raw | 2523 MiB/s | 14329 MiB/s | 99.8 | 3.12 ms | 358.9 MiB | 1.00x |
586
+ | zrecord-zstd | 6796 MiB/s | 10816 MiB/s | 75.3 | 4.09 ms | 24.4 MiB | 14.69x |
587
+ | zrecord-zstdict | 102 MiB/s | 11844 MiB/s | 82.5 | 3.61 ms | 16.2 MiB | 22.10x |
588
+ | zrecord-raw | 2801 MiB/s | 14655 MiB/s | 102.1 | 2.92 ms | 358.9 MiB | 1.00x |
581
589
  | npy-mmap-raw | 2004 MiB/s | 4749 MiB/s | 33.1 | 11.08 ms | 358.9 MiB | 1.00x |
582
590
  | hdf5-raw | 2350 MiB/s | 1836 MiB/s | 12.8 | 29.68 ms | 359.0 MiB | 1.00x |
583
591
  | hdf5-gzip | 277 MiB/s | 657 MiB/s | 4.6 | 62.53 ms | 26.1 MiB | 13.73x |
@@ -591,8 +599,8 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
591
599
  | tiledb-raw | 743 MiB/s | 630 MiB/s | 4.4 | 66.01 ms | 359.0 MiB | 1.00x |
592
600
  | tiledb-zstd | 1498 MiB/s | 1503 MiB/s | 10.5 | 27.82 ms | 26.9 MiB | 13.36x |
593
601
 
594
- At 147 KiB per record, Zrecord-raw reaches 14.0 GiB/s and is 3.0x npy-mmap-raw;
595
- plain zstd gathers at 10.7 GiB/s while reducing the corpus 14.69x. LMDB and Arrow
602
+ At 147 KiB per record, Zrecord-raw reaches 14.4 GiB/s and is 3.1x npy-mmap-raw;
603
+ plain zstd gathers at 10.6 GiB/s while reducing the corpus 14.69x. LMDB and Arrow
596
604
  IPC are competitive raw record
597
605
  stores, while codecs tied to whole IPC batches or Parquet row groups pay read
598
606
  amplification on random gathers. Dense demonstrates that
@@ -610,9 +618,9 @@ list or a one-dimensional variable-length abstraction is not enough.
610
618
 
611
619
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
612
620
  |---|---:|---:|---:|---:|---:|---:|
613
- | zrecord-zstd | 2407 MiB/s | 9927 MiB/s | 59.6 | 5.44 ms | 26.9 MiB | 15.52x |
614
- | zrecord-zstdict | 37 MiB/s | 10814 MiB/s | 65.0 | 4.86 ms | 17.7 MiB | 23.58x |
615
- | zrecord-raw | 1591 MiB/s | 13032 MiB/s | 78.2 | 3.87 ms | 417.2 MiB | 1.00x |
621
+ | zrecord-zstd | 2339 MiB/s | 9507 MiB/s | 57.1 | 5.16 ms | 26.9 MiB | 15.49x |
622
+ | zrecord-zstdict | 102 MiB/s | 10336 MiB/s | 62.1 | 4.85 ms | 18.2 MiB | 22.98x |
623
+ | zrecord-raw | 1420 MiB/s | 12929 MiB/s | 77.6 | 3.86 ms | 417.2 MiB | 1.00x |
616
624
  | hdf5-raw | 1451 MiB/s | 1059 MiB/s | 6.4 | 44.91 ms | 418.0 MiB | 1.00x |
617
625
  | hdf5-gzip | 250 MiB/s | 126 MiB/s | 0.7 | 359.23 ms | 29.9 MiB | 13.94x |
618
626
  | lmdb-raw | 1717 MiB/s | 7269 MiB/s | 43.6 | 7.93 ms | 422.2 MiB | 0.99x |
@@ -626,15 +634,15 @@ list or a one-dimensional variable-length abstraction is not enough.
626
634
  | tiledb-zstd | 675 MiB/s | 138 MiB/s | 0.8 | 328.05 ms | 27.0 MiB | 15.46x |
627
635
 
628
636
  Zrecord-raw is 1.8x LMDB and 3.2x Arrow IPC in logical gather. Zrecord-zstd
629
- delivers 9.7 GiB/s of
637
+ delivers 9.3 GiB/s of
630
638
  logical payload while reducing the corpus to 26.9 MiB. HDF5, Arrow IPC, Parquet,
631
639
  ArrayRecord and TileDB
632
640
  show the same framework/codec tradeoffs in both tables; compressed batch, chunk
633
641
  and row-group formats pay read amplification on random records.
634
642
 
635
643
  The shared generator makes compression ratios directly comparable across
636
- contracts: Zrecord zstd is 14.69x Dense versus 15.52x Ragged, and zstdict is
637
- 22.74x versus 23.58x. The remaining difference comes from the H/W distribution
644
+ contracts: Zrecord zstd is 14.69x Dense versus 15.49x Ragged, and zstdict is
645
+ 22.10x versus 22.98x. The remaining difference comes from the H/W distribution
638
646
  and Ragged shape metadata, not a different image entropy model.
639
647
 
640
648
  ### Small Token Records
@@ -654,19 +662,19 @@ accumulate at least two seconds.
654
662
 
655
663
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
656
664
  |---|---:|---:|---:|---:|---:|---:|
657
- | zrecord-zstd | 1019 MiB/s | 1959 MiB/s | 1002.8 | 0.35 ms | 193.2 MiB | 2.02x |
658
- | zrecord-zstdict | 52 MiB/s | 2238 MiB/s | 1145.9 | 0.31 ms | 153.8 MiB | 2.54x |
659
- | zrecord-raw | 2101 MiB/s | 7441 MiB/s | 3810.0 | 0.09 ms | 393.7 MiB | 0.99x |
660
- | npy-mmap-raw | 1778 MiB/s | 11454 MiB/s | 5864.4 | 0.06 ms | 390.6 MiB | 1.00x |
665
+ | zrecord-zstd | 1103 MiB/s | 2083 MiB/s | 1066.3 | 0.33 ms | 193.2 MiB | 2.02x |
666
+ | zrecord-zstdict | 70 MiB/s | 2160 MiB/s | 1105.7 | 0.32 ms | 154.4 MiB | 2.53x |
667
+ | zrecord-raw | 2178 MiB/s | 6862 MiB/s | 3513.2 | 0.10 ms | 393.7 MiB | 0.99x |
668
+ | npy-mmap-raw | 1587 MiB/s | 9070 MiB/s | 4644.0 | 0.08 ms | 390.6 MiB | 1.00x |
661
669
  | lmdb-raw | 639 MiB/s | 1104 MiB/s | 565.0 | 0.71 ms | 786.3 MiB | 0.50x |
662
670
  | arrow-ipc-raw | 2068 MiB/s | 225 MiB/s | 115.0 | 2.81 ms | 390.8 MiB | 1.00x |
663
671
  | arrayrecord-raw | 807 MiB/s | 150 MiB/s | 76.7 | 4.99 ms | 401.4 MiB | 0.97x |
664
672
  | arrayrecord-zstd | 127 MiB/s | 135 MiB/s | 69.4 | 4.45 ms | 201.3 MiB | 1.94x |
665
673
 
666
674
  The contiguous NumPy baseline is strongest when the whole corpus is one fixed
667
- typed matrix. Zrecord-raw reaches 3.81 Mrecords/s while retaining independent
675
+ typed matrix. Zrecord-raw reaches 3.51 Mrecords/s while retaining independent
668
676
  record semantics; the per-record zstd codecs halve disk and still return
669
- 1.00–1.15 Mrecords/s. LMDB's B-tree/page overhead is visible in both throughput
677
+ 1.07–1.11 Mrecords/s. LMDB's B-tree/page overhead is visible in both throughput
670
678
  and disk.
671
679
 
672
680
  #### Variable Token Sequences
@@ -678,16 +686,16 @@ with exact `int32` values and original one-dimensional shapes.
678
686
 
679
687
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
680
688
  |---|---:|---:|---:|---:|---:|---:|
681
- | zrecord-zstd | 236 MiB/s | 282 MiB/s | 533.7 | 0.66 ms | 67.5 MiB | 1.57x |
682
- | zrecord-zstdict | 43 MiB/s | 305 MiB/s | 576.9 | 0.61 ms | 49.2 MiB | 2.15x |
683
- | zrecord-raw | 486 MiB/s | 359 MiB/s | 679.0 | 0.52 ms | 110.7 MiB | 0.96x |
689
+ | zrecord-zstd | 271 MiB/s | 336 MiB/s | 634.2 | 0.54 ms | 67.4 MiB | 1.57x |
690
+ | zrecord-zstdict | 58 MiB/s | 364 MiB/s | 688.2 | 0.50 ms | 49.1 MiB | 2.16x |
691
+ | zrecord-raw | 546 MiB/s | 420 MiB/s | 794.2 | 0.44 ms | 110.5 MiB | 0.96x |
684
692
  | lmdb-raw | 318 MiB/s | 113 MiB/s | 214.3 | 1.48 ms | 153.0 MiB | 0.69x |
685
693
  | arrow-ipc-raw | 562 MiB/s | 44 MiB/s | 83.1 | 4.00 ms | 109.9 MiB | 0.96x |
686
694
  | arrayrecord-raw | 288 MiB/s | 26 MiB/s | 49.3 | 7.39 ms | 118.8 MiB | 0.89x |
687
695
  | arrayrecord-zstd | 62 MiB/s | 33 MiB/s | 61.5 | 5.58 ms | 76.2 MiB | 1.39x |
688
696
 
689
697
  Here the record contract, not bulk byte bandwidth, is the useful scale.
690
- Zrecord's three codecs return 534679 krecords/s with 0.52–0.66 ms p95;
698
+ Zrecord's three codecs return 634794 krecords/s with 0.44–0.54 ms p95;
691
699
  the dictionary gives the best disk ratio and is slightly ahead of plain zstd in
692
700
  this pass.
693
701
 
@@ -734,8 +742,8 @@ and Grain reads ArrayRecord.
734
742
 
735
743
  | loader | model | storage | batches/s | p95 | steady PSS | peak PSS | peak RSS |
736
744
  |---|---|---|---:|---:|---:|---:|---:|
737
- | **loaderx** | threads | zrecord-zstd | 167.1 | 14.63 ms | 986 MiB | 986 MiB | 989 MiB |
738
- | loaderx-raw | threads | zrecord-raw | 215.3 | 13.77 ms | 987 MiB | 987 MiB | 991 MiB |
745
+ | **loaderx** | threads | zrecord-zstd | 166.3 | 15.95 ms | 985 MiB | 986 MiB | 988 MiB |
746
+ | loaderx-raw | threads | zrecord-raw | 208.4 | 13.56 ms | 987 MiB | 987 MiB | 989 MiB |
739
747
  | torch | fork | npy-mmap-raw | 109.4 | 32.70 ms | 1783 MiB | 1889 MiB | 6396 MiB |
740
748
  | torch-spawn | spawn | npy-mmap-raw | 111.6 | 30.69 ms | 2835 MiB | 2913 MiB | 4880 MiB |
741
749
  | grain | processes | arrayrecord-zstd | 46.6 | 93.48 ms | 1847 MiB | 1946 MiB | 2065 MiB |
@@ -745,8 +753,8 @@ the transform threads overlap Python-side collation with the next gather. The
745
753
  memory is the source, Zrecord container and bounded in-flight batches. loaderx prefetches in
746
754
  threads inside one process, so workers share one interpreter, one NumPy runtime
747
755
  and one set of gather buffers. With source geometry and entropy held constant,
748
- raw is 1.29x compressed loaderx; compressed loaderx is 1.53x Torch fork, 1.50x
749
- Torch spawn and 3.59x Grain, while raw is 1.97x, 1.93x and 4.62x faster.
756
+ raw is 1.25x compressed loaderx; compressed loaderx is 1.52x Torch fork, 1.49x
757
+ Torch spawn and 3.57x Grain, while raw is 1.90x, 1.87x and 4.47x faster.
750
758
  Torch's aggregate RSS is high because
751
759
  Linux fork mappings are counted repeatedly; it is not a total-memory ratio
752
760
  against Zrecord's unaccounted page cache. The explicit `torch-spawn` row removes
@@ -815,13 +823,13 @@ specialization for compression and a complete variable-shape persistence model.
815
823
  copy are one path, so turning compression on costs part of a margin, not an
816
824
  order of magnitude. The ratio is the data, not the
817
825
  format: in the current Dense structured-vision workload, plain zstd reaches
818
- 14.69x and the balanced dictionary reaches 22.74x.
826
+ 14.69x and the balanced dictionary reaches 22.10x.
819
827
 
820
828
  **Loader results combine architecture and storage.** loaderx uses threads and
821
829
  never ends an epoch, so a step pays no IPC and never waits on an epoch boundary;
822
830
  torch uses finite shuffled epochs, worker processes and shared-memory handoff.
823
- Here compressed loaderx is 1.53x Torch fork, 1.50x Torch spawn and 3.59x Grain;
824
- raw loaderx is 1.97x, 1.93x and 4.62x faster, respectively.
831
+ Here compressed loaderx is 1.52x Torch fork, 1.49x Torch spawn and 3.57x Grain;
832
+ raw loaderx is 1.90x, 1.87x and 4.47x faster, respectively.
825
833
  Storage also differs per loader — each reads from what it was
826
834
  built for — so the loader table is a different comparison from either store
827
835
  table, not a rerun.
@@ -1059,7 +1067,7 @@ defend against bypassing the public Python API do not belong in zrecord.
1059
1067
  byte geometry. Dense stores persist
1060
1068
  one fixed-width physical record per logical record. Ragged stores
1061
1069
  persist one variable-width physical record per logical record:
1062
- `[u64le ndim][u64le dims...][payload]`. Shape and payload therefore share one
1070
+ `[u64le dim] * schema.ndim + [payload]`. Shape and payload therefore share one
1063
1071
  location, codec frame, and append publication.
1064
1072
  4. The IO model (`append | read`) is batch-oriented and
1065
1073
  shape-agnostic. Per-call adapters expose record boundaries through a compile-time
@@ -1076,7 +1084,7 @@ defend against bypassing the public Python API do not belong in zrecord.
1076
1084
  |-------|-----------|----------------------------------------|
1077
1085
  | 0 | raw | none |
1078
1086
  | 1 | zstd | zstd (plain, level 3) |
1079
- | 2 | zstdict | zstd with a trained dictionary (level 19) |
1087
+ | 2 | zstdict | zstd with a trained dictionary (level 15) |
1080
1088
  ```
1081
1089
 
1082
1090
  7. Compression is transparent to the client:
@@ -1096,19 +1104,21 @@ defend against bypassing the public Python API do not belong in zrecord.
1096
1104
  dictionary carries the structure shared across records, which per-record
1097
1105
  compression cannot see. On many small, similar records (image tiles, token
1098
1106
  sequences) this is a large win: the current Dense structured-vision set
1099
- is 14.69x with plain zstd and 22.74x with the balanced dictionary. The dictionary is loaded once on
1107
+ is 14.69x with plain zstd and 22.10x with the balanced dictionary. The dictionary is loaded once on
1100
1108
  open and shared, lock-free, across all reader threads. The dictionary size
1101
1109
  is chosen from the `DICT_TIERS` presets (see Codec notes).
1102
1110
  * A `zstd_dict` store needs its dictionary to read every record; a `raw` or
1103
1111
  `zstd` store rejects an unexpected dictionary as malformed state.
1104
1112
 
1105
1113
  ## Persistence format
1106
- The 2.0 clean break does not change the current on-disk format. That format is
1107
- the current implementation only: there is no compatibility layer, migration,
1108
- version dispatch, checksum, or recovery facility. Zrecord is not the authority
1109
- for irreplaceable data. Keep authoritative source data and reproducible build
1110
- scripts; after an interrupted build, storage failure, implementation change, or
1111
- content change, rebuild a complete container at a new path.
1114
+ The current format is the settled internal baseline for implementation work: optimizations keep
1115
+ the fixed files, Header/schema page, contiguous ``RecordLoc`` table and independent
1116
+ record payloads unless the product boundary is deliberately reopened. "Settled"
1117
+ does not promise cross-version persistence compatibility: there is no compatibility
1118
+ layer, migration, version dispatch, checksum, or recovery facility. Zrecord is not
1119
+ the authority for irreplaceable data. Keep authoritative source data and reproducible
1120
+ build scripts; after an interrupted build, storage failure, incompatible implementation
1121
+ change, or content change, rebuild a complete container at a new path.
1112
1122
 
1113
1123
  Native storage uses a fixed file set:
1114
1124
  ```
@@ -1128,8 +1138,8 @@ pread/pwrite at `4096 + i * 16`; there is no variable table base, segment
1128
1138
  mapping, or rollover fd table.
1129
1139
 
1130
1140
  **1. Python schema** — bytes `32..32+schema_length` are exactly one immutable
1131
- MsgPack object. Dense stores contain only ``dtype`` and ``item_shape``; Ragged
1132
- stores contain only ``dtype``. Native create persists these bytes together with
1141
+ MsgPack object. Both stores contain ``dtype`` and ``ndim``; Dense additionally
1142
+ contains ``item_shape``. Native create persists these bytes together with
1133
1143
  the physical container but does not decode them. Open acquires the native lifetime
1134
1144
  lock before copying the schema to Python for validation, so schema and physical
1135
1145
  metadata are one locked snapshot. Dense record width is derived once from
@@ -1192,8 +1202,8 @@ while `Ragged` supplies offsets for its shape-prefixed records.
1192
1202
  The private ABI turns those inputs into compile-time record sources and
1193
1203
  destinations; the engine has one append and one gather operation. Its shared
1194
1204
  opaque handle remains private and carries no typed-store geometry.
1195
- * Compressed append: workers claim individual records and reuse one operation-local
1196
- frame buffer per task, reserve physical offsets in completion order through a short frontier
1205
+ * Compressed append: workers claim record chunks and lease process-bounded
1206
+ ``ExecutionSlot`` scratch, reserve physical offsets in completion order through a short frontier
1197
1207
  lock, and issue positional payload writes in parallel. Logical IDs remain in
1198
1208
  the loc table, so physical completion order does not change random gather.
1199
1209
  The caller waits for every payload before publishing the new locations and