tensorfs 0.3.64__tar.gz → 0.3.65__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 (157) hide show
  1. {tensorfs-0.3.64 → tensorfs-0.3.65}/Cargo.lock +2 -2
  2. {tensorfs-0.3.64 → tensorfs-0.3.65}/Cargo.toml +1 -1
  3. {tensorfs-0.3.64 → tensorfs-0.3.65}/PKG-INFO +1 -1
  4. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/part_rows_tests.rs +157 -0
  5. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/progress.rs +10 -0
  6. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/resume_tests.rs +13 -3
  7. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived.rs +166 -10
  8. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/durability.rs +67 -27
  9. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/meta.rs +11 -0
  10. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-py/src/store.rs +7 -1
  11. {tensorfs-0.3.64 → tensorfs-0.3.65}/pyproject.toml +1 -1
  12. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/Cargo.toml +0 -0
  13. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/build.rs +0 -0
  14. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  15. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  16. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  17. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  18. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/asset_update.rs +0 -0
  19. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/b64.rs +0 -0
  20. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  21. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  22. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  23. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  24. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  25. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  26. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  27. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  28. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  29. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  30. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/canon.rs +0 -0
  31. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/capability.rs +0 -0
  32. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/catalog.rs +0 -0
  33. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/cbor.rs +0 -0
  34. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  35. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  36. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/corpus.rs +0 -0
  37. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  38. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  39. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  40. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  41. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  42. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  43. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/descriptors.rs +0 -0
  44. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/dtype.rs +0 -0
  45. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/err.rs +0 -0
  46. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/fetch.rs +0 -0
  47. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/fill.rs +0 -0
  48. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/fit.rs +0 -0
  49. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/gc.rs +0 -0
  50. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/header.rs +0 -0
  51. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/home.rs +0 -0
  52. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ids.rs +0 -0
  53. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  54. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  55. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  56. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  57. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  58. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  59. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  60. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  61. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  62. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  63. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  64. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  65. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/jcs.rs +0 -0
  66. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/lib.rs +0 -0
  67. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/limits.rs +0 -0
  68. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/machine.rs +0 -0
  69. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/manifest.rs +0 -0
  70. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/project.rs +0 -0
  71. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/providers.rs +0 -0
  72. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/read.rs +0 -0
  73. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/receipt.rs +0 -0
  74. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/reclaim.rs +0 -0
  75. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/registry.rs +0 -0
  76. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/relation.rs +0 -0
  77. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  78. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  79. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/repository.rs +0 -0
  80. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/sha256.rs +0 -0
  81. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  82. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/spec.rs +0 -0
  83. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/staging.rs +0 -0
  84. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/stats.rs +0 -0
  85. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/storage.rs +0 -0
  86. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/store.rs +0 -0
  87. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/api.rs +0 -0
  88. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/http.rs +0 -0
  89. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  90. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  91. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  92. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  93. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  94. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/pace.rs +0 -0
  95. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  96. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  97. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  98. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/push.rs +0 -0
  99. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  100. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  101. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  102. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/src/vectors.rs +0 -0
  103. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  104. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  105. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  106. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  107. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  108. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  109. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fit.rs +0 -0
  110. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  111. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  112. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  113. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  114. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  115. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  116. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  117. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  118. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  119. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  120. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/home.rs +0 -0
  121. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  122. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  123. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/liveness.rs +0 -0
  124. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  125. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  126. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  127. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  128. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  129. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  130. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  131. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  132. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  133. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  134. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_download.rs +0 -0
  135. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  136. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  137. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  138. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/staging.rs +0 -0
  139. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  140. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  141. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  142. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  143. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/version.rs +0 -0
  144. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-core/tests/write_through.rs +0 -0
  145. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-py/Cargo.toml +0 -0
  146. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-py/src/docs.rs +0 -0
  147. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-py/src/errors.rs +0 -0
  148. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-py/src/ingest.rs +0 -0
  149. {tensorfs-0.3.64 → tensorfs-0.3.65}/crates/tensorfs-py/src/lib.rs +0 -0
  150. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/__init__.py +0 -0
  151. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/_derived_channel.py +0 -0
  152. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/_ext.pyi +0 -0
  153. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/_source_channel.py +0 -0
  154. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/_tensors.py +0 -0
  155. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/derived.py +0 -0
  156. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/errors.py +0 -0
  157. {tensorfs-0.3.64 → tensorfs-0.3.65}/python/tensorfs/py.typed +0 -0
@@ -439,7 +439,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
439
439
 
440
440
  [[package]]
441
441
  name = "tensorfs-core"
442
- version = "0.3.64"
442
+ version = "0.3.65"
443
443
  dependencies = [
444
444
  "fs2",
445
445
  "libc",
@@ -454,7 +454,7 @@ dependencies = [
454
454
 
455
455
  [[package]]
456
456
  name = "tensorfs-py"
457
- version = "0.3.64"
457
+ version = "0.3.65"
458
458
  dependencies = [
459
459
  "pyo3",
460
460
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.64"
6
+ version = "0.3.65"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.64
3
+ Version: 0.3.65
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -44,6 +44,7 @@ fn add(store: &Store, meta: &Meta, id: &str, writer: &Begin, key: &str, byte: u8
44
44
  id,
45
45
  1,
46
46
  &writer.additions,
47
+ None,
47
48
  ("model", key, "value"),
48
49
  &mut [byte; 512].as_slice(),
49
50
  )
@@ -112,6 +113,7 @@ fn parts_are_rows_that_resume_refuse_conflicts_and_stay_retained() {
112
113
  &id,
113
114
  2,
114
115
  &resumed.additions,
116
+ None,
115
117
  ("model", "weight_2", "value"),
116
118
  &mut [3u8; 512].as_slice(),
117
119
  )
@@ -167,3 +169,158 @@ fn an_older_builds_inline_row_and_a_torn_journal_line_are_read() {
167
169
  assert!(!journal.exists());
168
170
  std::fs::remove_dir_all(store.root()).unwrap();
169
171
  }
172
+
173
+ #[test]
174
+ fn indexed_checkpoints_write_the_chain_full_checkpoints_write() {
175
+ const COUNT: usize = 40;
176
+ let mut stores = Vec::new();
177
+ for label in ["full", "indexed"] {
178
+ let (store, meta, id, mut declaration) = fixture(label);
179
+ let prototype = declaration.components[0].add[0].clone();
180
+ declaration.components[0].add = (0..COUNT)
181
+ .map(|index| {
182
+ let mut tensor = prototype.clone();
183
+ tensor.key = format!("weight_{index:03}");
184
+ tensor
185
+ })
186
+ .collect();
187
+ declaration.order = (0..COUNT)
188
+ .map(|index| ("model".into(), format!("weight_{index:03}")))
189
+ .collect();
190
+ declaration.configs = vec![ConfigDeclaration::Add {
191
+ target: "model.json".into(),
192
+ }];
193
+ declaration.max_new_bytes = (COUNT * 512 + 64) as u64;
194
+ let writer = begin(&store, &meta, &id, 1, declaration.clone(), None).unwrap();
195
+ stores.push((store, meta, id, declaration, writer));
196
+ }
197
+ let mut cursor = None;
198
+ let (mut full_prior, mut indexed_prior) = (None, None);
199
+ let mut heads: Vec<ObjectRef> = Vec::new();
200
+ for index in 0..COUNT {
201
+ let key = format!("weight_{index:03}");
202
+ let payload = [index as u8; 512];
203
+ let (store, meta, id, _, writer) = &stores[0];
204
+ add_indexed_part(
205
+ store,
206
+ meta,
207
+ id,
208
+ 1,
209
+ &writer.additions,
210
+ None,
211
+ ("model", &key, "value"),
212
+ &mut payload.as_slice(),
213
+ )
214
+ .unwrap();
215
+ let full = checkpoint_progress(store, meta, id, 1, "request", "model", full_prior.as_ref())
216
+ .unwrap();
217
+ let (store, meta, id, _, writer) = &stores[1];
218
+ add_indexed_part(
219
+ store,
220
+ meta,
221
+ id,
222
+ 1,
223
+ &writer.additions,
224
+ cursor.as_mut(),
225
+ ("model", &key, "value"),
226
+ &mut payload.as_slice(),
227
+ )
228
+ .unwrap();
229
+ // An older acknowledged head sends the writer down the full path, which re-seeds it.
230
+ let previous = if index == 25 {
231
+ heads.get(heads.len() - 3).cloned()
232
+ } else {
233
+ indexed_prior.clone()
234
+ };
235
+ let indexed = checkpoint_indexed(
236
+ store,
237
+ meta,
238
+ id,
239
+ 1,
240
+ "request",
241
+ "model",
242
+ previous.as_ref(),
243
+ &mut cursor,
244
+ )
245
+ .unwrap();
246
+ assert_eq!(full, indexed, "checkpoint {index} diverged");
247
+ assert_eq!(cursor.as_ref().unwrap().tip.head, indexed.1);
248
+ full_prior = full.1.head;
249
+ indexed_prior = indexed.1.head;
250
+ heads.push(indexed_prior.clone().unwrap());
251
+ if index == 20 {
252
+ for (store, meta, id, _, _) in &stores {
253
+ add_config(meta, id, 1, "model.json", &mut b"{}".as_slice()).unwrap();
254
+ let _ = store;
255
+ }
256
+ cursor = None;
257
+ }
258
+ }
259
+ let (store, meta, id, declaration, writer) = stores.pop().unwrap();
260
+ let head = indexed_prior.unwrap();
261
+ let canonical = String::from_utf8(declaration.clone().canonical_work_bytes().unwrap()).unwrap();
262
+ let restored = read_progress(&store, &head, &id, &canonical, &[]).unwrap();
263
+ assert_eq!(restored.added_parts.len(), COUNT);
264
+ assert_eq!(restored.added_configs.len(), 1);
265
+ fence(&meta, &id, 1).unwrap();
266
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
267
+ let resumed = begin(&store, &meta, &id, 2, declaration, Some(&head)).unwrap();
268
+ assert_eq!(completed(&meta, &id, 2).unwrap().0.len(), COUNT);
269
+ fence(&meta, &id, 2).unwrap();
270
+ release_guards(&meta, Some(resumed.writer_hold), resumed.source_leases);
271
+ std::fs::remove_dir_all(store.root()).unwrap();
272
+ for (store, meta, id, _, writer) in stores {
273
+ fence(&meta, &id, 1).unwrap();
274
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
275
+ std::fs::remove_dir_all(store.root()).unwrap();
276
+ }
277
+ }
278
+
279
+ #[test]
280
+ fn an_indexed_checkpoint_does_not_reread_its_chain() {
281
+ let (store, meta, id, declaration) = fixture("no-reread");
282
+ let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
283
+ let mut cursor = None;
284
+ let mut prior: Option<ObjectRef> = None;
285
+ let mut first = None;
286
+ for (index, key) in ["weight_0", "weight_1", "weight_2"].into_iter().enumerate() {
287
+ add_indexed_part(
288
+ &store,
289
+ &meta,
290
+ &id,
291
+ 1,
292
+ &writer.additions,
293
+ cursor.as_mut(),
294
+ ("model", key, "value"),
295
+ &mut [index as u8 + 1; 512].as_slice(),
296
+ )
297
+ .unwrap();
298
+ let (_, head) = checkpoint_indexed(
299
+ &store,
300
+ &meta,
301
+ &id,
302
+ 1,
303
+ "request",
304
+ "model",
305
+ prior.as_ref(),
306
+ &mut cursor,
307
+ )
308
+ .unwrap();
309
+ prior = head.head;
310
+ first.get_or_insert(prior.clone().unwrap());
311
+ if index == 0 {
312
+ // The chain's root link is gone: a checkpoint that walked the chain now fails.
313
+ let root = first.clone().unwrap();
314
+ std::fs::remove_file(store.object_path(root.sha256.trim_start_matches("sha256:")))
315
+ .unwrap();
316
+ assert!(
317
+ checkpoint_progress(&store, &meta, &id, 1, "request", "model", prior.as_ref())
318
+ .is_err()
319
+ );
320
+ }
321
+ }
322
+ assert_eq!(cursor.as_ref().unwrap().parts, 3);
323
+ fence(&meta, &id, 1).unwrap();
324
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
325
+ std::fs::remove_dir_all(store.root()).unwrap();
326
+ }
@@ -21,6 +21,16 @@ fn part_key(part: &AddedPart) -> (&str, &str, &str) {
21
21
  }
22
22
 
23
23
  impl Delta {
24
+ /// The parts a writer accepted since its last checkpoint, sorted as a delta stores them.
25
+ pub(super) fn accepted(work: ObjectRef, mut parts: Vec<AddedPart>) -> Self {
26
+ parts.sort_by(|left, right| part_key(left).cmp(&part_key(right)));
27
+ Self {
28
+ work,
29
+ parts,
30
+ configs: Vec::new(),
31
+ }
32
+ }
33
+
24
34
  pub(super) fn between(
25
35
  row: &TransactionRow,
26
36
  prior: Option<&TransactionRow>,
@@ -981,6 +981,7 @@ fn profile_derived_transaction_scaling() {
981
981
  let mut part_ms = 0.0;
982
982
  let mut checkpoint_ms = 0.0;
983
983
  let mut prior = None;
984
+ let mut cursor = None;
984
985
  for index in 0..writes {
985
986
  let key = format!("weight_{index:04}");
986
987
  let started = Instant::now();
@@ -990,15 +991,24 @@ fn profile_derived_transaction_scaling() {
990
991
  &id,
991
992
  1,
992
993
  &writer.additions,
994
+ cursor.as_mut(),
993
995
  ("model", &key, "value"),
994
996
  &mut [index as u8; 16].as_slice(),
995
997
  )
996
998
  .unwrap();
997
999
  part_ms += started.elapsed().as_secs_f64() * 1000.0;
998
1000
  let started = Instant::now();
999
- let (_, head) =
1000
- checkpoint_progress(&store, &meta, &id, 1, "profile", "model", prior.as_ref())
1001
- .unwrap();
1001
+ let (_, head) = checkpoint_indexed(
1002
+ &store,
1003
+ &meta,
1004
+ &id,
1005
+ 1,
1006
+ "profile",
1007
+ "model",
1008
+ prior.as_ref(),
1009
+ &mut cursor,
1010
+ )
1011
+ .unwrap();
1002
1012
  prior = head.head;
1003
1013
  checkpoint_ms += started.elapsed().as_secs_f64() * 1000.0;
1004
1014
  }
@@ -2711,6 +2711,158 @@ pub fn completed(
2711
2711
  ))
2712
2712
  }
2713
2713
 
2714
+ /// A writer's position in its checkpoint chain: the chain's tip and held blobs, its fixed
2715
+ /// plan and work context, and the parts it accepted since. With it a checkpoint writes and
2716
+ /// verifies only those parts instead of re-reading the chain it extends.
2717
+ #[derive(Debug, Clone)]
2718
+ pub struct CheckpointCursor {
2719
+ chain: String,
2720
+ plan: String,
2721
+ work: ObjectRef,
2722
+ tip: crate::durability::Tip,
2723
+ parts: usize,
2724
+ pending: Vec<AddedPart>,
2725
+ }
2726
+
2727
+ impl CheckpointCursor {
2728
+ /// Seed from a checkpoint the full path just wrote or confirmed. `None` when there is
2729
+ /// no head yet to extend.
2730
+ fn at(
2731
+ store: &Store,
2732
+ meta: &Meta,
2733
+ transaction: &str,
2734
+ writer_session: u64,
2735
+ chain: String,
2736
+ plan: &str,
2737
+ head: &crate::durability::Head,
2738
+ ) -> Result<Option<Self>> {
2739
+ let Some(tip) = head.head.as_ref() else {
2740
+ return Ok(None);
2741
+ };
2742
+ let mut rows: Vec<_> = meta.derived_row(transaction)?.into_iter().collect();
2743
+ let row = open_row(&mut rows, transaction, writer_session)?;
2744
+ let context = progress::context(row);
2745
+ let held = crate::durability::local_chain(store, tip, plan)?
2746
+ .blobs
2747
+ .into_iter()
2748
+ .map(|object| object.sha256)
2749
+ .collect();
2750
+ Ok(Some(Self {
2751
+ chain,
2752
+ plan: plan.to_string(),
2753
+ work: ObjectRef::of(&crate::canon::write(&context.to_value())),
2754
+ tip: crate::durability::Tip {
2755
+ head: head.clone(),
2756
+ held,
2757
+ },
2758
+ parts: row.added_parts.len(),
2759
+ pending: Vec::new(),
2760
+ }))
2761
+ }
2762
+
2763
+ /// Extend the chain by the pending parts, or `None` when anything but this writer's own
2764
+ /// accepted parts moved since the cursor was taken.
2765
+ fn advance(
2766
+ &mut self,
2767
+ store: &Store,
2768
+ meta: &Meta,
2769
+ transaction: &str,
2770
+ writer_session: u64,
2771
+ chain: &str,
2772
+ previous: Option<&ObjectRef>,
2773
+ ) -> Result<Option<(String, crate::durability::Head)>> {
2774
+ let tip = self.tip.head.head.clone();
2775
+ let root = roots::read(store, transaction)?;
2776
+ if self.chain != chain
2777
+ || previous.is_some_and(|previous| Some(previous) != tip.as_ref())
2778
+ || root.as_ref().and_then(|root| root.checkpoint.as_ref()) != tip.as_ref()
2779
+ {
2780
+ return Ok(None);
2781
+ }
2782
+ meta.check_session(transaction, writer_session)?;
2783
+ if meta.part_count(transaction)? != self.parts + self.pending.len() {
2784
+ return Ok(None);
2785
+ }
2786
+ if self.pending.is_empty() {
2787
+ return Ok(Some((self.plan.clone(), self.tip.head.clone())));
2788
+ }
2789
+ let parts = std::mem::take(&mut self.pending);
2790
+ let objects = parts
2791
+ .iter()
2792
+ .flat_map(|added| added.part.segments().iter().cloned())
2793
+ .collect();
2794
+ let count = parts.len();
2795
+ let delta = progress::Delta::accepted(self.work.clone(), parts);
2796
+ let policy = crate::durability::Policy {
2797
+ chain,
2798
+ operation: transaction,
2799
+ plan: &self.plan,
2800
+ interval: crate::durability::INTERVAL_BYTES,
2801
+ };
2802
+ let extended = crate::durability::extend(
2803
+ store,
2804
+ &policy,
2805
+ &self.tip,
2806
+ &crate::canon::write(&delta.to_value()),
2807
+ objects,
2808
+ None,
2809
+ )?;
2810
+ let Some(mut root) = root else {
2811
+ return Ok(None);
2812
+ };
2813
+ root.checkpoint = extended.head.head.clone();
2814
+ roots::write(store, &root)?;
2815
+ self.tip
2816
+ .held
2817
+ .extend(extended.named.into_iter().map(|object| object.sha256));
2818
+ self.tip.head = extended.head;
2819
+ self.parts += count;
2820
+ Ok(Some((self.plan.clone(), self.tip.head.clone())))
2821
+ }
2822
+ }
2823
+
2824
+ /// Checkpoint through the writer's cursor, which each full checkpoint re-seeds.
2825
+ #[allow(clippy::too_many_arguments)]
2826
+ pub fn checkpoint_indexed(
2827
+ store: &Store,
2828
+ meta: &Meta,
2829
+ transaction: &str,
2830
+ writer_session: u64,
2831
+ operation: &str,
2832
+ slot: &str,
2833
+ previous: Option<&ObjectRef>,
2834
+ cursor: &mut Option<CheckpointCursor>,
2835
+ ) -> Result<(String, crate::durability::Head)> {
2836
+ let chain = format!("{operation}/{slot}");
2837
+ if let Some(current) = cursor.as_mut() {
2838
+ if let Some(advanced) =
2839
+ current.advance(store, meta, transaction, writer_session, &chain, previous)?
2840
+ {
2841
+ return Ok(advanced);
2842
+ }
2843
+ }
2844
+ *cursor = None;
2845
+ let (plan, head) = checkpoint_progress(
2846
+ store,
2847
+ meta,
2848
+ transaction,
2849
+ writer_session,
2850
+ operation,
2851
+ slot,
2852
+ previous,
2853
+ )?;
2854
+ *cursor = CheckpointCursor::at(
2855
+ store,
2856
+ meta,
2857
+ transaction,
2858
+ writer_session,
2859
+ chain,
2860
+ &plan,
2861
+ &head,
2862
+ )?;
2863
+ Ok((plan, head))
2864
+ }
2865
+
2714
2866
  pub fn checkpoint_progress(
2715
2867
  store: &Store,
2716
2868
  meta: &Meta,
@@ -3115,6 +3267,7 @@ pub fn add_part<R: Read>(
3115
3267
  transaction,
3116
3268
  writer_session,
3117
3269
  &additions,
3270
+ None,
3118
3271
  value,
3119
3272
  reader,
3120
3273
  )
@@ -3122,12 +3275,14 @@ pub fn add_part<R: Read>(
3122
3275
 
3123
3276
  /// Accept one part. The cost is this part's bytes plus one indexed SQL row: the
3124
3277
  /// declaration is not re-read and earlier accepted parts are neither read nor rewritten.
3278
+ #[allow(clippy::too_many_arguments)]
3125
3279
  pub fn add_indexed_part<R: Read>(
3126
3280
  store: &Store,
3127
3281
  meta: &Meta,
3128
3282
  transaction: &str,
3129
3283
  writer_session: u64,
3130
3284
  additions: &Additions,
3285
+ cursor: Option<&mut CheckpointCursor>,
3131
3286
  value: (&str, &str, &str),
3132
3287
  reader: &mut R,
3133
3288
  ) -> Result<crate::checkpoint::Written> {
@@ -3168,16 +3323,17 @@ pub fn add_indexed_part<R: Read>(
3168
3323
  // The private root names the objects before SQL accepts the part, so GC never
3169
3324
  // observes an accepted part whose bytes it may reclaim.
3170
3325
  roots::append_parts(store, &transaction, written.part.segments())?;
3171
- meta.accept_part(
3172
- &transaction,
3173
- writer_session,
3174
- &AddedPart {
3175
- component: component.to_string(),
3176
- key: tensor_key.to_string(),
3177
- role: role.to_string(),
3178
- part: written.part.clone(),
3179
- },
3180
- )?;
3326
+ let added = AddedPart {
3327
+ component: component.to_string(),
3328
+ key: tensor_key.to_string(),
3329
+ role: role.to_string(),
3330
+ part: written.part.clone(),
3331
+ };
3332
+ if meta.accept_part(&transaction, writer_session, &added)? {
3333
+ if let Some(cursor) = cursor {
3334
+ cursor.pending.push(added);
3335
+ }
3336
+ }
3181
3337
  Ok(written)
3182
3338
  }
3183
3339
 
@@ -429,7 +429,7 @@ pub(crate) fn checkpoint_bytes(
429
429
  policy: &Policy<'_>,
430
430
  previous: Option<&ObjectRef>,
431
431
  bytes: &[u8],
432
- mut objects: Vec<ObjectRef>,
432
+ objects: Vec<ObjectRef>,
433
433
  hold: Option<&str>,
434
434
  ) -> Result<CheckpointExport> {
435
435
  let prior = previous
@@ -444,18 +444,68 @@ pub(crate) fn checkpoint_bytes(
444
444
  "checkpoint belongs to another operation slot",
445
445
  );
446
446
  }
447
- if bytes.len() > limits::DOC_MAX_BYTES {
448
- return refuse(
449
- Code::COUNT_CAP,
450
- "checkpoint progress exceeds the document byte cap",
451
- );
452
- }
453
447
  if bytes.is_empty() {
454
448
  return Ok(CheckpointExport {
455
449
  head: Head::default(),
456
450
  roots: Vec::new(),
457
451
  });
458
452
  }
453
+ let tip = Tip {
454
+ head: Head {
455
+ head: previous.cloned(),
456
+ links: prior.as_ref().map_or(0, |set| set.links),
457
+ bytes: prior.as_ref().map_or(0, |set| set.bytes),
458
+ },
459
+ held: prior
460
+ .iter()
461
+ .flat_map(|set| set.blobs.iter().map(|object| object.sha256.clone()))
462
+ .collect(),
463
+ };
464
+ let mut roots = previous
465
+ .map(|head| local_documents(store, head))
466
+ .transpose()?
467
+ .unwrap_or_default();
468
+ let extended = extend(store, policy, &tip, bytes, objects, hold)?;
469
+ roots.push(extended.progress);
470
+ roots.extend(extended.links);
471
+ Ok(CheckpointExport {
472
+ head: extended.head,
473
+ roots,
474
+ })
475
+ }
476
+
477
+ /// A chain's tip and every blob digest the chain already names: all a writer needs to
478
+ /// extend it without walking it.
479
+ #[derive(Debug, Clone, Default)]
480
+ pub(crate) struct Tip {
481
+ pub head: Head,
482
+ pub held: std::collections::HashSet<String>,
483
+ }
484
+
485
+ pub(crate) struct Extended {
486
+ pub head: Head,
487
+ pub progress: ObjectRef,
488
+ /// The progress document and every object the new links name, now held by the chain.
489
+ pub named: Vec<ObjectRef>,
490
+ pub links: Vec<ObjectRef>,
491
+ }
492
+
493
+ /// Write one progress document and the links naming only the objects `tip` does not yet
494
+ /// hold. Only the supplied objects are verified, so the cost is this checkpoint's own.
495
+ pub(crate) fn extend(
496
+ store: &Store,
497
+ policy: &Policy<'_>,
498
+ tip: &Tip,
499
+ bytes: &[u8],
500
+ mut objects: Vec<ObjectRef>,
501
+ hold: Option<&str>,
502
+ ) -> Result<Extended> {
503
+ if bytes.len() > limits::DOC_MAX_BYTES {
504
+ return refuse(
505
+ Code::COUNT_CAP,
506
+ "checkpoint progress exceeds the document byte cap",
507
+ );
508
+ }
459
509
  let progress = store
460
510
  .put_stream_held(
461
511
  &mut &*bytes,
@@ -486,24 +536,9 @@ pub(crate) fn checkpoint_bytes(
486
536
  }
487
537
  objects.push(progress.clone());
488
538
  objects = canonical_refs(objects);
489
- let held: std::collections::BTreeSet<&str> = prior
490
- .iter()
491
- .flat_map(|set| set.blobs.iter().map(|object| object.sha256.as_str()))
492
- .collect();
493
- objects.retain(|object| !held.contains(object.sha256.as_str()));
494
- let mut head = Head {
495
- head: previous.cloned(),
496
- links: prior.as_ref().map_or(0, |set| set.links),
497
- bytes: prior.as_ref().map_or(0, |set| set.bytes),
498
- };
499
- let mut roots = previous
500
- .map(|head| local_documents(store, head))
501
- .transpose()?
502
- .unwrap_or_default();
503
- roots.push(progress.clone());
504
- if objects.is_empty() {
505
- return Ok(CheckpointExport { head, roots });
506
- }
539
+ objects.retain(|object| !tip.held.contains(&object.sha256));
540
+ let mut head = tip.head.clone();
541
+ let mut links = Vec::new();
507
542
  let pages = objects.chunks(CHECKPOINT_REFS);
508
543
  let page_count = pages.len();
509
544
  for (index, page) in pages.enumerate() {
@@ -540,11 +575,16 @@ pub(crate) fn checkpoint_bytes(
540
575
  hold,
541
576
  )?
542
577
  .obj;
543
- roots.push(object.clone());
578
+ links.push(object.clone());
544
579
  head.head = Some(object);
545
580
  head.links += 1;
546
581
  }
547
- Ok(CheckpointExport { head, roots })
582
+ Ok(Extended {
583
+ head,
584
+ progress,
585
+ named: objects,
586
+ links,
587
+ })
548
588
  }
549
589
 
550
590
  /// Pin restored chain documents before their journal is installed. Tensor segments are
@@ -131,6 +131,17 @@ impl Meta {
131
131
  crate::derived::open_row(&mut rows, id, writer_session).map(|_| ())
132
132
  }
133
133
 
134
+ pub(crate) fn part_count(&self, id: &str) -> Result<usize> {
135
+ self.connection()?
136
+ .query_row(
137
+ "SELECT count(*) FROM tensorfs_derived_parts WHERE id=?1",
138
+ [id],
139
+ |row| row.get::<_, i64>(0),
140
+ )
141
+ .map(|count| count as usize)
142
+ .map_err(|error| sql("count derived parts", error))
143
+ }
144
+
134
145
  /// Record one accepted part for the current writer: one indexed read of the writer's
135
146
  /// session and of that part. `Ok(false)` is the same part accepted before.
136
147
  pub(crate) fn accept_part(
@@ -2221,6 +2221,7 @@ impl PyStore {
2221
2221
  writer_session_id,
2222
2222
  source_views: begin.source_views,
2223
2223
  additions: begin.additions,
2224
+ cursor: Mutex::new(None),
2224
2225
  guards: Mutex::new(Some(DerivedGuards {
2225
2226
  writer_hold: begin.writer_hold,
2226
2227
  source_leases: begin.source_leases,
@@ -2510,6 +2511,7 @@ pub struct PyDerivedWriter {
2510
2511
  writer_session_id: u64,
2511
2512
  source_views: Vec<derived::SourceView>,
2512
2513
  additions: derived::Additions,
2514
+ cursor: Mutex<Option<derived::CheckpointCursor>>,
2513
2515
  guards: Mutex<Option<DerivedGuards>>,
2514
2516
  }
2515
2517
 
@@ -2613,7 +2615,7 @@ impl PyDerivedWriter {
2613
2615
  .transpose()?;
2614
2616
  let (plan, head) = py
2615
2617
  .detach(|| {
2616
- derived::checkpoint_progress(
2618
+ derived::checkpoint_indexed(
2617
2619
  &self.store,
2618
2620
  &self.meta,
2619
2621
  &self.transaction_id,
@@ -2621,6 +2623,7 @@ impl PyDerivedWriter {
2621
2623
  operation_id,
2622
2624
  slot,
2623
2625
  previous.as_ref(),
2626
+ &mut self.cursor.lock().unwrap(),
2624
2627
  )
2625
2628
  })
2626
2629
  .or_refuse(py)?;
@@ -2745,6 +2748,7 @@ impl PyDerivedWriter {
2745
2748
  &self.transaction_id,
2746
2749
  self.writer_session_id,
2747
2750
  &self.additions,
2751
+ self.cursor.lock().unwrap().as_mut(),
2748
2752
  (component, key, role),
2749
2753
  &mut source,
2750
2754
  )
@@ -2779,6 +2783,8 @@ impl PyDerivedWriter {
2779
2783
  err: None,
2780
2784
  };
2781
2785
  let result = py.detach(|| {
2786
+ // A config is not a part: the next checkpoint takes the full path.
2787
+ *self.cursor.lock().unwrap() = None;
2782
2788
  derived::add_config(
2783
2789
  &self.meta,
2784
2790
  &self.transaction_id,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.64"
7
+ version = "0.3.65"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [