loaderx 2.2.1__tar.gz → 2.3.5__tar.gz

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