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.
- {tensorfs-0.3.43 → tensorfs-0.3.45}/Cargo.lock +2 -2
- {tensorfs-0.3.43 → tensorfs-0.3.45}/Cargo.toml +1 -1
- {tensorfs-0.3.43 → tensorfs-0.3.45}/PKG-INFO +5 -1
- tensorfs-0.3.45/crates/tensorfs-core/src/asset_update.rs +293 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/lib.rs +1 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/http.rs +37 -3
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/tests.rs +130 -4
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/store.rs +37 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/pyproject.toml +4 -1
- {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/__init__.py +25 -3
- {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/_ext.pyi +7 -0
- tensorfs-0.3.45/python/tensorfs/_tensors.py +498 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/catalog.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/pull.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.43 → tensorfs-0.3.45}/python/tensorfs/errors.py +0 -0
- {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.
|
|
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.
|
|
438
|
+
version = "0.3.45"
|
|
439
439
|
dependencies = [
|
|
440
440
|
"pyo3",
|
|
441
441
|
"tensorfs-core",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tensorfs
|
|
3
|
-
Version: 0.3.
|
|
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
|
+
}
|
|
@@ -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
|
|
927
|
-
//
|
|
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(
|
|
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
|
|
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.
|
|
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"
|