tensorfs 0.3.60__tar.gz → 0.3.61__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 (153) hide show
  1. {tensorfs-0.3.60 → tensorfs-0.3.61}/Cargo.lock +2 -2
  2. {tensorfs-0.3.60 → tensorfs-0.3.61}/Cargo.toml +1 -1
  3. {tensorfs-0.3.60 → tensorfs-0.3.61}/PKG-INFO +1 -1
  4. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/fetch.rs +32 -1
  5. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/main.rs +3 -1
  6. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/catalog.rs +183 -4
  7. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/fingerprint.rs +24 -1
  8. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/source.rs +96 -4
  9. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/stats.rs +21 -0
  10. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/store.rs +72 -41
  11. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/mod.rs +4 -1
  12. tensorfs-0.3.61/crates/tensorfs-core/src/transport/pace.rs +236 -0
  13. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/pull.rs +177 -100
  14. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/speculate.rs +11 -3
  15. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/tests.rs +254 -1
  16. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-py/src/lib.rs +3 -0
  17. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-py/src/store.rs +19 -12
  18. {tensorfs-0.3.60 → tensorfs-0.3.61}/pyproject.toml +1 -1
  19. {tensorfs-0.3.60 → tensorfs-0.3.61}/python/tensorfs/_ext.pyi +5 -0
  20. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/Cargo.toml +0 -0
  21. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/build.rs +0 -0
  22. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  23. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  24. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  25. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  26. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/asset_update.rs +0 -0
  27. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/b64.rs +0 -0
  28. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  29. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  30. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  31. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  32. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  33. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  34. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  35. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  36. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/canon.rs +0 -0
  37. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/capability.rs +0 -0
  38. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/cbor.rs +0 -0
  39. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  40. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  41. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/corpus.rs +0 -0
  42. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  43. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  44. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  45. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  46. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  47. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  48. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  49. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  50. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived.rs +0 -0
  51. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/dtype.rs +0 -0
  52. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/durability.rs +0 -0
  53. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/err.rs +0 -0
  54. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/fetch.rs +0 -0
  55. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/fill.rs +0 -0
  56. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/fit.rs +0 -0
  57. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/gc.rs +0 -0
  58. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/header.rs +0 -0
  59. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/home.rs +0 -0
  60. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ids.rs +0 -0
  61. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  62. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  63. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  64. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  65. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  66. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  67. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  68. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  69. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  70. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  71. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/jcs.rs +0 -0
  72. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/lib.rs +0 -0
  73. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/limits.rs +0 -0
  74. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/machine.rs +0 -0
  75. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/manifest.rs +0 -0
  76. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/meta.rs +0 -0
  77. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/project.rs +0 -0
  78. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/providers.rs +0 -0
  79. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/read.rs +0 -0
  80. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/receipt.rs +0 -0
  81. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/reclaim.rs +0 -0
  82. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/registry.rs +0 -0
  83. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/relation.rs +0 -0
  84. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  85. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  86. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/repository.rs +0 -0
  87. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/sha256.rs +0 -0
  88. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  89. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/spec.rs +0 -0
  90. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/staging.rs +0 -0
  91. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/storage.rs +0 -0
  92. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/api.rs +0 -0
  93. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/http.rs +0 -0
  94. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  95. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  96. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  97. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  98. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  99. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  100. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/push.rs +0 -0
  101. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  102. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/src/vectors.rs +0 -0
  103. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  104. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  105. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  106. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  107. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fit.rs +0 -0
  108. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  109. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  110. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  111. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  112. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  113. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  114. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  115. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  116. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  117. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  118. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/home.rs +0 -0
  119. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  120. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  121. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/liveness.rs +0 -0
  122. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  123. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  124. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  125. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  126. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  127. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  128. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  129. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  130. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  131. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  132. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_download.rs +0 -0
  133. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  134. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  135. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  136. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/staging.rs +0 -0
  137. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  138. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  139. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  140. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  141. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/version.rs +0 -0
  142. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-core/tests/write_through.rs +0 -0
  143. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-py/Cargo.toml +0 -0
  144. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-py/src/docs.rs +0 -0
  145. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-py/src/errors.rs +0 -0
  146. {tensorfs-0.3.60 → tensorfs-0.3.61}/crates/tensorfs-py/src/ingest.rs +0 -0
  147. {tensorfs-0.3.60 → tensorfs-0.3.61}/python/tensorfs/__init__.py +0 -0
  148. {tensorfs-0.3.60 → tensorfs-0.3.61}/python/tensorfs/_derived_channel.py +0 -0
  149. {tensorfs-0.3.60 → tensorfs-0.3.61}/python/tensorfs/_source_channel.py +0 -0
  150. {tensorfs-0.3.60 → tensorfs-0.3.61}/python/tensorfs/_tensors.py +0 -0
  151. {tensorfs-0.3.60 → tensorfs-0.3.61}/python/tensorfs/derived.py +0 -0
  152. {tensorfs-0.3.60 → tensorfs-0.3.61}/python/tensorfs/errors.py +0 -0
  153. {tensorfs-0.3.60 → tensorfs-0.3.61}/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.60"
442
+ version = "0.3.61"
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.60"
457
+ version = "0.3.61"
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.60"
6
+ version = "0.3.61"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.60
3
+ Version: 0.3.61
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -273,7 +273,8 @@ fn pull_with_budget(
273
273
  let mut request = PullRequest::new(&store, base, refspec, &credential, &policy);
274
274
  request.lane = flag(flags, "lane").unwrap_or("");
275
275
  request.session = flag(flags, "session").unwrap_or("");
276
- request.streams = crate::flag_num(flags, "streams", transport::STREAMS);
276
+ request.streams = crate::flag_num(flags, "streams", transport::PULL_STREAMS);
277
+ request.streams_start = crate::flag_num(flags, "streams-start", transport::STREAMS);
277
278
  request.deadline = deadline_of(flags);
278
279
  request.download_budget = budget;
279
280
  // THE WALK REPORTS WHILE IT WALKS.
@@ -300,7 +301,11 @@ fn pull_with_budget(
300
301
  let observe = |_object: &ObjectRef, moved: u64, door: ObjectSource| progress.saw(moved, door);
301
302
  request.on_plan = Some(&planned);
302
303
  request.on_object = Some(&observe);
304
+ let before = tensorfs_core::stats::snapshot();
305
+ let started = std::time::Instant::now();
303
306
  let report = ok!(transport::pull(&request));
307
+ let seconds = started.elapsed().as_secs_f64();
308
+ let after = tensorfs_core::stats::snapshot();
304
309
  progress.settle();
305
310
  if !tensorfs_core::repo_cache::finish_optional_backfills() {
306
311
  eprintln!("repo cache replication incomplete; local fetch remains complete");
@@ -313,6 +318,32 @@ fn pull_with_budget(
313
318
  quoted(&report.model), quoted(&report.manifest), quoted(&report.scope),
314
319
  report.cache_writes.bytes(), report.bytes_cached, report.bytes_total,
315
320
  );
321
+ // What each stream level bought, so a slow pull says whether more streams helped
322
+ // (one GET was the limit) or not (the link, disk or origin was).
323
+ let levels: Vec<String> = report
324
+ .levels
325
+ .iter()
326
+ .map(|level| {
327
+ format!(
328
+ "[{},{}]",
329
+ level.streams,
330
+ level.bytes_per_second.round() as u64
331
+ )
332
+ })
333
+ .collect();
334
+ // And what admitting them cost: a pull whose streams stop growing while its batches
335
+ // take most of the wall time is held by the disk, not the link.
336
+ eprintln!(
337
+ "{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}}}}",
338
+ quoted(&report.model),
339
+ seconds,
340
+ report.bytes_moved,
341
+ report.streams,
342
+ levels.join(","),
343
+ after.admitted_objects - before.admitted_objects,
344
+ after.admission_batches - before.admission_batches,
345
+ (after.admission_nanos - before.admission_nanos) as f64 / 1e9,
346
+ );
316
347
  println!(
317
348
  "model {}@{} lane {}",
318
349
  report.model, report.release, report.lane
@@ -42,7 +42,9 @@
42
42
  //! The ONE transport (tfs-048/tfs-049/tfs-050). The credential is `--credential-file` or
43
43
  //! TFS_CREDENTIAL (`bearer <token>`, or empty for anonymous), never bare argv; a
44
44
  //! URL of `-` arrives on stdin. Flags: --hub <base> --lane --session --streams <n>
45
- //! --timeout <s> --allow-hosts a,b,.c --allow-local --max-redirects <n>.
45
+ //! --streams-start <n> --timeout <s> --allow-hosts a,b,.c --allow-local --max-redirects <n>.
46
+ //! `--streams` is the most objects in flight (default 128, memory-bounded); the walk starts
47
+ //! at `--streams-start` (default 16) and doubles while measured goodput follows.
46
48
  //! `tfs fetch` reads and backfills the Store's bound repo cache between the local Store
47
49
  //! and the origin. It is bound by `tfs store ensure --repo-cache` and never by a flag here.
48
50
  //!
@@ -903,8 +903,9 @@ impl Catalog {
903
903
  hold_on(&self.connection()?, operation, key, length)
904
904
  }
905
905
 
906
- /// The object's existing hold and verification become durable together. Filesystem
907
- /// publication still precedes its verification record, with FULL synchronization.
906
+ /// A HELD admission: the hold is written before the object is published, so an
907
+ /// operation that cannot hold it publishes nothing. Unheld admissions, which is every
908
+ /// pull, take `record_grouped` instead.
908
909
  pub(crate) fn admit_blob(
909
910
  &self,
910
911
  operation: Option<&str>,
@@ -914,7 +915,7 @@ impl Catalog {
914
915
  // One admission at a time per catalog in this process, holding the write lock from
915
916
  // before publication: streams landing objects together queue here instead of in
916
917
  // SQLite's busy timeout, where a lost race refused bytes already verified.
917
- let turn = admissions(&self.path);
918
+ let turn = held_admissions(&self.path);
918
919
  let _turn = turn.lock().unwrap_or_else(|error| error.into_inner());
919
920
  let mut connection = self.connection()?;
920
921
  let transaction = connection
@@ -941,6 +942,117 @@ impl Catalog {
941
942
  Ok(verified.is_some())
942
943
  }
943
944
 
945
+ /// The object's existing hold and verification become durable together. Filesystem
946
+ /// publication has already happened, on the caller's thread, and precedes this record,
947
+ /// which is written with FULL synchronization.
948
+ ///
949
+ /// Records queue per catalog in this process, and whichever arrives while no batch is
950
+ /// committing leads the next one: one immediate transaction and one commit make every
951
+ /// waiting record durable. One object alone costs what it always did. Before this,
952
+ /// directory creation, link, directory fsync and commit ran for one object at a time
953
+ /// under one lock, several fsyncs each, and on a disk busy flushing those same objects'
954
+ /// bodies that queue, not the network, set a pull's rate.
955
+ pub(crate) fn record_grouped(&self, recording: Recording) -> Result<()> {
956
+ let group = admission_group(&self.path);
957
+ let mut state = group
958
+ .state
959
+ .lock()
960
+ .unwrap_or_else(|error| error.into_inner());
961
+ let ticket = state.next;
962
+ state.next += 1;
963
+ state.queue.push((ticket, recording));
964
+ loop {
965
+ if let Some(result) = state.done.remove(&ticket) {
966
+ return result;
967
+ }
968
+ if state.leading {
969
+ state = group
970
+ .turned
971
+ .wait(state)
972
+ .unwrap_or_else(|error| error.into_inner());
973
+ continue;
974
+ }
975
+ state.leading = true;
976
+ let batch = std::mem::take(&mut state.queue);
977
+ let mut lead = Lead {
978
+ group: &group,
979
+ tickets: batch.iter().map(|(ticket, _)| *ticket).collect(),
980
+ results: Vec::new(),
981
+ };
982
+ drop(state);
983
+ let started = std::time::Instant::now();
984
+ let objects = batch.len();
985
+ lead.results = self.record_batch(batch);
986
+ crate::stats::admission_batch(objects, started.elapsed());
987
+ drop(lead);
988
+ state = group
989
+ .state
990
+ .lock()
991
+ .unwrap_or_else(|error| error.into_inner());
992
+ }
993
+ }
994
+
995
+ fn record_batch(&self, batch: Vec<(u64, Recording)>) -> Vec<(u64, Result<()>)> {
996
+ let fail_all = |batch: Vec<(u64, Recording)>, error: Refusal| {
997
+ batch
998
+ .into_iter()
999
+ .map(|(ticket, _)| (ticket, Err(error.clone())))
1000
+ .collect::<Vec<_>>()
1001
+ };
1002
+ let mut connection = match self.connection() {
1003
+ Ok(connection) => connection,
1004
+ Err(error) => return fail_all(batch, error),
1005
+ };
1006
+ let mut transaction =
1007
+ match connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) {
1008
+ Ok(transaction) => transaction,
1009
+ Err(error) => return fail_all(batch, sql("begin blob admission", error)),
1010
+ };
1011
+ let mut results = Vec::with_capacity(batch.len());
1012
+ let mut written = Vec::with_capacity(batch.len());
1013
+ // Each object's row sits behind its own savepoint: one refused row refuses that
1014
+ // object, as it did when every object had a transaction of its own.
1015
+ for (ticket, recording) in batch {
1016
+ let outcome = transaction
1017
+ .savepoint()
1018
+ .map_err(|error| sql("begin object admission", error))
1019
+ .and_then(|savepoint| {
1020
+ if let Some(row) = &recording.record {
1021
+ write_verification_on(&savepoint, row)?;
1022
+ }
1023
+ savepoint
1024
+ .commit()
1025
+ .map_err(|error| sql("record object admission", error))
1026
+ });
1027
+ match outcome {
1028
+ Ok(()) => written.push((ticket, recording.record)),
1029
+ Err(error) => results.push((ticket, Err(error))),
1030
+ }
1031
+ }
1032
+ match transaction
1033
+ .commit()
1034
+ .map_err(|error| sql("commit blob admission", error))
1035
+ {
1036
+ Ok(()) => {
1037
+ let trust = Trust::live(&self.path);
1038
+ for (ticket, record) in written {
1039
+ if let (Some(row), Some(trust)) = (&record, &trust) {
1040
+ trust.remember(row);
1041
+ }
1042
+ results.push((ticket, Ok(())));
1043
+ }
1044
+ }
1045
+ Err(error) => {
1046
+ results.extend(
1047
+ written
1048
+ .into_iter()
1049
+ .map(|(ticket, _)| (ticket, Err(error.clone()))),
1050
+ );
1051
+ }
1052
+ }
1053
+ results
1054
+ }
1055
+
944
1056
  pub fn held_keys(&self) -> Result<Vec<crate::storage::HeldKey>> {
945
1057
  let connection = self.connection()?;
946
1058
  let mut statement = connection
@@ -1084,7 +1196,7 @@ impl Catalog {
1084
1196
  }
1085
1197
  }
1086
1198
 
1087
- fn admissions(path: &Path) -> Arc<Mutex<()>> {
1199
+ fn held_admissions(path: &Path) -> Arc<Mutex<()>> {
1088
1200
  static TURNS: OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>> = OnceLock::new();
1089
1201
  TURNS
1090
1202
  .get_or_init(Default::default)
@@ -1095,6 +1207,73 @@ fn admissions(path: &Path) -> Arc<Mutex<()>> {
1095
1207
  .clone()
1096
1208
  }
1097
1209
 
1210
+ /// One published object's catalog rows, handed to the admission group.
1211
+ pub(crate) struct Recording {
1212
+ /// The verification row, when this call installed the object.
1213
+ pub record: Option<crate::store::VRecord>,
1214
+ }
1215
+
1216
+ #[derive(Default)]
1217
+ struct AdmissionGroup {
1218
+ state: Mutex<GroupState>,
1219
+ turned: std::sync::Condvar,
1220
+ }
1221
+
1222
+ #[derive(Default)]
1223
+ struct GroupState {
1224
+ next: u64,
1225
+ leading: bool,
1226
+ queue: Vec<(u64, Recording)>,
1227
+ done: HashMap<u64, Result<()>>,
1228
+ }
1229
+
1230
+ fn admission_group(path: &Path) -> Arc<AdmissionGroup> {
1231
+ static GROUPS: OnceLock<Mutex<HashMap<PathBuf, Arc<AdmissionGroup>>>> = OnceLock::new();
1232
+ GROUPS
1233
+ .get_or_init(Default::default)
1234
+ .lock()
1235
+ .unwrap_or_else(|error| error.into_inner())
1236
+ .entry(path.to_path_buf())
1237
+ .or_default()
1238
+ .clone()
1239
+ }
1240
+
1241
+ /// A leader hands its batch's results back however it leaves, so a panic inside one batch
1242
+ /// cannot strand the admissions queued behind it.
1243
+ struct Lead<'a> {
1244
+ group: &'a AdmissionGroup,
1245
+ tickets: Vec<u64>,
1246
+ results: Vec<(u64, Result<()>)>,
1247
+ }
1248
+
1249
+ impl Drop for Lead<'_> {
1250
+ fn drop(&mut self) {
1251
+ let mut state = self
1252
+ .group
1253
+ .state
1254
+ .lock()
1255
+ .unwrap_or_else(|error| error.into_inner());
1256
+ state.leading = false;
1257
+ let mut answered = std::collections::HashSet::new();
1258
+ for (ticket, result) in self.results.drain(..) {
1259
+ answered.insert(ticket);
1260
+ state.done.insert(ticket, result);
1261
+ }
1262
+ for ticket in &self.tickets {
1263
+ if !answered.contains(ticket) {
1264
+ state.done.insert(
1265
+ *ticket,
1266
+ Err(Refusal {
1267
+ code: Code::IO_FAILED,
1268
+ detail: "the admission batch holding this object did not finish".into(),
1269
+ }),
1270
+ );
1271
+ }
1272
+ }
1273
+ self.group.turned.notify_all();
1274
+ }
1275
+ }
1276
+
1098
1277
  fn hold_on(connection: &Connection, operation: &str, key: &str, length: u64) -> Result<()> {
1099
1278
  let kind = if key.starts_with("blobs/") {
1100
1279
  "blob"
@@ -262,6 +262,21 @@ impl FingerprintRegistry {
262
262
  crate::canon::write(&self.to_value())
263
263
  }
264
264
 
265
+ /// Extend this registry with a newer reviewed one: its entries and profiles replace
266
+ /// these under the same key and add the rest. Every entry keeps its own provenance.
267
+ pub fn extend(&mut self, newer: FingerprintRegistry) {
268
+ for entry in newer.entries {
269
+ self.merge(entry);
270
+ }
271
+ for profile in newer.source_profiles {
272
+ self.merge_source_profile(profile);
273
+ }
274
+ }
275
+
276
+ /// Key set and component classify. An entry banked from the exact same tensor schema is
277
+ /// preferred; otherwise any entry for this key set and component serves when they agree
278
+ /// on dialect and converter, and conversion validates the actual shapes and dtypes (an
279
+ /// fp16 upload of a model banked in bf16 classifies).
265
280
  pub fn classify(&self, fp: &Fingerprint) -> Verdict {
266
281
  let keyset: Vec<&Banked> = self
267
282
  .entries
@@ -281,7 +296,7 @@ impl FingerprintRegistry {
281
296
  banked_as: keyset.iter().map(|e| name_of(e)).collect(),
282
297
  };
283
298
  }
284
- let hits: Vec<&Banked> = component
299
+ let exact: Vec<&Banked> = component
285
300
  .iter()
286
301
  .copied()
287
302
  .filter(|e| {
@@ -289,6 +304,14 @@ impl FingerprintRegistry {
289
304
  && e.logical_keys as usize == fp.logical_keys
290
305
  })
291
306
  .collect();
307
+ let hits = if exact.is_empty() {
308
+ let mut verdicts = component.clone();
309
+ verdicts.sort_by(|a, b| (&a.dialect, &a.converter).cmp(&(&b.dialect, &b.converter)));
310
+ verdicts.dedup_by(|a, b| (&a.dialect, &a.converter) == (&b.dialect, &b.converter));
311
+ verdicts
312
+ } else {
313
+ exact
314
+ };
292
315
  match hits.len() {
293
316
  0 => Verdict::Unregistered,
294
317
  1 => Verdict::Authorized(Box::new(hits[0].clone())),
@@ -242,6 +242,9 @@ fn profile_assignments(
242
242
  ) -> Vec<Vec<SourceAssignment>> {
243
243
  let mut candidates = Vec::new();
244
244
  for component in &profile.components {
245
+ // Carriers banked from this exact tensor schema first; otherwise any carrier with
246
+ // the reviewed key set, whose shapes and dtypes the conversion itself validates.
247
+ let mut exact = Vec::new();
245
248
  let mut hits = Vec::new();
246
249
  for (variant_index, variant) in component.variants.iter().enumerate() {
247
250
  for entry in registry.entries.iter().filter(|entry| {
@@ -277,15 +280,21 @@ fn profile_assignments(
277
280
  Ok(observed) => observed,
278
281
  Err(_) => continue,
279
282
  };
280
- if observed.keyset_digest == variant.keyset_digest
281
- && observed.tensor_schema_digest == entry.tensor_schema_digest
283
+ if observed.keyset_digest != variant.keyset_digest {
284
+ continue;
285
+ }
286
+ let hit = (index, projected, variant_index);
287
+ if observed.tensor_schema_digest == entry.tensor_schema_digest
282
288
  && observed.logical_keys as u64 == entry.logical_keys
283
289
  {
284
- hits.push((index, projected, variant_index));
290
+ exact.push(hit);
291
+ } else if !hits.contains(&hit) {
292
+ hits.push(hit);
285
293
  }
286
294
  }
287
295
  }
288
296
  }
297
+ let hits = if exact.is_empty() { hits } else { exact };
289
298
  if hits.is_empty() {
290
299
  return Vec::new();
291
300
  }
@@ -303,6 +312,58 @@ fn profile_assignments(
303
312
  solutions
304
313
  }
305
314
 
315
+ /// Where a Store keeps a reviewed registry newer than the built-in one: shipped as a pinned
316
+ /// file or served by the Hub, and bound with [`bind_registry`]. It only extends the built-in
317
+ /// registry, so a TensorFS older than a model can still ingest it.
318
+ pub const REGISTRY_BINDING: &str = "fingerprint-registry.json";
319
+
320
+ /// Bind (or with `None`, clear) the Store's reviewed registry. The data is validated here,
321
+ /// provenance included, so a malformed file is a caller's mistake at bind time.
322
+ pub fn bind_registry(store: &Store, bytes: Option<&[u8]>) -> Result<()> {
323
+ let path = store.root().join(REGISTRY_BINDING);
324
+ let Some(bytes) = bytes else {
325
+ return match fs::remove_file(&path) {
326
+ Ok(()) => Ok(()),
327
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
328
+ Err(error) => refuse(Code::IO_FAILED, format!("clear registry binding: {error}")),
329
+ };
330
+ };
331
+ FingerprintRegistry::parse(bytes)?;
332
+ let temporary = store.root().join("tmp").join(format!(
333
+ "{REGISTRY_BINDING}.{}",
334
+ crate::meta::now_nanos_unique()
335
+ ));
336
+ let written = fs::write(&temporary, bytes)
337
+ .and_then(|()| fs::File::open(&temporary)?.sync_all())
338
+ .and_then(|()| fs::rename(&temporary, &path));
339
+ if let Err(error) = written {
340
+ let _ = fs::remove_file(&temporary);
341
+ return refuse(Code::IO_FAILED, format!("bind registry: {error}"));
342
+ }
343
+ Ok(())
344
+ }
345
+
346
+ /// The registry an ingest on this Store classifies with: the built-in one, extended by the
347
+ /// Store's bound registry, then by a caller-`supplied` one. A bound file that no longer
348
+ /// parses is skipped (it can only add); supplied data that does not parse refuses.
349
+ pub fn effective_registry(store: &Store, supplied: Option<&[u8]>) -> Result<(String, Vec<u8>)> {
350
+ let mut registry = FingerprintRegistry::parse(BUILTIN_REGISTRY_BYTES)?;
351
+ let mut locator = BUILTIN_REGISTRY.to_string();
352
+ let bound = fs::read(store.root().join(REGISTRY_BINDING))
353
+ .ok()
354
+ .filter(|bytes| bytes.len() <= crate::limits::DOC_MAX_BYTES)
355
+ .and_then(|bytes| FingerprintRegistry::parse(&bytes).ok());
356
+ if let Some(bound) = bound {
357
+ registry.extend(bound);
358
+ locator.push_str("+store");
359
+ }
360
+ if let Some(bytes) = supplied {
361
+ registry.extend(FingerprintRegistry::parse(bytes)?);
362
+ locator.push_str("+supplied");
363
+ }
364
+ Ok((locator, registry.to_bytes()))
365
+ }
366
+
306
367
  pub fn read_header_only(path: &Path) -> Result<(SourceHeader, Vec<u8>)> {
307
368
  let json = fs::read(path).map_err(|error| Refusal {
308
369
  code: Code::IO_FAILED,
@@ -1658,7 +1719,8 @@ pub fn prepare_model_source(
1658
1719
  store: &Store,
1659
1720
  request: &PrepareModelSource,
1660
1721
  ) -> Result<PreparedModelSources> {
1661
- prepare_model_source_with_registry(store, request, BUILTIN_REGISTRY, BUILTIN_REGISTRY_BYTES)
1722
+ let (locator, bytes) = effective_registry(store, None)?;
1723
+ prepare_model_source_with_registry(store, request, &locator, &bytes)
1662
1724
  }
1663
1725
 
1664
1726
  /// One member of an exact provider selection: its pinned identity and the header a ranged
@@ -2308,6 +2370,36 @@ mod tests {
2308
2370
  let _ = fs::remove_dir_all(root);
2309
2371
  }
2310
2372
 
2373
+ #[test]
2374
+ fn a_bound_registry_ingests_models_the_build_never_banked_at_any_precision() {
2375
+ let root = temporary("bound-registry");
2376
+ let (store, registry, mut request) = fixture(&root, false);
2377
+ // Banked from F32 carriers; the first carrier is now re-uploaded as F16.
2378
+ let first_path = request.roster[0].path.clone();
2379
+ let header = "{\"weight\":{\"data_offsets\":[0,2],\"dtype\":\"F16\",\"shape\":[1]}}";
2380
+ let mut bytes = (header.len() as u64).to_le_bytes().to_vec();
2381
+ bytes.extend_from_slice(header.as_bytes());
2382
+ bytes.extend_from_slice(&[0x00, 0x3c]);
2383
+ fs::write(&first_path, &bytes).unwrap();
2384
+ request.roster[0].object_id = ObjectRef::of(&bytes).id();
2385
+ request.roster[0].length = bytes.len() as u64;
2386
+ request.roster[0].header = header_prefix(&bytes);
2387
+
2388
+ // Unknown to the compiled-in registry until the reviewed data is bound.
2389
+ assert_eq!(
2390
+ prepare_model_source(&store, &request).unwrap_err().code,
2391
+ Code::UNREGISTERED_FINGERPRINT
2392
+ );
2393
+ bind_registry(&store, Some(&registry)).unwrap();
2394
+ let prepared = prepare_model_source(&store, &request).unwrap();
2395
+ assert!(prepared.complete);
2396
+ assert_eq!(prepared.sources.len(), 2);
2397
+ // Registry data without provenance is not reviewed data.
2398
+ assert!(bind_registry(&store, Some(b"{\"entries\":[{\"component\":\"x\"}]}")).is_err());
2399
+ bind_registry(&store, None).unwrap();
2400
+ let _ = fs::remove_dir_all(root);
2401
+ }
2402
+
2311
2403
  #[test]
2312
2404
  #[ignore = "subprocess for source journal SIGKILL proof"]
2313
2405
  fn source_tail_snapshot_worker() {
@@ -6,6 +6,9 @@ use std::sync::atomic::{AtomicU64, Ordering};
6
6
  static CATALOG_OPENS: AtomicU64 = AtomicU64::new(0);
7
7
  static PRESENCE_PASSES: AtomicU64 = AtomicU64::new(0);
8
8
  static REPO_CACHE_VERIFIED_READS: AtomicU64 = AtomicU64::new(0);
9
+ static ADMISSION_BATCHES: AtomicU64 = AtomicU64::new(0);
10
+ static ADMITTED_OBJECTS: AtomicU64 = AtomicU64::new(0);
11
+ static ADMISSION_NANOS: AtomicU64 = AtomicU64::new(0);
9
12
 
10
13
  #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11
14
  pub struct Stats {
@@ -15,6 +18,12 @@ pub struct Stats {
15
18
  pub presence_passes: u64,
16
19
  /// Repository-cache entries read and hashed.
17
20
  pub repo_cache_verified_reads: u64,
21
+ /// Blob admission batches committed (one transaction and commit each).
22
+ pub admission_batches: u64,
23
+ /// Blobs those batches published and recorded.
24
+ pub admitted_objects: u64,
25
+ /// Wall time those batches spent linking, fsyncing and committing.
26
+ pub admission_nanos: u64,
18
27
  }
19
28
 
20
29
  pub fn snapshot() -> Stats {
@@ -22,6 +31,9 @@ pub fn snapshot() -> Stats {
22
31
  catalog_opens: CATALOG_OPENS.load(Ordering::Relaxed),
23
32
  presence_passes: PRESENCE_PASSES.load(Ordering::Relaxed),
24
33
  repo_cache_verified_reads: REPO_CACHE_VERIFIED_READS.load(Ordering::Relaxed),
34
+ admission_batches: ADMISSION_BATCHES.load(Ordering::Relaxed),
35
+ admitted_objects: ADMITTED_OBJECTS.load(Ordering::Relaxed),
36
+ admission_nanos: ADMISSION_NANOS.load(Ordering::Relaxed),
25
37
  }
26
38
  }
27
39
 
@@ -48,3 +60,12 @@ pub(crate) fn presence_pass() {
48
60
  pub(crate) fn repo_cache_verified_read() {
49
61
  REPO_CACHE_VERIFIED_READS.fetch_add(1, Ordering::Relaxed);
50
62
  }
63
+
64
+ pub(crate) fn admission_batch(objects: usize, spent: std::time::Duration) {
65
+ ADMISSION_BATCHES.fetch_add(1, Ordering::Relaxed);
66
+ ADMITTED_OBJECTS.fetch_add(objects as u64, Ordering::Relaxed);
67
+ ADMISSION_NANOS.fetch_add(
68
+ u64::try_from(spent.as_nanos()).unwrap_or(u64::MAX),
69
+ Ordering::Relaxed,
70
+ );
71
+ }