tensorfs 0.3.43__tar.gz → 0.3.45__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 (137) hide show
  1. {tensorfs-0.3.43 → tensorfs-0.3.45}/Cargo.lock +2 -2
  2. {tensorfs-0.3.43 → tensorfs-0.3.45}/Cargo.toml +1 -1
  3. {tensorfs-0.3.43 → tensorfs-0.3.45}/PKG-INFO +5 -1
  4. tensorfs-0.3.45/crates/tensorfs-core/src/asset_update.rs +293 -0
  5. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/lib.rs +1 -0
  6. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/http.rs +37 -3
  7. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/tests.rs +130 -4
  8. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/store.rs +37 -0
  9. {tensorfs-0.3.43 → tensorfs-0.3.45}/pyproject.toml +4 -1
  10. {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/__init__.py +25 -3
  11. {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/_ext.pyi +7 -0
  12. tensorfs-0.3.45/python/tensorfs/_tensors.py +498 -0
  13. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/Cargo.toml +0 -0
  14. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/build.rs +0 -0
  15. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  16. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  17. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  18. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  19. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/b64.rs +0 -0
  20. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  21. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  22. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  23. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  24. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  25. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  26. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  27. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  28. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  29. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  30. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/canon.rs +0 -0
  31. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/capability.rs +0 -0
  32. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/catalog.rs +0 -0
  33. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/cbor.rs +0 -0
  34. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  35. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  36. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/corpus.rs +0 -0
  37. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  38. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  39. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  40. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  41. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  42. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  43. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  44. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived.rs +0 -0
  45. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/dtype.rs +0 -0
  46. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/durability.rs +0 -0
  47. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/err.rs +0 -0
  48. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/fetch.rs +0 -0
  49. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/fill.rs +0 -0
  50. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/fit.rs +0 -0
  51. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/gc.rs +0 -0
  52. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/header.rs +0 -0
  53. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/home.rs +0 -0
  54. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ids.rs +0 -0
  55. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  56. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  57. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  58. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  59. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  60. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  61. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  62. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  63. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  64. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  65. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  66. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/jcs.rs +0 -0
  67. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/limits.rs +0 -0
  68. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/machine.rs +0 -0
  69. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/manifest.rs +0 -0
  70. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/meta.rs +0 -0
  71. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/project.rs +0 -0
  72. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/providers.rs +0 -0
  73. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/read.rs +0 -0
  74. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/receipt.rs +0 -0
  75. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/reclaim.rs +0 -0
  76. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/registry.rs +0 -0
  77. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/relation.rs +0 -0
  78. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  79. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/repository.rs +0 -0
  80. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/sha256.rs +0 -0
  81. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  82. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/spec.rs +0 -0
  83. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/staging.rs +0 -0
  84. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/storage.rs +0 -0
  85. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/store.rs +0 -0
  86. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/api.rs +0 -0
  87. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  88. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  89. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  90. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  91. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  92. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  93. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/push.rs +0 -0
  94. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  95. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/vectors.rs +0 -0
  96. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  97. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  98. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fit.rs +0 -0
  99. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  100. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  101. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  102. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  103. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  104. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  105. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  106. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  107. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  108. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/home.rs +0 -0
  109. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  110. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  111. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/liveness.rs +0 -0
  112. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  113. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  114. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  115. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  116. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  117. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  118. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  119. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  120. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  121. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  122. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  123. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  124. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/staging.rs +0 -0
  125. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  126. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  127. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/version.rs +0 -0
  128. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/write_through.rs +0 -0
  129. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/Cargo.toml +0 -0
  130. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/docs.rs +0 -0
  131. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/errors.rs +0 -0
  132. {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/lib.rs +0 -0
  133. {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/_derived_channel.py +0 -0
  134. {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/_source_channel.py +0 -0
  135. {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/derived.py +0 -0
  136. {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/errors.py +0 -0
  137. {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/py.typed +0 -0
@@ -421,7 +421,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
421
421
 
422
422
  [[package]]
423
423
  name = "tensorfs-core"
424
- version = "0.3.43"
424
+ version = "0.3.45"
425
425
  dependencies = [
426
426
  "fs2",
427
427
  "libc",
@@ -435,7 +435,7 @@ dependencies = [
435
435
 
436
436
  [[package]]
437
437
  name = "tensorfs-py"
438
- version = "0.3.43"
438
+ version = "0.3.45"
439
439
  dependencies = [
440
440
  "pyo3",
441
441
  "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.43"
6
+ version = "0.3.45"
7
7
  edition = "2021"
@@ -1,8 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.43
3
+ Version: 0.3.45
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
7
+ Requires-Dist: torch>=2.11 ; extra == 'torch'
8
+ Requires-Dist: torchao==0.18.0 ; extra == 'torch'
9
+ Requires-Dist: numpy>=1.26 ; extra == 'torch'
10
+ Provides-Extra: torch
7
11
  Summary: TensorFS — canonical tensor artifact substrate
8
12
  Requires-Python: >=3.12
@@ -0,0 +1,293 @@
1
+ //! Metadata-only model-asset updates.
2
+ //!
3
+ //! An asset update rewrites only the CozyTensors header and manifest. Tensor part
4
+ //! ObjectRefs are copied verbatim; asset source objects are selected from an existing
5
+ //! manifest and are never opened or rehashed by this operation. Payload verification
6
+ //! remains the normal `read_asset` responsibility (or the repository admission gate).
7
+
8
+ use std::collections::BTreeMap;
9
+
10
+ use crate::checkpoint;
11
+ use crate::err::{refuse, Code, Refusal, Result};
12
+ use crate::header::Asset;
13
+ use crate::ids::ObjectRef;
14
+ use crate::manifest::{Entry, Manifest};
15
+ use crate::store::Store;
16
+
17
+ #[derive(Debug, Clone, PartialEq, Eq)]
18
+ pub enum Edit {
19
+ /// Add or replace one named asset from an exact ordinary-file entry in the source
20
+ /// manifest. The source ObjectRef becomes the sole segment; no payload is read.
21
+ Copy {
22
+ name: String,
23
+ source_path: String,
24
+ media_type: String,
25
+ },
26
+ /// Remove an asset by its exact header name. Missing names are harmless, making
27
+ /// retries deterministic.
28
+ Drop { name: String },
29
+ }
30
+
31
+ #[derive(Debug, Clone, PartialEq, Eq)]
32
+ pub struct ResultSnapshot {
33
+ pub header: ObjectRef,
34
+ pub manifest: ObjectRef,
35
+ }
36
+
37
+ fn source_object(manifest: &Manifest, path: &str) -> Result<ObjectRef> {
38
+ manifest
39
+ .entries()
40
+ .iter()
41
+ .find_map(|(candidate, entry)| {
42
+ (candidate == path).then(|| match entry {
43
+ Entry::File(object) => Ok(object.clone()),
44
+ Entry::CozyTensors(_) => Err(Refusal {
45
+ code: Code::WRONG_TYPE,
46
+ detail: format!("asset source {path:?} is a CozyTensors header, not a file"),
47
+ }),
48
+ })
49
+ })
50
+ .unwrap_or_else(|| {
51
+ Err(Refusal {
52
+ code: Code::OBJECT_ABSENT,
53
+ detail: format!("asset source {path:?} is absent from the source manifest"),
54
+ })
55
+ })
56
+ }
57
+
58
+ /// Apply add/replace/drop edits to one existing checkpoint without reading tensor payloads.
59
+ ///
60
+ /// The source manifest and header are read, and each selected source object is checked only
61
+ /// through its existing verification record and length. The new header and manifest are the
62
+ /// only bytes written. Callers that need a payload guarantee should subsequently use
63
+ /// [`crate::checkpoint::read_asset`], which hashes the reassembled asset once.
64
+ pub fn update(
65
+ store: &Store,
66
+ source_manifest: &ObjectRef,
67
+ edits: &[Edit],
68
+ ) -> Result<ResultSnapshot> {
69
+ let source = checkpoint::load_manifest(store, source_manifest)?;
70
+ let header_ref = source.header().ok_or_else(|| Refusal {
71
+ code: Code::ATTACHMENT_CARDINALITY,
72
+ detail: "asset update source has no CozyTensors header".into(),
73
+ })?;
74
+ let mut header = checkpoint::load_header(store, header_ref)?;
75
+ let closure = checkpoint::load_closure(store, &header)?;
76
+
77
+ let mut assets: BTreeMap<String, Asset> = header.assets.drain(..).collect();
78
+ for edit in edits {
79
+ match edit {
80
+ Edit::Drop { name } => {
81
+ assets.remove(name);
82
+ }
83
+ Edit::Copy {
84
+ name,
85
+ source_path,
86
+ media_type,
87
+ } => {
88
+ if name.is_empty() || media_type.is_empty() {
89
+ return refuse(
90
+ Code::MISSING_FIELD,
91
+ "asset name and media_type are required",
92
+ );
93
+ }
94
+ let object = source_object(&source, source_path)?;
95
+ // This consults only the durable record's inode/length facts. It does not
96
+ // open or hash the payload, which is the defining property of this operation.
97
+ let record = store
98
+ .record_valid(&object.sha256)
99
+ .map_err(|detail| Refusal {
100
+ code: Code::OBJECT_CORRUPT,
101
+ detail: format!("asset source {} is not verified: {detail}", object.id()),
102
+ })?;
103
+ if record.length != object.length {
104
+ return refuse(
105
+ Code::LENGTH_MISMATCH,
106
+ format!("asset source {} record length differs", object.id()),
107
+ );
108
+ }
109
+ assets.insert(
110
+ name.clone(),
111
+ Asset {
112
+ logical_sha256: object.sha256.clone(),
113
+ logical_length: object.length,
114
+ media_type: media_type.clone(),
115
+ segments: vec![object],
116
+ },
117
+ );
118
+ }
119
+ }
120
+ }
121
+ header.assets = assets.into_iter().collect();
122
+ header.validate(&closure)?;
123
+ let header_object = checkpoint::put_doc(store, &header)?;
124
+ let manifest = checkpoint::build_manifest(&header, &header_object, &closure)?;
125
+ let manifest_object = store.put_manifest(&manifest)?.obj;
126
+ Ok(ResultSnapshot {
127
+ header: header_object,
128
+ manifest: manifest_object,
129
+ })
130
+ }
131
+
132
+ #[cfg(test)]
133
+ mod tests {
134
+ use super::*;
135
+ use crate::checkpoint;
136
+ use crate::header::{Body, Header, Part, Tensor};
137
+ use crate::manifest::Draft;
138
+ use crate::store::Fault;
139
+ use crate::{dtype::Dtype, registry};
140
+
141
+ fn temp() -> std::path::PathBuf {
142
+ std::env::temp_dir().join(format!(
143
+ "tensorfs-asset-update-{}",
144
+ crate::meta::now_nanos_unique()
145
+ ))
146
+ }
147
+
148
+ fn put(store: &Store, bytes: &[u8]) -> ObjectRef {
149
+ let expected = ObjectRef::of(bytes);
150
+ let mut input = bytes;
151
+ store
152
+ .put_stream(&mut input, Some(&expected), &Fault::default())
153
+ .unwrap()
154
+ .obj
155
+ }
156
+
157
+ #[test]
158
+ fn copies_asset_refs_without_changing_tensor_refs_and_read_asset_verifies_bytes() {
159
+ let root = temp();
160
+ let store = Store::init(&root).unwrap();
161
+ let spec = registry::seeds()
162
+ .into_iter()
163
+ .find(|seed| seed.alias == "plain/1")
164
+ .unwrap()
165
+ .spec;
166
+ let header = Header {
167
+ configs: vec![],
168
+ assets: vec![],
169
+ encodings: vec![spec.clone()],
170
+ components: vec![(
171
+ "model".into(),
172
+ vec![(
173
+ "weight".into(),
174
+ Tensor {
175
+ dtype: Dtype::U8,
176
+ shape: vec![3],
177
+ encoding: spec.object_id(),
178
+ parts: vec![(
179
+ "value".into(),
180
+ Part {
181
+ dtype: Dtype::U8,
182
+ shape: vec![3],
183
+ body: Body::Inline(vec![1, 2, 3]),
184
+ },
185
+ )],
186
+ },
187
+ )],
188
+ )],
189
+ };
190
+ let header_ref = put(&store, &header.canonical_bytes().unwrap());
191
+ let asset_bytes = b"tokenizer bytes";
192
+ let asset_ref = put(&store, asset_bytes);
193
+ let source = Draft {
194
+ entries: vec![
195
+ ("model.cozytensors".into(), Entry::CozyTensors(header_ref)),
196
+ ("tokenizer/vocab.txt".into(), Entry::File(asset_ref.clone())),
197
+ ],
198
+ }
199
+ .seal()
200
+ .unwrap();
201
+ let source_ref = store.put_manifest(&source).unwrap().obj;
202
+
203
+ let result = update(
204
+ &store,
205
+ &source_ref,
206
+ &[Edit::Copy {
207
+ name: "tokenizer/vocab.txt".into(),
208
+ source_path: "tokenizer/vocab.txt".into(),
209
+ media_type: "text/plain".into(),
210
+ }],
211
+ )
212
+ .unwrap();
213
+ let output = checkpoint::load_header(&store, &result.header).unwrap();
214
+ assert_eq!(output.tensors().count(), 1);
215
+ assert_eq!(
216
+ output.tensors().next().unwrap().2.parts[0].1.body,
217
+ Body::Inline(vec![1, 2, 3])
218
+ );
219
+ let asset = &output.assets[0].1;
220
+ assert_eq!(asset.segments, vec![asset_ref]);
221
+ assert_eq!(asset.logical_length, asset_bytes.len() as u64);
222
+ let mut verified = Vec::new();
223
+ checkpoint::read_asset(&store, "tokenizer/vocab.txt", asset, &mut verified).unwrap();
224
+ assert_eq!(verified, asset_bytes);
225
+ std::fs::remove_dir_all(root).unwrap();
226
+ }
227
+
228
+ #[test]
229
+ fn drops_are_deterministic_and_do_not_touch_payloads() {
230
+ let root = temp();
231
+ let store = Store::init(&root).unwrap();
232
+ let spec = registry::seeds().into_iter().next().unwrap().spec;
233
+ let payload = put(&store, b"asset");
234
+ let header = Header {
235
+ configs: vec![],
236
+ assets: vec![(
237
+ "old".into(),
238
+ Asset {
239
+ logical_sha256: payload.sha256.clone(),
240
+ logical_length: payload.length,
241
+ media_type: "text/plain".into(),
242
+ segments: vec![payload.clone()],
243
+ },
244
+ )],
245
+ encodings: vec![spec.clone()],
246
+ components: vec![(
247
+ "model".into(),
248
+ vec![(
249
+ "w".into(),
250
+ Tensor {
251
+ dtype: Dtype::U8,
252
+ shape: vec![1],
253
+ encoding: spec.object_id(),
254
+ parts: vec![(
255
+ "value".into(),
256
+ Part {
257
+ dtype: Dtype::U8,
258
+ shape: vec![1],
259
+ body: Body::Inline(vec![0]),
260
+ },
261
+ )],
262
+ },
263
+ )],
264
+ )],
265
+ };
266
+ let href = put(&store, &header.canonical_bytes().unwrap());
267
+ let source = Draft {
268
+ entries: vec![("model.cozytensors".into(), Entry::CozyTensors(href))],
269
+ }
270
+ .seal()
271
+ .unwrap();
272
+ let source_ref = store.put_manifest(&source).unwrap().obj;
273
+ let one = update(
274
+ &store,
275
+ &source_ref,
276
+ &[Edit::Drop {
277
+ name: "missing".into(),
278
+ }],
279
+ )
280
+ .unwrap();
281
+ let two = update(
282
+ &store,
283
+ &source_ref,
284
+ &[Edit::Drop {
285
+ name: "missing".into(),
286
+ }],
287
+ )
288
+ .unwrap();
289
+ assert_eq!(one.header, two.header);
290
+ assert_eq!(one.manifest, two.manifest);
291
+ std::fs::remove_dir_all(root).unwrap();
292
+ }
293
+ }
@@ -8,6 +8,7 @@
8
8
  // crates/tensorfs-core/src is the check that it stays that way.
9
9
  #![deny(unsafe_code)]
10
10
 
11
+ pub mod asset_update;
11
12
  pub mod b64;
12
13
  mod cache_roots;
13
14
  pub mod canon;
@@ -21,6 +21,11 @@
21
21
  //! exactly zero bytes. A CONTROL-plane exchange now answers to `Judge::Transfer`: the
22
22
  //! pull's own progress, which is a measurement, and never to this call's own duration,
23
23
  //! which would be a constant.
24
+ //!
25
+ //! Object GETs also apply their existing measured stream-silence rule while
26
+ //! handshaking, writing and reading headers (h3a-070). Previously Judge::Stream
27
+ //! was ignored in those phases, so body retries were unreachable for a silent
28
+ //! TLS peer. Phase progress resets that phase's clock; no object wall limit is added.
24
29
 
25
30
  use crate::err::{refuse, Code, Refusal, Result};
26
31
  use crate::transport::ledger::{Deadline, Ledger};
@@ -122,6 +127,24 @@ fn transfer_verdict(
122
127
  )
123
128
  }
124
129
 
130
+ /// Object-stream silence is judged before the body too. Each phase starts with
131
+ /// its own window and advances only when transport bytes actually move; slow
132
+ /// progress may span arbitrarily many windows without exhausting a wall budget.
133
+ fn stream_verdict(judge: Judge, ledger: &Ledger, advanced: Instant, phase: &str) -> Result<()> {
134
+ if judge != Judge::Stream || advanced.elapsed() <= ledger.patience() {
135
+ return Ok(());
136
+ }
137
+ refuse(
138
+ Code::TRANSFER_FAILED,
139
+ format!(
140
+ "the object stream stopped during {phase}: silent {:.1} s against the \
141
+ {:.1} s this pull's measured pace allows",
142
+ advanced.elapsed().as_secs_f64(),
143
+ ledger.patience().as_secs_f64(),
144
+ ),
145
+ )
146
+ }
147
+
125
148
  pub struct Client {
126
149
  tls: Arc<rustls::ClientConfig>,
127
150
  }
@@ -283,6 +306,7 @@ fn tls_handshake(
283
306
  fail: Code,
284
307
  ) -> Result<()> {
285
308
  let sample = ledger.sample();
309
+ let mut advanced = Instant::now();
286
310
  while conn.is_handshaking() {
287
311
  transfer_verdict(judge, ledger, since, "a TLS handshake")?;
288
312
  let tick = match deadline.remaining()? {
@@ -293,8 +317,11 @@ fn tls_handshake(
293
317
  let _ = sock.set_write_timeout(Some(tick.max(Duration::from_millis(1))));
294
318
  if conn.wants_write() {
295
319
  match conn.write_tls(sock) {
320
+ Ok(n) if n > 0 => advanced = Instant::now(),
296
321
  Ok(_) => {}
297
- Err(e) if would_block(&e) => {}
322
+ Err(e) if would_block(&e) => {
323
+ stream_verdict(judge, ledger, advanced, "TLS handshake")?;
324
+ }
298
325
  Err(e) => return refuse(fail, format!("TLS handshake write: {e}")),
299
326
  }
300
327
  continue;
@@ -302,12 +329,15 @@ fn tls_handshake(
302
329
  match conn.read_tls(sock) {
303
330
  Ok(0) => return refuse(fail, "origin closed the connection mid-handshake"),
304
331
  Ok(_) => {
332
+ advanced = Instant::now();
305
333
  conn.process_new_packets().map_err(|e| Refusal {
306
334
  code: fail,
307
335
  detail: format!("TLS refused: {e}"),
308
336
  })?;
309
337
  }
310
- Err(e) if would_block(&e) => {}
338
+ Err(e) if would_block(&e) => {
339
+ stream_verdict(judge, ledger, advanced, "TLS handshake")?;
340
+ }
311
341
  Err(e) => return refuse(fail, format!("TLS handshake read: {e}")),
312
342
  }
313
343
  }
@@ -733,6 +763,7 @@ fn write_all(
733
763
  Err(e) => return refuse(fail, format!("the connection failed mid-write: {e}")),
734
764
  }
735
765
  }
766
+ stream_verdict(judge, ledger, advanced, "request write")?;
736
767
  if let Some(bound) = silence {
737
768
  if advanced.elapsed() > bound {
738
769
  return refuse(
@@ -811,6 +842,7 @@ fn read_step_ticking(
811
842
  since: Instant,
812
843
  fail: Code,
813
844
  ) -> Result<usize> {
845
+ let advanced = Instant::now();
814
846
  loop {
815
847
  transfer_verdict(judge, ledger, since, "a response head")?;
816
848
  let tick = match deadline.remaining()? {
@@ -820,7 +852,9 @@ fn read_step_ticking(
820
852
  wire.set_read_timeout(tick);
821
853
  match wire.read_step(buf) {
822
854
  Ok(n) => return Ok(n),
823
- Err(e) if would_block(&e) => {}
855
+ Err(e) if would_block(&e) => {
856
+ stream_verdict(judge, ledger, advanced, "response head")?;
857
+ }
824
858
  Err(e) => return refuse(fail, format!("the connection failed: {e}")),
825
859
  }
826
860
  }
@@ -57,6 +57,8 @@ impl Request {
57
57
  }
58
58
 
59
59
  enum Serve {
60
+ /// Read the request, then hold the socket without sending response headers.
61
+ StallHead,
60
62
  /// Content-Length framed whole answer.
61
63
  Whole {
62
64
  status: u16,
@@ -146,6 +148,11 @@ fn handle(mut socket: TcpStream, dispatch: Dispatch, hits: Arc<Mutex<HashMap<Str
146
148
  .entry(request.path.clone())
147
149
  .or_insert(0) += 1;
148
150
  match dispatch(&request) {
151
+ Serve::StallHead => {
152
+ let mut sink = [0u8; 16];
153
+ let _ = socket.set_read_timeout(Some(Duration::from_secs(30)));
154
+ let _ = socket.read(&mut sink);
155
+ }
149
156
  Serve::Whole {
150
157
  status,
151
158
  body,
@@ -915,6 +922,120 @@ fn black_holed_stream_is_stalled_by_the_ledger_and_reasked() {
915
922
  let _ = std::fs::remove_dir_all(root);
916
923
  }
917
924
 
925
+ fn prebody_silence_is_judged(https: bool) {
926
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
927
+ let port = listener.local_addr().unwrap().port();
928
+ std::thread::spawn(move || {
929
+ let (mut socket, _) = listener.accept().unwrap();
930
+ socket
931
+ .set_read_timeout(Some(Duration::from_secs(3)))
932
+ .unwrap();
933
+ // Accept HTTP request bytes or a TLS ClientHello, but never answer.
934
+ let mut bytes = [0u8; 8192];
935
+ while matches!(socket.read(&mut bytes), Ok(n) if n > 0) {}
936
+ });
937
+ let scheme = if https { "https" } else { "http" };
938
+ let checked = local_policy()
939
+ .check(&format!("{scheme}://127.0.0.1:{port}/object"))
940
+ .unwrap();
941
+ let ledger = Ledger::with_resolution(Duration::from_millis(25));
942
+ ledger.moved(1024); // A pull that has already moved bytes, as on the real pod.
943
+ let response = super::http::request(
944
+ &super::http::Client::new(),
945
+ &checked,
946
+ "GET",
947
+ &[],
948
+ None,
949
+ Deadline::after_seconds(Some(1.0)),
950
+ &ledger,
951
+ super::http::Judge::Stream,
952
+ None,
953
+ Code::TRANSFER_FAILED,
954
+ );
955
+ let refusal = match response {
956
+ Err(refusal) => refusal,
957
+ Ok(_) => panic!("silent origin unexpectedly answered"),
958
+ };
959
+ assert_eq!(refusal.code, Code::TRANSFER_FAILED, "{refusal}");
960
+ assert!(
961
+ refusal.detail.contains(if https {
962
+ "TLS handshake"
963
+ } else {
964
+ "response head"
965
+ }),
966
+ "{refusal}"
967
+ );
968
+ }
969
+
970
+ #[test]
971
+ fn an_object_stalled_before_headers_is_judged_by_stream_progress() {
972
+ prebody_silence_is_judged(false);
973
+ }
974
+
975
+ #[test]
976
+ fn an_object_stalled_during_tls_is_judged_by_stream_progress() {
977
+ prebody_silence_is_judged(true);
978
+ }
979
+
980
+ #[test]
981
+ fn a_prebody_stall_is_reasked_without_discarding_completed_objects() {
982
+ let root = temporary("head-stall-retry");
983
+ let store = Store::init(&root).unwrap();
984
+ let checkpoint = Arc::new(checkpoint(2, 64 * 1024));
985
+ let victim = checkpoint.objects[0].sha256.clone();
986
+ let selected = victim.clone();
987
+ let origin = hub_origin(Arc::clone(&checkpoint), move |hex, attempt| {
988
+ (hex == selected && attempt == 1).then_some(Serve::StallHead)
989
+ });
990
+ let policy = SourcePolicy::default();
991
+ let report = pull(&request_for(&store, &origin.base, &policy)).unwrap();
992
+ assert_eq!(report.fetched, 4);
993
+ for object in &checkpoint.objects {
994
+ assert!(store.record_valid(&object.sha256).is_ok());
995
+ assert_eq!(
996
+ origin.hits(&format!("/obj/{}", object.sha256)),
997
+ if object.sha256 == victim { 2 } else { 1 }
998
+ );
999
+ }
1000
+ let _ = std::fs::remove_dir_all(root);
1001
+ }
1002
+
1003
+ #[test]
1004
+ fn fragmented_headers_can_progress_across_multiple_stream_windows() {
1005
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1006
+ let port = listener.local_addr().unwrap().port();
1007
+ std::thread::spawn(move || {
1008
+ let (mut socket, _) = listener.accept().unwrap();
1009
+ read_request(&mut socket).unwrap();
1010
+ socket.write_all(b"HTTP/1.1 200 OK\r\n").unwrap();
1011
+ for _ in 0..10 {
1012
+ std::thread::sleep(Duration::from_millis(50));
1013
+ socket.write_all(b"X-Progress: yes\r\n").unwrap();
1014
+ }
1015
+ socket.write_all(b"Content-Length: 3\r\n\r\nabc").unwrap();
1016
+ });
1017
+ let checked = local_policy()
1018
+ .check(&format!("http://127.0.0.1:{port}/object"))
1019
+ .unwrap();
1020
+ let ledger = Ledger::with_resolution(Duration::from_millis(25));
1021
+ let started = std::time::Instant::now();
1022
+ let response = super::http::request(
1023
+ &super::http::Client::new(),
1024
+ &checked,
1025
+ "GET",
1026
+ &[],
1027
+ None,
1028
+ Deadline::none(),
1029
+ &ledger,
1030
+ super::http::Judge::Stream,
1031
+ None,
1032
+ Code::TRANSFER_FAILED,
1033
+ )
1034
+ .unwrap();
1035
+ assert_eq!(response.read_capped(3).unwrap(), b"abc");
1036
+ assert!(started.elapsed() > ledger.floor() * 2);
1037
+ }
1038
+
918
1039
  #[test]
919
1040
  fn slow_origin_teaches_the_ledger_and_is_left_alone() {
920
1041
  let root = temporary("slow");
@@ -923,21 +1044,26 @@ fn slow_origin_teaches_the_ledger_and_is_left_alone() {
923
1044
  let slow = checkpoint.objects[0].sha256.clone();
924
1045
  let slow_for = slow.clone();
925
1046
  let bodies = checkpoint.bodies.clone();
926
- // 400 ms to first byte teaches the ledger this origin pauses; the 200 ms drip gaps
927
- // that follow are far over the 150 ms noise floor and must NOT be called stalls.
1047
+ // A successful 400 ms first answer teaches this origin's latency; subsequent
1048
+ // 800 ms gaps exceed the initial floor but remain below that measured term.
1049
+ // Unlike the old body-only rule,
1050
+ // initial pre-header silence is now bounded, so this fixture gives the first
1051
+ // answer a sufficient 600 ms floor. Production timing constants are unchanged.
928
1052
  let origin = hub_origin(Arc::clone(&checkpoint), move |hex, _| {
929
1053
  if hex == slow_for {
930
1054
  return Some(Serve::Drip {
931
1055
  body: bodies[hex].clone(),
932
1056
  chunk: 1024,
933
- gap: Duration::from_millis(200),
1057
+ gap: Duration::from_millis(800),
934
1058
  ttfb: Duration::from_millis(400),
935
1059
  });
936
1060
  }
937
1061
  None
938
1062
  });
939
1063
  let policy = SourcePolicy::default();
940
- let report = pull(&request_for(&store, &origin.base, &policy)).unwrap();
1064
+ let mut request = request_for(&store, &origin.base, &policy);
1065
+ request.sample_seconds = 0.1;
1066
+ let report = pull(&request).unwrap();
941
1067
  assert_eq!(report.fetched, 3);
942
1068
  assert_eq!(origin.hits(&format!("/obj/{slow}")), 1); // one ask; never condemned
943
1069
  let _ = std::fs::remove_dir_all(root);
@@ -591,6 +591,43 @@ impl PyStore {
591
591
  tensorfs_core::IMPLEMENTATION_SHA256
592
592
  }
593
593
 
594
+ /// Rewrite only a CozyTensors header/manifest to add, replace, or drop model assets.
595
+ /// `copies` are `(name, source_path, media_type)` rows selected from the exact source
596
+ /// manifest; TensorFS reuses their ObjectRefs without reading payload bytes.
597
+ #[pyo3(signature = (manifest_id, manifest_length, copies=Vec::new(), drops=Vec::new()))]
598
+ fn update_model_assets<'p>(
599
+ &self,
600
+ py: Python<'p>,
601
+ manifest_id: &str,
602
+ manifest_length: u64,
603
+ copies: Vec<(String, String, String)>,
604
+ drops: Vec<String>,
605
+ ) -> PyResult<Bound<'p, PyDict>> {
606
+ let source = obj_ref(py, manifest_id, manifest_length)?;
607
+ let mut edits = Vec::with_capacity(copies.len() + drops.len());
608
+ edits.extend(copies.into_iter().map(|(name, source_path, media_type)| {
609
+ tensorfs_core::asset_update::Edit::Copy {
610
+ name,
611
+ source_path,
612
+ media_type,
613
+ }
614
+ }));
615
+ edits.extend(
616
+ drops
617
+ .into_iter()
618
+ .map(|name| tensorfs_core::asset_update::Edit::Drop { name }),
619
+ );
620
+ let result = py
621
+ .detach(|| tensorfs_core::asset_update::update(&self.store, &source, &edits))
622
+ .or_refuse(py)?;
623
+ let out = PyDict::new(py);
624
+ out.set_item("header_id", result.header.id())?;
625
+ out.set_item("header_length", result.header.length)?;
626
+ out.set_item("manifest_id", result.manifest.id())?;
627
+ out.set_item("manifest_length", result.manifest.length)?;
628
+ Ok(out)
629
+ }
630
+
594
631
  fn verify_checkpoint_source(
595
632
  &self,
596
633
  py: Python<'_>,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.43"
7
+ version = "0.3.45"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [
@@ -13,6 +13,9 @@ classifiers = [
13
13
  "Programming Language :: Rust",
14
14
  ]
15
15
 
16
+ [project.optional-dependencies]
17
+ torch = ["torch>=2.11", "torchao==0.18.0", "numpy>=1.26"]
18
+
16
19
  [tool.maturin]
17
20
  manifest-path = "crates/tensorfs-py/Cargo.toml"
18
21
  module-name = "tensorfs._ext"