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.
Files changed (109) hide show
  1. {loaderx-2.0.7 → loaderx-2.3.3}/MANIFEST.in +1 -1
  2. {loaderx-2.0.7/loaderx.egg-info → loaderx-2.3.3}/PKG-INFO +151 -118
  3. {loaderx-2.0.7 → loaderx-2.3.3}/README.md +150 -117
  4. {loaderx-2.0.7 → loaderx-2.3.3}/build.zig +5 -3
  5. {loaderx-2.0.7 → loaderx-2.3.3}/build.zig.zon +1 -1
  6. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/__init__.py +1 -1
  7. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/_store.py +19 -13
  8. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/utils.py +34 -3
  9. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/zrecord.py +108 -67
  10. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/zsampler.py +5 -4
  11. {loaderx-2.0.7 → loaderx-2.3.3/loaderx.egg-info}/PKG-INFO +151 -118
  12. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/SOURCES.txt +3 -0
  13. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/_bench_common.py +5 -3
  14. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/bench_dense.py +4 -0
  15. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/bench_ragged.py +33 -28
  16. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/test_loaderx.py +213 -95
  17. {loaderx-2.0.7 → loaderx-2.3.3}/src/record/engine.zig +256 -296
  18. {loaderx-2.0.7 → loaderx-2.3.3}/src/record/executor.zig +12 -16
  19. loaderx-2.3.3/src/record/storage.zig +411 -0
  20. {loaderx-2.0.7 → loaderx-2.3.3}/src/store.zig +18 -35
  21. {loaderx-2.0.7 → loaderx-2.3.3}/src/zsampler.zig +19 -28
  22. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/bits.h +92 -87
  23. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/bitstream.h +26 -29
  24. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/compiler.h +36 -22
  25. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/cpu.h +1 -1
  26. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/debug.h +0 -9
  27. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/error_private.c +1 -0
  28. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/error_private.h +0 -10
  29. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/fse.h +2 -17
  30. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/fse_decompress.c +2 -0
  31. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/huf.h +0 -9
  32. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/mem.h +7 -11
  33. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/pool.h +0 -9
  34. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/portability_macros.h +22 -9
  35. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/threading.h +0 -8
  36. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/xxhash.h +93 -19
  37. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_deps.h +12 -0
  38. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_internal.h +1 -69
  39. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_trace.h +5 -12
  40. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/hist.c +10 -0
  41. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/hist.h +7 -0
  42. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress.c +1057 -367
  43. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_internal.h +227 -125
  44. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_literals.c +1 -1
  45. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_sequences.c +7 -7
  46. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_sequences.h +7 -6
  47. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_superblock.c +17 -17
  48. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_cwksp.h +41 -24
  49. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_double_fast.c +58 -50
  50. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_double_fast.h +4 -12
  51. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_fast.c +91 -74
  52. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_fast.h +4 -12
  53. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_lazy.c +64 -64
  54. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_lazy.h +30 -39
  55. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm.c +48 -33
  56. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm.h +6 -14
  57. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_opt.c +55 -51
  58. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_opt.h +8 -16
  59. loaderx-2.3.3/vendor/zstd/lib/compress/zstd_preSplit.c +238 -0
  60. loaderx-2.3.3/vendor/zstd/lib/compress/zstd_preSplit.h +33 -0
  61. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstdmt_compress.c +134 -93
  62. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstdmt_compress.h +4 -15
  63. loaderx-2.3.3/vendor/zstd/lib/decompress/huf_decompress_amd64.S +602 -0
  64. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress.c +14 -11
  65. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_block.c +6 -12
  66. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_internal.h +5 -5
  67. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/cover.c +60 -19
  68. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/divsufsort.h +0 -10
  69. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/zdict.c +2 -2
  70. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/zdict.h +15 -8
  71. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/zstd.h +241 -132
  72. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/zstd_errors.h +1 -8
  73. loaderx-2.0.7/src/record/storage.zig +0 -341
  74. {loaderx-2.0.7 → loaderx-2.3.3}/LICENSE +0 -0
  75. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/_lib.py +0 -0
  76. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx/dataloader.py +0 -0
  77. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/dependency_links.txt +0 -0
  78. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/requires.txt +0 -0
  79. {loaderx-2.0.7 → loaderx-2.3.3}/loaderx.egg-info/top_level.txt +0 -0
  80. {loaderx-2.0.7 → loaderx-2.3.3}/pyproject.toml +0 -0
  81. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/bench.py +0 -0
  82. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/build_wheels.py +0 -0
  83. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/prepare_tokens.py +0 -0
  84. {loaderx-2.0.7 → loaderx-2.3.3}/scripts/requirements-bench.txt +0 -0
  85. {loaderx-2.0.7 → loaderx-2.3.3}/setup.cfg +0 -0
  86. {loaderx-2.0.7 → loaderx-2.3.3}/setup.py +0 -0
  87. {loaderx-2.0.7 → loaderx-2.3.3}/src/zstd/c.zig +0 -0
  88. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/COPYING +0 -0
  89. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/LICENSE +0 -0
  90. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/allocations.h +0 -0
  91. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/debug.c +0 -0
  92. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/entropy_common.c +0 -0
  93. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/pool.c +0 -0
  94. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/threading.c +0 -0
  95. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/xxhash.c +0 -0
  96. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_common.c +0 -0
  97. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/clevels.h +0 -0
  98. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/fse_compress.c +0 -0
  99. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/huf_compress.c +0 -0
  100. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_literals.h +0 -0
  101. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_superblock.h +0 -0
  102. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm_geartab.h +0 -0
  103. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/huf_decompress.c +0 -0
  104. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_ddict.c +0 -0
  105. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_ddict.h +0 -0
  106. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_block.h +0 -0
  107. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/cover.h +0 -0
  108. {loaderx-2.0.7 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/divsufsort.c +0 -0
  109. {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.0.7
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`` 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
@@ -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. Dense
292
- schema contains only ``dtype`` and ``item_shape``; Ragged schema contains only
293
- ``dtype``. Structured, subarray, object, and metadata-bearing dtypes are not
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 4064 bytes available in the fixed 4096-byte
296
- metadata page. Ragged schema size is effectively fixed; Dense schema size grows
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 19. The dictionary
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, codec='zstd_dict', dict_bytes=d) as ds:
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, and ``dtype`` is all the destination needs. The
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 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
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.3+deb13-amd64, x86_64 |
560
- | python | CPython 3.14.7 (standard GIL build), numpy 2.5.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 | 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 |
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.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
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 | 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 |
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.8x LMDB and 3.2x Arrow IPC in logical gather. Zrecord-zstd
629
- delivers 9.7 GiB/s of
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.52x Ragged, and zstdict is
637
- 22.74x versus 23.58x. The remaining difference comes from the H/W distribution
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 | 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 |
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.81 Mrecords/s while retaining independent
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.00–1.15 Mrecords/s. LMDB's B-tree/page overhead is visible in both throughput
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 | 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 |
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 534679 krecords/s with 0.52–0.66 ms p95;
703
+ Zrecord's three codecs return 595735 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 | 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 |
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.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.
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.74x.
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.53x Torch fork, 1.50x Torch spawn and 3.59x Grain;
824
- raw loaderx is 1.97x, 1.93x and 4.62x faster, respectively.
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. Parallel chunks may finish
1051
- and occupy `data.zr` in a different physical order, but logical ID is the
1052
- stable append position and `RecordLoc[ID]` preserves it. Index and slice
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 ndim][u64le dims...][payload]`. Shape and payload therefore share one
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 19) |
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.74x with the balanced dictionary. The dictionary is loaded once on
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 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.
1112
-
1113
- Native storage uses a fixed file set:
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 static Header/schema page + RecordLoc table
1117
- ├── data.zr payload stream
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 32-byte Header, then the opaque MsgPack schema and unused zero padding.
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 `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
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 32 bytes of `meta.zr`. The format
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
- * `tail_offset` (u64) is the absolute committed frontier in `data.zr`.
1148
- * `schema_length` (u32) is the occupied prefix of the static schema area and
1149
- must be in `1..4064`.
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
- tail_offset: u64,
1156
- schema_length: u32,
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. Record table** — contiguous 16-byte entries start at offset 4096 in
1163
- `meta.zr` and grow as location runs are written. `offset` is an absolute byte offset in `data.zr`;
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
- **4. No fixed record-count cap.** The table and payload stream grow naturally in
1179
- their fixed files. The practical bounds are the u64 count, supported positional
1180
- file offsets, 2 GiB per record, and disk.
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
- * 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
1197
- lock, and issue positional payload writes in parallel. Logical IDs remain in
1198
- the loc table, so physical completion order does not change random gather.
1199
- The caller waits for every payload before publishing the new locations and
1200
- in-process length. Append does not overwrite the on-disk Header and is
1201
- intentionally lazy. Writer ``close()`` writes the current Header and closes
1202
- the files, making the page-cache state available for read-only open.
1203
- Every compressed frame owns one absolute range in `data.zr`; positional writes extend the file to the
1204
- current frontier. Python budgets the process-wide executor at three quarters
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 append preserves the stronger invariant already supplied by Python: every
1210
- record in one Dense or packed Ragged batch is a boundary inside one contiguous
1211
- source buffer. After locating the records, the engine consumes that buffer in
1212
- plan order and passes each contiguous planned range directly to
1213
- `writePositionalAll` on `data.zr`. There is no per-record iovec construction,
1214
- byte-budget flush, payload copy, or platform-specific syscall path. Zig's
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 `meta.zr` and `data.zr` handles require no rollover fd-table
1221
- synchronization; record I/O itself stays lock free.
1222
- * Every record is read at the offset its table entry records — the record table
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
- * Lanes receive contiguous blocks rather than a strided subset, keeping each
1235
- worker's reads and writes sequential.
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
- * Payload: one naturally growing `data.zr`, accessed concurrently through
1247
- `readPositionalAll`/`writePositionalAll`. No path depends on filesystem
1248
- sparse-file support.
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