tensorfs 0.3.32__tar.gz → 0.3.34__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 (127) hide show
  1. {tensorfs-0.3.32 → tensorfs-0.3.34}/Cargo.lock +2 -2
  2. {tensorfs-0.3.32 → tensorfs-0.3.34}/Cargo.toml +1 -1
  3. {tensorfs-0.3.32 → tensorfs-0.3.34}/PKG-INFO +1 -1
  4. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/source.rs +79 -1
  5. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/transaction.rs +15 -0
  6. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/repo_cache.rs +54 -10
  7. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/ledger.rs +25 -2
  8. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-py/src/store.rs +70 -6
  9. {tensorfs-0.3.32 → tensorfs-0.3.34}/pyproject.toml +1 -1
  10. {tensorfs-0.3.32 → tensorfs-0.3.34}/python/tensorfs/_ext.pyi +6 -0
  11. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/Cargo.toml +0 -0
  12. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/build.rs +0 -0
  13. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  14. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  15. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  16. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  17. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/b64.rs +0 -0
  18. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  19. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  20. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  21. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  22. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  23. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  24. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  25. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  26. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  27. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  28. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/canon.rs +0 -0
  29. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/capability.rs +0 -0
  30. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/catalog.rs +0 -0
  31. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/cbor.rs +0 -0
  32. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  33. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/corpus.rs +0 -0
  34. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  35. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  36. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  37. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  38. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  39. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  40. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/derived.rs +0 -0
  41. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/dtype.rs +0 -0
  42. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/durability.rs +0 -0
  43. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/err.rs +0 -0
  44. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/fetch.rs +0 -0
  45. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/fill.rs +0 -0
  46. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/fit.rs +0 -0
  47. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/gc.rs +0 -0
  48. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/header.rs +0 -0
  49. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/home.rs +0 -0
  50. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ids.rs +0 -0
  51. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  52. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  53. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  54. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  55. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  56. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  57. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  58. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  59. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/jcs.rs +0 -0
  60. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/lib.rs +0 -0
  61. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/limits.rs +0 -0
  62. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/machine.rs +0 -0
  63. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/manifest.rs +0 -0
  64. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/meta.rs +0 -0
  65. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/project.rs +0 -0
  66. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/providers.rs +0 -0
  67. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/read.rs +0 -0
  68. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/receipt.rs +0 -0
  69. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/reclaim.rs +0 -0
  70. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/registry.rs +0 -0
  71. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/relation.rs +0 -0
  72. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/repository.rs +0 -0
  73. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/sha256.rs +0 -0
  74. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  75. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/spec.rs +0 -0
  76. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/staging.rs +0 -0
  77. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/storage.rs +0 -0
  78. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/store.rs +0 -0
  79. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/api.rs +0 -0
  80. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/http.rs +0 -0
  81. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  82. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  83. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  84. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  85. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  86. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/push.rs +0 -0
  87. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  88. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  89. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/src/vectors.rs +0 -0
  90. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  91. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fit.rs +0 -0
  92. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  93. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  94. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  95. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  96. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  97. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  98. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  99. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  100. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/home.rs +0 -0
  101. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  102. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  103. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/liveness.rs +0 -0
  104. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  105. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  106. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  107. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  108. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  109. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  110. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  111. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  112. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  113. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  114. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  115. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  116. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/staging.rs +0 -0
  117. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  118. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  119. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/version.rs +0 -0
  120. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-core/tests/write_through.rs +0 -0
  121. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-py/Cargo.toml +0 -0
  122. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-py/src/docs.rs +0 -0
  123. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-py/src/errors.rs +0 -0
  124. {tensorfs-0.3.32 → tensorfs-0.3.34}/crates/tensorfs-py/src/lib.rs +0 -0
  125. {tensorfs-0.3.32 → tensorfs-0.3.34}/python/tensorfs/__init__.py +0 -0
  126. {tensorfs-0.3.32 → tensorfs-0.3.34}/python/tensorfs/errors.py +0 -0
  127. {tensorfs-0.3.32 → tensorfs-0.3.34}/python/tensorfs/py.typed +0 -0
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
392
 
393
393
  [[package]]
394
394
  name = "tensorfs-core"
395
- version = "0.3.32"
395
+ version = "0.3.34"
396
396
  dependencies = [
397
397
  "fs2",
398
398
  "minicbor",
@@ -404,7 +404,7 @@ dependencies = [
404
404
 
405
405
  [[package]]
406
406
  name = "tensorfs-py"
407
- version = "0.3.32"
407
+ version = "0.3.34"
408
408
  dependencies = [
409
409
  "pyo3",
410
410
  "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.32"
6
+ version = "0.3.34"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.32
3
+ Version: 0.3.34
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -809,6 +809,8 @@ pub struct PreparedModelSources {
809
809
  pub deferred_ops: usize,
810
810
  pub converted_bytes: u64,
811
811
  pub largest_op_bytes: u64,
812
+ /// Minimum body admission for the next atomic op, without writing that op.
813
+ pub required_write_bytes: u64,
812
814
  }
813
815
 
814
816
  #[derive(Debug, Clone, PartialEq, Eq)]
@@ -1112,6 +1114,24 @@ pub fn prepare_model_source_with_registry(
1112
1114
  request: &PrepareModelSource,
1113
1115
  registry_locator: &str,
1114
1116
  registry_bytes: &[u8],
1117
+ ) -> Result<PreparedModelSources> {
1118
+ prepare_model_source_with_registry_budget(
1119
+ store,
1120
+ request,
1121
+ registry_locator,
1122
+ registry_bytes,
1123
+ None,
1124
+ )
1125
+ }
1126
+
1127
+ /// An explicit body budget never admits an oversized op. Zero advances only metadata
1128
+ /// and replay, reporting the next atomic body's native byte count for host admission.
1129
+ pub fn prepare_model_source_with_registry_budget(
1130
+ store: &Store,
1131
+ request: &PrepareModelSource,
1132
+ registry_locator: &str,
1133
+ registry_bytes: &[u8],
1134
+ write_budget_bytes: Option<u64>,
1115
1135
  ) -> Result<PreparedModelSources> {
1116
1136
  // The guard bridges local snapshot publication and recipient root adoption. A
1117
1137
  // predecessor can be released between them, but GC cannot remove the saved bytes.
@@ -1171,7 +1191,10 @@ pub fn prepare_model_source_with_registry(
1171
1191
  request,
1172
1192
  registry_locator,
1173
1193
  registry_bytes,
1174
- &mut transaction::Budget::new(durability::INTERVAL_BYTES),
1194
+ &mut match write_budget_bytes {
1195
+ Some(limit) => transaction::Budget::bounded(limit),
1196
+ None => transaction::Budget::new(durability::INTERVAL_BYTES),
1197
+ },
1175
1198
  )
1176
1199
  }
1177
1200
 
@@ -1365,6 +1388,7 @@ fn prepare_model_source_with_budget(
1365
1388
  deferred_ops: 0,
1366
1389
  converted_bytes: 0,
1367
1390
  largest_op_bytes: 0,
1391
+ required_write_bytes: 0,
1368
1392
  });
1369
1393
  }
1370
1394
  crate::catalog::SourcePreparation::Open(writer) => writer,
@@ -1381,6 +1405,7 @@ fn prepare_model_source_with_budget(
1381
1405
  deferred_ops: 0,
1382
1406
  converted_bytes: 0,
1383
1407
  largest_op_bytes: 0,
1408
+ required_write_bytes: 0,
1384
1409
  };
1385
1410
  let mut needed = std::collections::BTreeSet::new();
1386
1411
  for (requested, (plan_hex, prepared)) in request.profiles.iter().zip(plans) {
@@ -1470,6 +1495,7 @@ fn prepare_model_source_with_budget(
1470
1495
  result.converted_roles += progress.converted_roles;
1471
1496
  result.converted_bytes += progress.written;
1472
1497
  result.largest_op_bytes = budget.largest_op;
1498
+ result.required_write_bytes = budget.required_write_bytes;
1473
1499
  result.resumed_roles += progress.resumed_roles;
1474
1500
  result.deferred_ops += progress.deferred_ops;
1475
1501
  if let Some(outcome) = outcome {
@@ -2526,6 +2552,58 @@ mod tests {
2526
2552
  fs::remove_dir_all(root).unwrap();
2527
2553
  }
2528
2554
 
2555
+ #[test]
2556
+ fn strict_source_pass_reports_body_admission_then_resumes_without_reconversion() {
2557
+ let root = temporary("strict-source-admission");
2558
+ let (store, registry, request) = fixture_sized(&root, false, Some(1024));
2559
+ let advance = |limit| {
2560
+ prepare_model_source_with_registry_budget(
2561
+ &store,
2562
+ &request,
2563
+ "fixture:registry",
2564
+ &registry,
2565
+ Some(limit),
2566
+ )
2567
+ .unwrap()
2568
+ };
2569
+ for limit in [0, 4095] {
2570
+ let held = advance(limit);
2571
+ assert!(!held.complete);
2572
+ assert_eq!(held.converted_roles, 0);
2573
+ assert_eq!(held.converted_bytes, 0);
2574
+ assert_eq!(held.largest_op_bytes, 0);
2575
+ assert_eq!(held.required_write_bytes, 4096);
2576
+ assert_eq!(held.deferred_ops, 2);
2577
+ }
2578
+ let first = advance(4096);
2579
+ assert!(!first.complete);
2580
+ assert_eq!(first.converted_bytes, 4096);
2581
+ assert_eq!(first.converted_roles, 1);
2582
+ assert_eq!(first.required_write_bytes, 0);
2583
+ let held = advance(0);
2584
+ assert_eq!(held.converted_bytes, 0);
2585
+ assert_eq!(held.resumed_roles, 1);
2586
+ assert_eq!(held.required_write_bytes, 4096);
2587
+ assert_eq!(held.checkpoints, first.checkpoints);
2588
+ let completed = advance(4096);
2589
+ assert!(completed.complete);
2590
+ assert_eq!(completed.converted_bytes, 4096);
2591
+ assert_eq!(completed.resumed_roles, 1);
2592
+ let replay = advance(0);
2593
+ assert!(replay.replayed && replay.complete);
2594
+ assert_eq!(replay.sources, completed.sources);
2595
+ assert_eq!(replay.converted_bytes, 0);
2596
+ assert_eq!(replay.required_write_bytes, 0);
2597
+ let clean = Store::init(&root.join("clean")).unwrap();
2598
+ let expected =
2599
+ prepare_model_source_with_registry(&clean, &request, "fixture:registry", &registry)
2600
+ .unwrap();
2601
+ assert_eq!(completed.sources, expected.sources);
2602
+ release_model_source(&store, &request.operation_id).unwrap();
2603
+ release_model_source(&clean, &request.operation_id).unwrap();
2604
+ fs::remove_dir_all(root).unwrap();
2605
+ }
2606
+
2529
2607
  #[test]
2530
2608
  fn one_pass_budget_is_shared_by_profiles_and_allows_one_oversized_op() {
2531
2609
  for limit in [1024, 4096] {
@@ -444,6 +444,8 @@ pub struct Budget {
444
444
  used: u64,
445
445
  stopped: bool,
446
446
  pub largest_op: u64,
447
+ strict: bool,
448
+ pub required_write_bytes: u64,
447
449
  }
448
450
 
449
451
  impl Budget {
@@ -453,12 +455,25 @@ impl Budget {
453
455
  used: 0,
454
456
  stopped: false,
455
457
  largest_op: 0,
458
+ strict: false,
459
+ required_write_bytes: 0,
460
+ }
461
+ }
462
+ pub fn bounded(limit: u64) -> Self {
463
+ Self {
464
+ strict: true,
465
+ ..Self::new(limit)
456
466
  }
457
467
  }
458
468
  fn take(&mut self, bytes: u64) -> bool {
459
469
  if self.stopped {
460
470
  return false;
461
471
  }
472
+ if self.strict && self.used == 0 && bytes > self.limit {
473
+ self.required_write_bytes = bytes;
474
+ self.stopped = true;
475
+ return false;
476
+ }
462
477
  if self.used > 0 && bytes > self.limit.saturating_sub(self.used) {
463
478
  self.stopped = true;
464
479
  return false;
@@ -1085,6 +1085,8 @@ mod backfill_tests {
1085
1085
  #[cfg(test)]
1086
1086
  mod exit_wait_tests {
1087
1087
  use super::*;
1088
+ use std::sync::atomic::{AtomicU64, Ordering};
1089
+ use std::sync::mpsc;
1088
1090
  use std::sync::Arc;
1089
1091
  use std::time::{Duration, Instant};
1090
1092
 
@@ -1093,11 +1095,24 @@ mod exit_wait_tests {
1093
1095
  struct Paced<'a>(&'a [u8]);
1094
1096
  impl Read for Paced<'_> {
1095
1097
  fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
1096
- std::thread::sleep(Duration::from_millis(5));
1097
1098
  let take = out.len().min(4096);
1098
1099
  self.0.read(&mut out[..take])
1099
1100
  }
1100
1101
  }
1102
+ struct Progress {
1103
+ activity: Arc<BackfillActivity>,
1104
+ elapsed_ms: Arc<AtomicU64>,
1105
+ }
1106
+ impl store::Progress for Progress {
1107
+ fn admitted(&self) {}
1108
+ fn moved(&self, bytes: u64) {
1109
+ // Advance time only after real bytes were copied and hashed. Thread
1110
+ // startup, filesystem scheduling and final fsync are not a stalled
1111
+ // transfer injected by this test.
1112
+ self.elapsed_ms.fetch_add(5, Ordering::SeqCst);
1113
+ self.activity.moved(bytes);
1114
+ }
1115
+ }
1101
1116
  let root = std::env::temp_dir().join(format!(
1102
1117
  "tensorfs-exit-moving-{}-{}",
1103
1118
  std::process::id(),
@@ -1110,22 +1125,51 @@ mod exit_wait_tests {
1110
1125
  Duration::from_millis(10),
1111
1126
  )));
1112
1127
  activity.offered();
1113
- let observed = activity.clone();
1128
+ let elapsed_ms = Arc::new(AtomicU64::new(0));
1129
+ let clock = elapsed_ms.clone();
1130
+ let started = Instant::now();
1131
+ let (sampled, observation) = mpsc::channel();
1132
+ activity.pending.lock().unwrap().1 =
1133
+ Ledger::with_clock(Duration::from_millis(10), move || {
1134
+ let elapsed = clock.load(Ordering::SeqCst);
1135
+ if elapsed >= 320 && std::thread::current().name() == Some("copy-wait") {
1136
+ let _ = sampled.send(());
1137
+ }
1138
+ started + Duration::from_millis(elapsed)
1139
+ });
1140
+ let observed = Progress {
1141
+ activity: activity.clone(),
1142
+ elapsed_ms: elapsed_ms.clone(),
1143
+ };
1114
1144
  let target = destination.clone();
1115
1145
  let wanted = object.clone();
1116
- let started = Instant::now();
1146
+ let (finish, finishing) = mpsc::channel();
1117
1147
  let copy = std::thread::spawn(move || {
1118
- let result =
1119
- publish_stream(&mut Paced(&data), &target, &wanted, Some(observed.as_ref()));
1120
- observed.finished();
1148
+ let result = publish_stream(&mut Paced(&data), &target, &wanted, Some(&observed));
1149
+ finishing.recv().unwrap();
1150
+ observed.activity.finished();
1121
1151
  result
1122
1152
  });
1123
- assert!(activity.wait(), "a moving copy was abandoned");
1124
- assert_eq!(copy.join().unwrap(), Ok(true));
1153
+ let waiting = activity.clone();
1154
+ let waiter = std::thread::Builder::new()
1155
+ .name("copy-wait".into())
1156
+ .spawn(move || waiting.wait())
1157
+ .unwrap();
1158
+ // The wait must actually sample a pending copy after more than the initial
1159
+ // 60 ms floor, before finished() can make it return trivially. This timeout
1160
+ // is only a deadlock guard; it is not the liveness clock being qualified.
1161
+ let sampled_pending = observation.recv_timeout(Duration::from_secs(5));
1162
+ finish.send(()).unwrap();
1125
1163
  assert!(
1126
- started.elapsed() > Duration::from_millis(60),
1127
- "copy never outlived the initial silence floor"
1164
+ sampled_pending.is_ok(),
1165
+ "wait never observed the moving copy"
1128
1166
  );
1167
+ assert!(waiter.join().unwrap(), "a moving copy was abandoned");
1168
+ assert_eq!(copy.join().unwrap(), Ok(true));
1169
+ let state = activity.pending.lock().unwrap();
1170
+ assert!(Duration::from_millis(elapsed_ms.load(Ordering::SeqCst)) > state.1.floor());
1171
+ assert_eq!(state.1.still().moved, object.length);
1172
+ drop(state);
1129
1173
  assert_eq!(
1130
1174
  verify_path(&destination, CacheKind::Blob, &object, None),
1131
1175
  Ok(())
@@ -137,6 +137,8 @@ pub struct Ledger {
137
137
  sample: Duration,
138
138
  worst_gap: Mutex<Duration>,
139
139
  progress: Mutex<Progress>,
140
+ #[cfg(test)]
141
+ clock: Option<Box<dyn Fn() -> Instant + Send + Sync>>,
140
142
  }
141
143
 
142
144
  impl Ledger {
@@ -155,9 +157,30 @@ impl Ledger {
155
157
  advanced: Instant::now(),
156
158
  worst_still: Duration::ZERO,
157
159
  }),
160
+ #[cfg(test)]
161
+ clock: None,
158
162
  }
159
163
  }
160
164
 
165
+ #[cfg(test)]
166
+ pub(crate) fn with_clock(
167
+ sample: Duration,
168
+ clock: impl Fn() -> Instant + Send + Sync + 'static,
169
+ ) -> Self {
170
+ let mut ledger = Self::with_resolution(sample);
171
+ ledger.progress.lock().unwrap().advanced = clock();
172
+ ledger.clock = Some(Box::new(clock));
173
+ ledger
174
+ }
175
+
176
+ fn now(&self) -> Instant {
177
+ #[cfg(test)]
178
+ if let Some(clock) = &self.clock {
179
+ return clock();
180
+ }
181
+ Instant::now()
182
+ }
183
+
161
184
  pub fn sample(&self) -> Duration {
162
185
  self.sample
163
186
  }
@@ -215,7 +238,7 @@ impl Ledger {
215
238
  /// stream cannot raise the bar the next stream is judged by.
216
239
  fn advance(&self, bytes: u64) {
217
240
  let mut progress = self.progress.lock().unwrap();
218
- let now = Instant::now();
241
+ let now = self.now();
219
242
  let gap = now.saturating_duration_since(progress.advanced);
220
243
  if gap > progress.worst_still {
221
244
  progress.worst_still = gap;
@@ -238,7 +261,7 @@ impl Ledger {
238
261
  let progress = self.progress.lock().unwrap();
239
262
  Still {
240
263
  moved: progress.moved,
241
- silent: progress.advanced.elapsed(),
264
+ silent: self.now().saturating_duration_since(progress.advanced),
242
265
  }
243
266
  }
244
267
  }
@@ -1073,7 +1073,7 @@ impl PyStore {
1073
1073
 
1074
1074
  /// Advance the exact source plan over verified physical carriers. Checkpoint heads are
1075
1075
  /// local progress until the RecordOwner acknowledges their complete upload.
1076
- #[pyo3(signature = (operation_id, source_selection_digest, profiles, roster, landed, *, checkpoints=Vec::new(), adopt_from_operation_id=None, registry=None))]
1076
+ #[pyo3(signature = (operation_id, source_selection_digest, profiles, roster, landed, *, checkpoints=Vec::new(), adopt_from_operation_id=None, registry=None, write_budget_bytes=None))]
1077
1077
  #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1078
1078
  fn prepare_model_source<'p>(
1079
1079
  &self,
@@ -1086,6 +1086,7 @@ impl PyStore {
1086
1086
  checkpoints: Vec<(String, String, u64)>,
1087
1087
  adopt_from_operation_id: Option<String>,
1088
1088
  registry: Option<Vec<u8>>,
1089
+ write_budget_bytes: Option<u64>,
1089
1090
  ) -> PyResult<Bound<'p, PyDict>> {
1090
1091
  let checkpoints = checkpoints
1091
1092
  .into_iter()
@@ -1126,14 +1127,21 @@ impl PyStore {
1126
1127
  adopt_from_operation_id,
1127
1128
  };
1128
1129
  let prepared = py
1129
- .detach(|| match &registry {
1130
- Some(bytes) => model_source::prepare_model_source_with_registry(
1130
+ .detach(|| {
1131
+ let (locator, bytes) = match &registry {
1132
+ Some(bytes) => ("supplied:native-registry", bytes.as_slice()),
1133
+ None => (
1134
+ model_source::BUILTIN_REGISTRY,
1135
+ model_source::BUILTIN_REGISTRY_BYTES,
1136
+ ),
1137
+ };
1138
+ model_source::prepare_model_source_with_registry_budget(
1131
1139
  &self.store,
1132
1140
  &request,
1133
- "supplied:native-registry",
1141
+ locator,
1134
1142
  bytes,
1135
- ),
1136
- None => model_source::prepare_model_source(&self.store, &request),
1143
+ write_budget_bytes,
1144
+ )
1137
1145
  })
1138
1146
  .or_refuse(py)?;
1139
1147
  let out = PyDict::new(py);
@@ -1145,6 +1153,11 @@ impl PyStore {
1145
1153
  out.set_item("deferred_ops", prepared.deferred_ops)?;
1146
1154
  out.set_item("converted_bytes", prepared.converted_bytes)?;
1147
1155
  out.set_item("largest_op_bytes", prepared.largest_op_bytes)?;
1156
+ out.set_item("required_write_bytes", prepared.required_write_bytes)?;
1157
+ out.set_item(
1158
+ "write_interval_bytes",
1159
+ tensorfs_core::durability::INTERVAL_BYTES,
1160
+ )?;
1148
1161
  let rows = PyList::empty(py);
1149
1162
  for source in prepared.sources {
1150
1163
  let row = PyDict::new(py);
@@ -1208,6 +1221,7 @@ impl PyStore {
1208
1221
  checkpoints,
1209
1222
  adopt_from_operation_id,
1210
1223
  registry,
1224
+ None,
1211
1225
  )
1212
1226
  }
1213
1227
 
@@ -2798,6 +2812,56 @@ impl PyLease {
2798
2812
  Ok(PyBytes::new(py, &bytes))
2799
2813
  }
2800
2814
 
2815
+ /// Read a bounded semantic part from a header already covered by this read lease.
2816
+ /// This is the same native range reader used by derived writers, without a writer.
2817
+ #[allow(clippy::too_many_arguments)]
2818
+ fn read_part_into(
2819
+ &self,
2820
+ py: Python<'_>,
2821
+ header: &[u8],
2822
+ component: &str,
2823
+ key: &str,
2824
+ role: &str,
2825
+ off: u64,
2826
+ into: &Bound<'_, PyAny>,
2827
+ ) -> PyResult<()> {
2828
+ let header_ref = ObjectRef::of(header);
2829
+ let header = Header::parse(header).or_refuse(py)?;
2830
+ let name = format!("{component}/{key}/{role}");
2831
+ let part = header
2832
+ .components
2833
+ .iter()
2834
+ .find(|(candidate, _)| candidate == component)
2835
+ .and_then(|(_, tensors)| tensors.iter().find(|(candidate, _)| candidate == key))
2836
+ .and_then(|(_, tensor)| tensor.parts.iter().find(|(candidate, _)| candidate == role))
2837
+ .map(|(_, part)| part)
2838
+ .ok_or_else(|| missing(py, "model tensor part", &name))?;
2839
+ let mut buffer = Writable::new(py, into)?;
2840
+ let destination = buffer.slice();
2841
+ let guard = self.lease.lock_py_attached(py).unwrap();
2842
+ let lease = guard.as_ref().ok_or_else(|| released(py))?;
2843
+ lease
2844
+ .covers(&ObjectRange {
2845
+ obj: header_ref,
2846
+ off: 0,
2847
+ len: 0,
2848
+ })
2849
+ .or_refuse(py)?;
2850
+ py.detach(|| {
2851
+ lease.recheck(&self.meta)?;
2852
+ read::read_part_into(
2853
+ lease,
2854
+ &name,
2855
+ part,
2856
+ off,
2857
+ destination.len() as u64,
2858
+ destination,
2859
+ )?;
2860
+ lease.recheck(&self.meta)
2861
+ })
2862
+ .or_refuse(py)
2863
+ }
2864
+
2801
2865
  /// **The streaming shape.** One plan, a ring of CALLER-owned slots, and a reader pool
2802
2866
  /// that never stops at a batch boundary.
2803
2867
  ///
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.32"
7
+ version = "0.3.34"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [
@@ -108,6 +108,8 @@ class PreparedModelSource(TypedDict):
108
108
  deferred_ops: int
109
109
  converted_bytes: int
110
110
  largest_op_bytes: int
111
+ required_write_bytes: int
112
+ write_interval_bytes: int
111
113
 
112
114
  class ManifestRow(TypedDict):
113
115
  manifest_id: str
@@ -351,6 +353,9 @@ class ReadLease:
351
353
  self, object: str, object_length: int, off: int, length: int, into: object
352
354
  ) -> None: ...
353
355
  def read_asset(self, header: _Bytes, name: str, max_bytes: int = 67108864) -> _Bytes: ...
356
+ def read_part_into(
357
+ self, header: _Bytes, component: str, key: str, role: str, off: int, into: object
358
+ ) -> None: ...
354
359
  def stream(
355
360
  self,
356
361
  plan: ReadPlan,
@@ -519,6 +524,7 @@ class Store:
519
524
  checkpoints: Sequence[tuple[str, str, int]] = ...,
520
525
  adopt_from_operation_id: str | None = ...,
521
526
  registry: bytes | None = None,
527
+ write_budget_bytes: int | None = None,
522
528
  ) -> PreparedModelSource: ...
523
529
  def checkpoint_page(
524
530
  self, head_id: str, head_length: int, *, operation_id: str, slot: str,