loaderx 2.2.1__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 (108) hide show
  1. {loaderx-2.2.1/loaderx.egg-info → loaderx-2.3.3}/PKG-INFO +102 -79
  2. {loaderx-2.2.1 → loaderx-2.3.3}/README.md +101 -78
  3. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx/__init__.py +1 -1
  4. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx/_store.py +8 -4
  5. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx/zrecord.py +23 -7
  6. {loaderx-2.2.1 → loaderx-2.3.3/loaderx.egg-info}/PKG-INFO +102 -79
  7. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/_bench_common.py +5 -3
  8. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/bench_dense.py +4 -0
  9. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/bench_ragged.py +9 -3
  10. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/test_loaderx.py +90 -54
  11. {loaderx-2.2.1 → loaderx-2.3.3}/src/record/engine.zig +202 -241
  12. {loaderx-2.2.1 → loaderx-2.3.3}/src/record/executor.zig +0 -10
  13. {loaderx-2.2.1 → loaderx-2.3.3}/src/record/storage.zig +125 -55
  14. {loaderx-2.2.1 → loaderx-2.3.3}/src/store.zig +5 -10
  15. {loaderx-2.2.1 → loaderx-2.3.3}/LICENSE +0 -0
  16. {loaderx-2.2.1 → loaderx-2.3.3}/MANIFEST.in +0 -0
  17. {loaderx-2.2.1 → loaderx-2.3.3}/build.zig +0 -0
  18. {loaderx-2.2.1 → loaderx-2.3.3}/build.zig.zon +0 -0
  19. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx/_lib.py +0 -0
  20. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx/dataloader.py +0 -0
  21. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx/utils.py +0 -0
  22. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx/zsampler.py +0 -0
  23. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx.egg-info/SOURCES.txt +0 -0
  24. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx.egg-info/dependency_links.txt +0 -0
  25. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx.egg-info/requires.txt +0 -0
  26. {loaderx-2.2.1 → loaderx-2.3.3}/loaderx.egg-info/top_level.txt +0 -0
  27. {loaderx-2.2.1 → loaderx-2.3.3}/pyproject.toml +0 -0
  28. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/bench.py +0 -0
  29. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/build_wheels.py +0 -0
  30. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/prepare_tokens.py +0 -0
  31. {loaderx-2.2.1 → loaderx-2.3.3}/scripts/requirements-bench.txt +0 -0
  32. {loaderx-2.2.1 → loaderx-2.3.3}/setup.cfg +0 -0
  33. {loaderx-2.2.1 → loaderx-2.3.3}/setup.py +0 -0
  34. {loaderx-2.2.1 → loaderx-2.3.3}/src/zsampler.zig +0 -0
  35. {loaderx-2.2.1 → loaderx-2.3.3}/src/zstd/c.zig +0 -0
  36. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/COPYING +0 -0
  37. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/LICENSE +0 -0
  38. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/allocations.h +0 -0
  39. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/bits.h +0 -0
  40. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/bitstream.h +0 -0
  41. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/compiler.h +0 -0
  42. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/cpu.h +0 -0
  43. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/debug.c +0 -0
  44. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/debug.h +0 -0
  45. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/entropy_common.c +0 -0
  46. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/error_private.c +0 -0
  47. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/error_private.h +0 -0
  48. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/fse.h +0 -0
  49. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/fse_decompress.c +0 -0
  50. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/huf.h +0 -0
  51. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/mem.h +0 -0
  52. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/pool.c +0 -0
  53. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/pool.h +0 -0
  54. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/portability_macros.h +0 -0
  55. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/threading.c +0 -0
  56. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/threading.h +0 -0
  57. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/xxhash.c +0 -0
  58. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/xxhash.h +0 -0
  59. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_common.c +0 -0
  60. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_deps.h +0 -0
  61. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_internal.h +0 -0
  62. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/common/zstd_trace.h +0 -0
  63. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/clevels.h +0 -0
  64. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/fse_compress.c +0 -0
  65. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/hist.c +0 -0
  66. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/hist.h +0 -0
  67. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/huf_compress.c +0 -0
  68. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress.c +0 -0
  69. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_internal.h +0 -0
  70. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_literals.c +0 -0
  71. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_literals.h +0 -0
  72. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_sequences.c +0 -0
  73. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_sequences.h +0 -0
  74. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_superblock.c +0 -0
  75. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_compress_superblock.h +0 -0
  76. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_cwksp.h +0 -0
  77. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_double_fast.c +0 -0
  78. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_double_fast.h +0 -0
  79. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_fast.c +0 -0
  80. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_fast.h +0 -0
  81. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_lazy.c +0 -0
  82. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_lazy.h +0 -0
  83. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm.c +0 -0
  84. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm.h +0 -0
  85. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_ldm_geartab.h +0 -0
  86. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_opt.c +0 -0
  87. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_opt.h +0 -0
  88. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_preSplit.c +0 -0
  89. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstd_preSplit.h +0 -0
  90. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstdmt_compress.c +0 -0
  91. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/compress/zstdmt_compress.h +0 -0
  92. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/huf_decompress.c +0 -0
  93. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/huf_decompress_amd64.S +0 -0
  94. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_ddict.c +0 -0
  95. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_ddict.h +0 -0
  96. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress.c +0 -0
  97. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_block.c +0 -0
  98. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_block.h +0 -0
  99. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/decompress/zstd_decompress_internal.h +0 -0
  100. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/cover.c +0 -0
  101. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/cover.h +0 -0
  102. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/divsufsort.c +0 -0
  103. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/divsufsort.h +0 -0
  104. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/fastcover.c +0 -0
  105. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/dictBuilder/zdict.c +0 -0
  106. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/zdict.h +0 -0
  107. {loaderx-2.2.1 → loaderx-2.3.3}/vendor/zstd/lib/zstd.h +0 -0
  108. {loaderx-2.2.1 → loaderx-2.3.3}/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.3
4
4
  Summary: Rebuildable high-performance ordered record containers
5
5
  Author-email: Ben0i0d <ben0i0d@foxmail.com>
6
6
  License-Expression: MIT
@@ -269,7 +269,7 @@ buffer, then completes the native append before returning. Nothing is inferred.
269
269
  ```python
270
270
  from loaderx.zrecord import Dense, Ragged
271
271
 
272
- 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)
273
273
  ds.append(images[i:i + 1024]) # synchronous native batch; returns None
274
274
  ds.append(single_image[None]) # one sample is batch_size 1 — add the axis yourself
275
275
  ds.close() # publish before opening
@@ -282,6 +282,11 @@ with Dense.open('mnist/x') as ds:
282
282
  first_four = ds[:4] # opened containers are read-only
283
283
  ```
284
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
+
285
290
  Dense and Ragged append both report native errors in the current call and return
286
291
  only after accepting the batch. A writer cannot be read, and a reader cannot be
287
292
  appended to. ``close()`` on a writer publishes the container Header;
@@ -295,7 +300,7 @@ schemas contain ``dtype`` and ``ndim``; Dense additionally contains
295
300
  appended record to have that rank, while every dimension length may vary.
296
301
  Structured, subarray, object, metadata-bearing, and zero-itemsize dtypes are not
297
302
  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
303
+ string. The encoded schema has 2040 bytes available in the fixed 4096-byte
299
304
  metadata page. Ragged schema size is fixed; Dense schema size grows
300
305
  only with the integer ``item_shape``, so the physical limit is far above any
301
306
  practical NumPy array rank.
@@ -502,8 +507,8 @@ so CFFI, NumPy allocation, and Ragged list/shape reconstruction are timed.
502
507
  ### Methodology
503
508
 
504
509
  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
510
+ Zrecord Store rows and the two loaderx identity rows were refreshed on 2.3.0
511
+ with the default `data_shards=4`. Unchanged
507
512
  optional Store backends, sampler rows and external loader rows remain from the
508
513
  immediately preceding 2.0.9 run. These are not three-run medians: an unexpected result is traced
509
514
  separately instead of being hidden by repeated aggregation. Within each store workload every
@@ -583,9 +588,9 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
583
588
 
584
589
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
585
590
  |---|---:|---:|---:|---:|---:|---:|
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 |
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 |
589
594
  | npy-mmap-raw | 2004 MiB/s | 4749 MiB/s | 33.1 | 11.08 ms | 358.9 MiB | 1.00x |
590
595
  | hdf5-raw | 2350 MiB/s | 1836 MiB/s | 12.8 | 29.68 ms | 359.0 MiB | 1.00x |
591
596
  | hdf5-gzip | 277 MiB/s | 657 MiB/s | 4.6 | 62.53 ms | 26.1 MiB | 13.73x |
@@ -599,8 +604,8 @@ Fixed-resolution vision records — 147 KiB per record, 36.8 MiB per batch:
599
604
  | tiledb-raw | 743 MiB/s | 630 MiB/s | 4.4 | 66.01 ms | 359.0 MiB | 1.00x |
600
605
  | tiledb-zstd | 1498 MiB/s | 1503 MiB/s | 10.5 | 27.82 ms | 26.9 MiB | 13.36x |
601
606
 
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
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
604
609
  IPC are competitive raw record
605
610
  stores, while codecs tied to whole IPC batches or Parquet row groups pay read
606
611
  amplification on random gathers. Dense demonstrates that
@@ -618,9 +623,9 @@ list or a one-dimensional variable-length abstraction is not enough.
618
623
 
619
624
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
620
625
  |---|---:|---:|---:|---:|---:|---:|
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 |
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 |
624
629
  | hdf5-raw | 1451 MiB/s | 1059 MiB/s | 6.4 | 44.91 ms | 418.0 MiB | 1.00x |
625
630
  | hdf5-gzip | 250 MiB/s | 126 MiB/s | 0.7 | 359.23 ms | 29.9 MiB | 13.94x |
626
631
  | lmdb-raw | 1717 MiB/s | 7269 MiB/s | 43.6 | 7.93 ms | 422.2 MiB | 0.99x |
@@ -633,16 +638,16 @@ list or a one-dimensional variable-length abstraction is not enough.
633
638
  | tiledb-raw | 392 MiB/s | 48 MiB/s | 0.3 | 941.09 ms | 417.2 MiB | 1.00x |
634
639
  | tiledb-zstd | 675 MiB/s | 138 MiB/s | 0.8 | 328.05 ms | 27.0 MiB | 15.46x |
635
640
 
636
- Zrecord-raw is 1.8x LMDB and 3.2x Arrow IPC in logical gather. Zrecord-zstd
637
- delivers 9.3 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
638
643
  logical payload while reducing the corpus to 26.9 MiB. HDF5, Arrow IPC, Parquet,
639
644
  ArrayRecord and TileDB
640
645
  show the same framework/codec tradeoffs in both tables; compressed batch, chunk
641
646
  and row-group formats pay read amplification on random records.
642
647
 
643
648
  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
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
646
651
  and Ragged shape metadata, not a different image entropy model.
647
652
 
648
653
  ### Small Token Records
@@ -662,9 +667,9 @@ accumulate at least two seconds.
662
667
 
663
668
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
664
669
  |---|---:|---:|---:|---:|---:|---:|
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 |
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 |
668
673
  | npy-mmap-raw | 1587 MiB/s | 9070 MiB/s | 4644.0 | 0.08 ms | 390.6 MiB | 1.00x |
669
674
  | lmdb-raw | 639 MiB/s | 1104 MiB/s | 565.0 | 0.71 ms | 786.3 MiB | 0.50x |
670
675
  | arrow-ipc-raw | 2068 MiB/s | 225 MiB/s | 115.0 | 2.81 ms | 390.8 MiB | 1.00x |
@@ -672,9 +677,9 @@ accumulate at least two seconds.
672
677
  | arrayrecord-zstd | 127 MiB/s | 135 MiB/s | 69.4 | 4.45 ms | 201.3 MiB | 1.94x |
673
678
 
674
679
  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
680
+ typed matrix. Zrecord-raw reaches 3.27 Mrecords/s while retaining independent
676
681
  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
682
+ 1.03–1.06 Mrecords/s. LMDB's B-tree/page overhead is visible in both throughput
678
683
  and disk.
679
684
 
680
685
  #### Variable Token Sequences
@@ -686,16 +691,16 @@ with exact `int32` values and original one-dimensional shapes.
686
691
 
687
692
  | backend | logical write | logical gather | krecords/s | p95 | disk | ratio |
688
693
  |---|---:|---:|---:|---:|---:|---:|
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 |
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 |
692
697
  | lmdb-raw | 318 MiB/s | 113 MiB/s | 214.3 | 1.48 ms | 153.0 MiB | 0.69x |
693
698
  | arrow-ipc-raw | 562 MiB/s | 44 MiB/s | 83.1 | 4.00 ms | 109.9 MiB | 0.96x |
694
699
  | arrayrecord-raw | 288 MiB/s | 26 MiB/s | 49.3 | 7.39 ms | 118.8 MiB | 0.89x |
695
700
  | arrayrecord-zstd | 62 MiB/s | 33 MiB/s | 61.5 | 5.58 ms | 76.2 MiB | 1.39x |
696
701
 
697
702
  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;
703
+ Zrecord's three codecs return 595735 krecords/s with 0.50–0.60 ms p95;
699
704
  the dictionary gives the best disk ratio and is slightly ahead of plain zstd in
700
705
  this pass.
701
706
 
@@ -742,8 +747,8 @@ and Grain reads ArrayRecord.
742
747
 
743
748
  | loader | model | storage | batches/s | p95 | steady PSS | peak PSS | peak RSS |
744
749
  |---|---|---|---:|---:|---:|---:|---:|
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 |
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 |
747
752
  | torch | fork | npy-mmap-raw | 109.4 | 32.70 ms | 1783 MiB | 1889 MiB | 6396 MiB |
748
753
  | torch-spawn | spawn | npy-mmap-raw | 111.6 | 30.69 ms | 2835 MiB | 2913 MiB | 4880 MiB |
749
754
  | grain | processes | arrayrecord-zstd | 46.6 | 93.48 ms | 1847 MiB | 1946 MiB | 2065 MiB |
@@ -753,8 +758,8 @@ the transform threads overlap Python-side collation with the next gather. The
753
758
  memory is the source, Zrecord container and bounded in-flight batches. loaderx prefetches in
754
759
  threads inside one process, so workers share one interpreter, one NumPy runtime
755
760
  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.
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.
758
763
  Torch's aggregate RSS is high because
759
764
  Linux fork mappings are counted repeatedly; it is not a total-memory ratio
760
765
  against Zrecord's unaccounted page cache. The explicit `torch-spawn` row removes
@@ -828,8 +833,8 @@ format: in the current Dense structured-vision workload, plain zstd reaches
828
833
  **Loader results combine architecture and storage.** loaderx uses threads and
829
834
  never ends an epoch, so a step pays no IPC and never waits on an epoch boundary;
830
835
  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.
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.
833
838
  Storage also differs per loader — each reads from what it was
834
839
  built for — so the loader table is a different comparison from either store
835
840
  table, not a rerun.
@@ -1055,9 +1060,9 @@ license to trust storage or the operating system: native code still validates
1055
1060
  normal I/O behavior, basic malformed-store rejection and native memory safety; checks that only
1056
1061
  defend against bypassing the public Python API do not belong in zrecord.
1057
1062
 
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
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
1061
1066
  operations are implemented as ordered gathers over those positions.
1062
1067
  2. It hands the container layer a dense sequence space: records are exactly
1063
1068
  `0..N-1`. Named streams are composed dynamically by a plain Python dict;
@@ -1120,24 +1125,33 @@ the authority for irreplaceable data. Keep authoritative source data and reprodu
1120
1125
  build scripts; after an interrupted build, storage failure, incompatible implementation
1121
1126
  change, or content change, rebuild a complete container at a new path.
1122
1127
 
1123
- Native storage uses a fixed file set:
1128
+ Native storage uses one metadata file and a create-time-fixed payload file set:
1124
1129
  ```
1125
1130
  store/
1126
- ├── meta.zr 4096-byte static Header/schema page + RecordLoc table
1127
- ├── 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
1128
1135
  └── dict.zr zstd dictionary (only in dict stores)
1129
1136
  ```
1130
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
+
1131
1144
  ### Metadata (meta.zr)
1132
1145
 
1133
1146
  Files are read and written **positionally** — pread/pwrite at computed offsets,
1134
1147
  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.
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.
1136
1150
  An array of 16-byte `RecordLoc`s starts at offset 4096. Record `i` is one
1137
1151
  pread/pwrite at `4096 + i * 16`; there is no variable table base, segment
1138
1152
  mapping, or rollover fd table.
1139
1153
 
1140
- **1. Python schema** — bytes `32..32+schema_length` are exactly one immutable
1154
+ **1. Python schema** — bytes `2056..2056+schema_length` are exactly one immutable
1141
1155
  MsgPack object. Both stores contain ``dtype`` and ``ndim``; Dense additionally
1142
1156
  contains ``item_shape``. Native create persists these bytes together with
1143
1157
  the physical container but does not decode them. Open acquires the native lifetime
@@ -1147,30 +1161,36 @@ dtype/item_shape and passed to the native handle as runtime geometry; it is not
1147
1161
  independently persisted as a second authority. There is no format version or
1148
1162
  legacy kind dispatch.
1149
1163
 
1150
- **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
1151
1165
  deliberately carries no payload or metadata checksum.
1152
1166
 
1153
1167
  * `codec` is the store's one compression method, stamped at creation and
1154
1168
  immutable — there is no per-record tag anywhere.
1155
1169
  * `length` (u64) is the physical record count; it equals logical length for both
1156
1170
  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`.
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`.
1160
1174
 
1161
1175
  ```zig
1162
1176
  const Codec = enum(u8) { raw = 0, zstd = 1, zstdict = 2, _ };
1163
1177
  const Header = extern struct {
1164
1178
  length: u64,
1165
- tail_offset: u64,
1166
- schema_length: u32,
1167
- reserved: [11]u8,
1179
+ schema_length: u16,
1180
+ data_shards: u8,
1168
1181
  codec: u8,
1182
+ reserved: [4]u8,
1169
1183
  };
1170
1184
  ```
1171
1185
 
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`;
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`;
1174
1194
  `phys_length`/`logic_length` are the stored and original sizes. The
1175
1195
  codec is not here: it is the header's, so a record is stored exactly the way the
1176
1196
  store is declared.
@@ -1185,9 +1205,9 @@ const RecordLoc = extern struct {
1185
1205
 
1186
1206
  There is no liveness flag. Every entry below `length` is a record.
1187
1207
 
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.
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.
1191
1211
 
1192
1212
  ## Executor
1193
1213
 
@@ -1202,34 +1222,36 @@ while `Ragged` supplies offsets for its shape-prefixed records.
1202
1222
  The private ABI turns those inputs into compile-time record sources and
1203
1223
  destinations; the engine has one append and one gather operation. Its shared
1204
1224
  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
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
1215
1239
  of the logical CPUs available to the process, leaving headroom for packing,
1216
1240
  transforms, and the caller without encoding a platform-specific thread count.
1217
1241
  Each producer configures its CCtx or shared immutable CDict once, then starts
1218
1242
  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.
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.
1227
1249
  **2. Read.** Fill the destination memory concurrently, in place from the Python
1228
1250
  side (executed on async threads).
1229
1251
  * 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
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
1233
1255
  is addressed by pure arithmetic, so random access is one pread for the
1234
1256
  location and one for the bytes, with no batching assumptions about layout.
1235
1257
  Each lane reads one location and immediately reads/decompresses that record;
@@ -1241,8 +1263,9 @@ budget; lanes for which the runtime cannot reserve concurrency run inline on the
1241
1263
  calling thread. Python configures the process-level budget as
1242
1264
  `max(physical cores, logical cores * 3 / 4)`, using platform topology where
1243
1265
  available.
1244
- * Lanes receive contiguous blocks rather than a strided subset, keeping each
1245
- 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.
1246
1269
  * Each lane creates one zstd context (`ZSTD_CCtx` to write, `ZSTD_DCtx` to
1247
1270
  read) and reuses it across every record it handles, rather than paying that
1248
1271
  setup per record. The dictionary (`ZSTD_CDict`/`ZSTD_DDict`) is immutable, so
@@ -1252,10 +1275,10 @@ available.
1252
1275
 
1253
1276
  **4. File access.**
1254
1277
  * 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.
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.
1259
1282
 
1260
1283
  **Execution model.** Opened readers are immutable, so calls on the same reader may
1261
1284
  gather concurrently. Creator appends are synchronous and native Storage