tensorfs 0.3.78__tar.gz → 0.3.80__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.78 → tensorfs-0.3.80}/Cargo.lock +2 -2
- {tensorfs-0.3.78 → tensorfs-0.3.80}/Cargo.toml +1 -1
- {tensorfs-0.3.78 → tensorfs-0.3.80}/PKG-INFO +1 -1
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/ingest.rs +1 -0
- tensorfs-0.3.80/crates/tensorfs-core/src/catalog/lock.rs +206 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/catalog.rs +28 -1
- tensorfs-0.3.78/crates/tensorfs-core/src/repo_cache/identity.rs → tensorfs-0.3.80/crates/tensorfs-core/src/filesystem_identity.rs +30 -24
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/fill.rs +3 -3
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/convert.rs +10 -12
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/fingerprint.rs +22 -3
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/h3_lora.rs +12 -5
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/preflight.rs +0 -3
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/source.rs +19 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/lib.rs +1 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/meta.rs +67 -14
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/repo_cache.rs +5 -6
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +4 -1
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/pull.rs +3 -1
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/tests.rs +95 -20
- tensorfs-0.3.80/crates/tensorfs-core/tests/catalog_owner_probe.c +29 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/h3_lora.rs +153 -7
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/source_prefixes.rs +3 -1
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/store_boundaries.rs +42 -16
- {tensorfs-0.3.78 → tensorfs-0.3.80}/pyproject.toml +1 -1
- tensorfs-0.3.78/crates/tensorfs-core/src/catalog/lock.rs +0 -121
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/ensure.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/descriptors.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/disk.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ensure/admission.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ensure/flight.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ensure.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/narrow.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/routes/anima.routes.json +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/routes/anima.spec.json +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/routes.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/http.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/pace.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/unsynced.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/as_is.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/civitai_anima.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/ensure.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/source_download.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-py/src/ensure.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-py/src/ingest.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/crates/tensorfs-py/src/store.rs +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/_ext.pyi +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.78 → tensorfs-0.3.80}/python/tensorfs/py.typed +0 -0
|
@@ -439,7 +439,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
439
439
|
|
|
440
440
|
[[package]]
|
|
441
441
|
name = "tensorfs-core"
|
|
442
|
-
version = "0.3.
|
|
442
|
+
version = "0.3.80"
|
|
443
443
|
dependencies = [
|
|
444
444
|
"fs2",
|
|
445
445
|
"libc",
|
|
@@ -454,7 +454,7 @@ dependencies = [
|
|
|
454
454
|
|
|
455
455
|
[[package]]
|
|
456
456
|
name = "tensorfs-py"
|
|
457
|
-
version = "0.3.
|
|
457
|
+
version = "0.3.80"
|
|
458
458
|
dependencies = [
|
|
459
459
|
"pyo3",
|
|
460
460
|
"tensorfs-core",
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
//! Waiting for another process's catalog lock by whether its holder can still release it.
|
|
2
|
+
//!
|
|
3
|
+
//! SQLite's locks are POSIX locks: they die with the process that holds them. A holder
|
|
4
|
+
//! that is alive is committing or about to, so a writer waits for it for as long as it
|
|
5
|
+
//! takes; a flat deadline refused a busy, progressing writer as if it were dead. The only
|
|
6
|
+
//! holder that cannot release is one that is stopped (a debugger or SIGSTOP), and only
|
|
7
|
+
//! then does a statement give up. Dead processes have already released their locks.
|
|
8
|
+
//!
|
|
9
|
+
//! Holders come from `/proc/locks`, matched by the catalog's inode (and its WAL `-shm`).
|
|
10
|
+
//! The catalog file itself is never opened here: closing a second descriptor on it would
|
|
11
|
+
//! drop every POSIX lock this process holds on it.
|
|
12
|
+
|
|
13
|
+
use std::cell::RefCell;
|
|
14
|
+
use std::path::{Path, PathBuf};
|
|
15
|
+
use std::time::Duration;
|
|
16
|
+
|
|
17
|
+
use rusqlite::Connection;
|
|
18
|
+
|
|
19
|
+
use crate::err::Result;
|
|
20
|
+
|
|
21
|
+
thread_local! {
|
|
22
|
+
/// The catalog this thread's statements wait on. A connection is opened for each
|
|
23
|
+
/// operation just before it is used, on the thread that uses it, so the one opened last
|
|
24
|
+
/// here is the one a busy statement belongs to.
|
|
25
|
+
static WAITING_ON: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Install the waiting handler on one connection.
|
|
29
|
+
pub(crate) fn wait_for_live_holders(connection: &Connection, path: &Path) -> Result<()> {
|
|
30
|
+
WAITING_ON.with(|waiting| *waiting.borrow_mut() = Some(path.to_path_buf()));
|
|
31
|
+
connection
|
|
32
|
+
.busy_handler(Some(busy))
|
|
33
|
+
.map_err(|error| super::sql("install catalog lock wait", error))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn busy(attempt: i32) -> bool {
|
|
37
|
+
let path = WAITING_ON.with(|waiting| waiting.borrow().clone());
|
|
38
|
+
if path.is_some_and(|path| holders_cannot_release(&path)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
std::thread::sleep(Duration::from_millis(attempt.clamp(1, 64) as u64));
|
|
42
|
+
true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Every currently held write lock belongs to a stopped process. Without a current
|
|
46
|
+
/// holder to name (the lock just went, or no `/proc`), keep asking.
|
|
47
|
+
fn holders_cannot_release(path: &Path) -> bool {
|
|
48
|
+
let files: Vec<(u64, u64)> = [path.to_path_buf(), shm(path)]
|
|
49
|
+
.iter()
|
|
50
|
+
.filter_map(|file| std::fs::metadata(file).ok())
|
|
51
|
+
.map(|metadata| {
|
|
52
|
+
use std::os::unix::fs::MetadataExt;
|
|
53
|
+
(metadata.dev(), metadata.ino())
|
|
54
|
+
})
|
|
55
|
+
.collect();
|
|
56
|
+
let Ok(locks) = std::fs::read_to_string("/proc/locks") else {
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
snapshots_cannot_release(&locks, &files, std::process::id(), stopped, || {
|
|
60
|
+
std::fs::read_to_string("/proc/locks").ok()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn snapshots_cannot_release(
|
|
65
|
+
observed: &str,
|
|
66
|
+
files: &[(u64, u64)],
|
|
67
|
+
own: u32,
|
|
68
|
+
is_stopped: impl Fn(u32) -> bool,
|
|
69
|
+
current: impl FnOnce() -> Option<String>,
|
|
70
|
+
) -> bool {
|
|
71
|
+
let holders = write_holders(observed, files, own);
|
|
72
|
+
if holders.is_empty() || !holders.iter().all(|pid| is_stopped(*pid)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// A holder may unlock or exit between the lock snapshot and its process-state
|
|
76
|
+
// read. Refuse only if those stopped processes still hold the actual locks.
|
|
77
|
+
let Some(current) = current() else {
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
let current = write_holders(¤t, files, own);
|
|
81
|
+
!current.is_empty()
|
|
82
|
+
&& current
|
|
83
|
+
.iter()
|
|
84
|
+
.all(|pid| holders.contains(pid) && is_stopped(*pid))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn shm(path: &Path) -> PathBuf {
|
|
88
|
+
let mut name = path.as_os_str().to_owned();
|
|
89
|
+
name.push("-shm");
|
|
90
|
+
PathBuf::from(name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Other processes' POSIX write locks on these inodes, from `/proc/locks` lines such as
|
|
94
|
+
/// `3: POSIX ADVISORY WRITE 167671 103:02:204736108 1073741824 1073742335`.
|
|
95
|
+
fn write_holders(locks: &str, files: &[(u64, u64)], own: u32) -> Vec<u32> {
|
|
96
|
+
locks
|
|
97
|
+
.lines()
|
|
98
|
+
.filter_map(|line| {
|
|
99
|
+
let fields: Vec<&str> = line.split_whitespace().collect();
|
|
100
|
+
let at = fields.iter().position(|field| *field == "POSIX")?;
|
|
101
|
+
if fields[..at].contains(&"->") || fields.get(at + 2) != Some(&"WRITE") {
|
|
102
|
+
return None;
|
|
103
|
+
}
|
|
104
|
+
let pid: u32 = fields.get(at + 3)?.parse().ok()?;
|
|
105
|
+
let mut file = fields.get(at + 4)?.split(':');
|
|
106
|
+
let major = u32::from_str_radix(file.next()?, 16).ok()?;
|
|
107
|
+
let minor = u32::from_str_radix(file.next()?, 16).ok()?;
|
|
108
|
+
let inode: u64 = file.next()?.parse().ok()?;
|
|
109
|
+
let device = rustix::fs::makedev(major, minor) as u64;
|
|
110
|
+
(file.next().is_none() && pid != own && files.contains(&(device, inode))).then_some(pid)
|
|
111
|
+
})
|
|
112
|
+
.collect()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Only stopped and traced-stopped processes keep locks they cannot release.
|
|
116
|
+
fn stopped(pid: u32) -> bool {
|
|
117
|
+
std::fs::read_to_string(format!("/proc/{pid}/stat"))
|
|
118
|
+
.ok()
|
|
119
|
+
.is_some_and(|stat| process_is_stopped(&stat))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn process_is_stopped(stat: &str) -> bool {
|
|
123
|
+
stat.rsplit_once(')')
|
|
124
|
+
.and_then(|(_, fields)| fields.split_whitespace().next())
|
|
125
|
+
.is_some_and(|state| matches!(state, "T" | "t"))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[cfg(test)]
|
|
129
|
+
mod tests {
|
|
130
|
+
use super::*;
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn only_other_processes_write_locks_on_the_catalog_are_holders() {
|
|
134
|
+
let locks = "\
|
|
135
|
+
1: POSIX ADVISORY WRITE 4242 103:02:77 1073741824 1073742335
|
|
136
|
+
2: POSIX ADVISORY READ 5151 103:02:77 1073741826 1073742335
|
|
137
|
+
3: POSIX ADVISORY WRITE 6161 103:02:99 120 120
|
|
138
|
+
4: FLOCK ADVISORY WRITE 7171 103:02:77 0 EOF
|
|
139
|
+
5: POSIX ADVISORY WRITE 1000 103:02:77 0 EOF
|
|
140
|
+
6: -> POSIX ADVISORY WRITE 8181 103:02:77 0 EOF
|
|
141
|
+
7: POSIX ADVISORY WRITE 9191 104:02:77 0 EOF
|
|
142
|
+
";
|
|
143
|
+
let device = rustix::fs::makedev(0x103, 2) as u64;
|
|
144
|
+
assert_eq!(write_holders(locks, &[(device, 77)], 1000), vec![4242]);
|
|
145
|
+
assert_eq!(
|
|
146
|
+
write_holders(locks, &[(device, 77), (device, 99)], 1000),
|
|
147
|
+
vec![4242, 6161]
|
|
148
|
+
);
|
|
149
|
+
assert!(write_holders(locks, &[(device, 5)], 1000).is_empty());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn exited_holder_is_progress_and_never_a_stopped_writer() {
|
|
154
|
+
for state in ["Z", "X", "R", "S", "D"] {
|
|
155
|
+
assert!(!process_is_stopped(&format!("4242 (writer) {state} 1 2 3")));
|
|
156
|
+
}
|
|
157
|
+
for state in ["T", "t"] {
|
|
158
|
+
assert!(process_is_stopped(&format!("4242 (writer) {state} 1 2 3")));
|
|
159
|
+
}
|
|
160
|
+
assert!(!process_is_stopped("unavailable"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn stopped_holder_must_still_own_the_lock_before_refusal() {
|
|
165
|
+
let held = "1: POSIX ADVISORY WRITE 4242 103:02:77 0 EOF\n";
|
|
166
|
+
let files = [(rustix::fs::makedev(0x103, 2) as u64, 77)];
|
|
167
|
+
// Stable SIGSTOP remains a typed refusal, while snapshot -> unlock ->
|
|
168
|
+
// SIGSTOP must let SQLite retry rather than refuse the old snapshot.
|
|
169
|
+
assert!(snapshots_cannot_release(
|
|
170
|
+
held,
|
|
171
|
+
&files,
|
|
172
|
+
1000,
|
|
173
|
+
|_| true,
|
|
174
|
+
|| Some(held.into())
|
|
175
|
+
));
|
|
176
|
+
assert!(!snapshots_cannot_release(
|
|
177
|
+
held,
|
|
178
|
+
&files,
|
|
179
|
+
1000,
|
|
180
|
+
|_| true,
|
|
181
|
+
|| Some(String::new())
|
|
182
|
+
));
|
|
183
|
+
assert!(!snapshots_cannot_release(
|
|
184
|
+
held,
|
|
185
|
+
&files,
|
|
186
|
+
1000,
|
|
187
|
+
|_| true,
|
|
188
|
+
|| None
|
|
189
|
+
));
|
|
190
|
+
let replacement = held.replace("4242", "6161");
|
|
191
|
+
assert!(!snapshots_cannot_release(
|
|
192
|
+
held,
|
|
193
|
+
&files,
|
|
194
|
+
1000,
|
|
195
|
+
|_| true,
|
|
196
|
+
|| Some(replacement)
|
|
197
|
+
));
|
|
198
|
+
assert!(!snapshots_cannot_release(
|
|
199
|
+
held,
|
|
200
|
+
&files,
|
|
201
|
+
1000,
|
|
202
|
+
|_| false,
|
|
203
|
+
|| panic!("a live holder must retry")
|
|
204
|
+
));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -104,7 +104,14 @@ pub fn connections_opened(root: &Path) -> u64 {
|
|
|
104
104
|
.unwrap_or(0)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
fn connect(path: &Path) -> Result<Connection> {
|
|
107
|
+
pub(crate) fn connect(path: &Path) -> Result<Connection> {
|
|
108
|
+
// SQLite creates WAL/SHM files before assigning their database owner. Opening
|
|
109
|
+
// them under root exposes a transient root-owned inode to concurrent owner
|
|
110
|
+
// connections, which can permanently fall back to read-only WAL access.
|
|
111
|
+
let _identity = crate::filesystem_identity::FilesystemIdentity::for_store_root(
|
|
112
|
+
path.parent().unwrap_or(path),
|
|
113
|
+
)
|
|
114
|
+
.map_err(|error| io("enter catalog filesystem identity", error))?;
|
|
108
115
|
*opened()
|
|
109
116
|
.lock()
|
|
110
117
|
.unwrap_or_else(|error| error.into_inner())
|
|
@@ -253,6 +260,26 @@ impl Trust {
|
|
|
253
260
|
self.load(&mut kept).map(|_| ())
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
/// Finish setup on the already-validated reader connection. The callback is
|
|
264
|
+
/// synchronous under its mutex; no second setup connection or credential
|
|
265
|
+
/// lifetime escapes into the returned value.
|
|
266
|
+
pub(crate) fn with_connection<T>(
|
|
267
|
+
&self,
|
|
268
|
+
use_connection: impl FnOnce(&mut Connection) -> Result<T>,
|
|
269
|
+
) -> Result<T> {
|
|
270
|
+
let mut kept = self.kept.lock().unwrap_or_else(|error| error.into_inner());
|
|
271
|
+
self.load(&mut kept)?;
|
|
272
|
+
let connection = &mut kept.as_mut().expect("validated connection").connection;
|
|
273
|
+
// A kept connection can be used by another Runtime thread later. Install
|
|
274
|
+
// the lock context on the thread actually executing this operation.
|
|
275
|
+
lock::wait_for_live_holders(connection, &self.path)?;
|
|
276
|
+
let _identity = crate::filesystem_identity::FilesystemIdentity::for_store_root(
|
|
277
|
+
self.path.parent().unwrap_or(&self.path),
|
|
278
|
+
)
|
|
279
|
+
.map_err(|error| io("enter catalog filesystem identity", error))?;
|
|
280
|
+
use_connection(connection)
|
|
281
|
+
}
|
|
282
|
+
|
|
256
283
|
fn miss(&self, sha256: &str) -> Option<crate::store::VRecord> {
|
|
257
284
|
let mut kept = self.kept.lock().unwrap_or_else(|error| error.into_inner());
|
|
258
285
|
if self.load(&mut kept).ok()? {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//! Scope
|
|
1
|
+
//! Scope synchronous filesystem I/O to the trusted local Store owner.
|
|
2
2
|
//!
|
|
3
3
|
//! Only Linux filesystem IDs change, only on the current native thread, and only
|
|
4
|
-
//! while
|
|
5
|
-
//!
|
|
4
|
+
//! while Store I/O runs. Real/effective IDs, other Runtime threads, existing
|
|
5
|
+
//! permissions, and configured paths are unchanged.
|
|
6
6
|
|
|
7
|
-
use
|
|
8
|
-
use crate::store::Store;
|
|
7
|
+
use std::{io, path::Path};
|
|
9
8
|
|
|
10
9
|
#[cfg(target_os = "linux")]
|
|
11
10
|
mod linux {
|
|
@@ -16,7 +15,7 @@ mod linux {
|
|
|
16
15
|
|
|
17
16
|
use nix::unistd::{geteuid, setfsgid, setfsuid, Gid, Uid};
|
|
18
17
|
|
|
19
|
-
use super::{
|
|
18
|
+
use super::{io, Path};
|
|
20
19
|
|
|
21
20
|
// Linux has no getfsuid/getfsgid. An invalid ID cannot be installed; each
|
|
22
21
|
// setter returns the previous/current ID even when the attempted set fails.
|
|
@@ -27,14 +26,14 @@ mod linux {
|
|
|
27
26
|
)
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
pub(
|
|
29
|
+
pub(crate) struct FilesystemIdentity {
|
|
31
30
|
previous: Option<(Uid, Gid)>,
|
|
32
31
|
// A credential guard must be destroyed by the exact thread that made it.
|
|
33
32
|
_thread: PhantomData<Rc<()>>,
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
impl
|
|
37
|
-
pub(
|
|
35
|
+
impl FilesystemIdentity {
|
|
36
|
+
pub(crate) fn for_store_root(root: &Path) -> io::Result<Self> {
|
|
38
37
|
let mut guard = Self {
|
|
39
38
|
previous: None,
|
|
40
39
|
_thread: PhantomData,
|
|
@@ -44,9 +43,12 @@ mod linux {
|
|
|
44
43
|
}
|
|
45
44
|
// Authority is the already-open LOCAL Store's owner, never a UID
|
|
46
45
|
// supplied by an artifact, request, or remote cache directory.
|
|
47
|
-
let owner = fs::symlink_metadata(
|
|
46
|
+
let owner = fs::symlink_metadata(root)?;
|
|
48
47
|
if !owner.is_dir() || owner.uid() == u32::MAX || owner.gid() == u32::MAX {
|
|
49
|
-
return Err(
|
|
48
|
+
return Err(io::Error::new(
|
|
49
|
+
io::ErrorKind::PermissionDenied,
|
|
50
|
+
"cannot enter Store filesystem identity",
|
|
51
|
+
));
|
|
50
52
|
}
|
|
51
53
|
let desired = (Uid::from_raw(owner.uid()), Gid::from_raw(owner.gid()));
|
|
52
54
|
let previous = filesystem_ids();
|
|
@@ -58,14 +60,17 @@ mod linux {
|
|
|
58
60
|
setfsuid(desired.0);
|
|
59
61
|
if filesystem_ids() != desired {
|
|
60
62
|
// Drop restores either field that changed before refusing the
|
|
61
|
-
//
|
|
62
|
-
return Err(
|
|
63
|
+
// synchronous I/O. The caller receives the identity refusal.
|
|
64
|
+
return Err(io::Error::new(
|
|
65
|
+
io::ErrorKind::PermissionDenied,
|
|
66
|
+
"cannot enter Store filesystem identity",
|
|
67
|
+
));
|
|
63
68
|
}
|
|
64
69
|
Ok(guard)
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
impl Drop for
|
|
73
|
+
impl Drop for FilesystemIdentity {
|
|
69
74
|
fn drop(&mut self) {
|
|
70
75
|
let Some((uid, gid)) = self.previous else {
|
|
71
76
|
return;
|
|
@@ -74,7 +79,7 @@ mod linux {
|
|
|
74
79
|
setfsgid(gid);
|
|
75
80
|
if filesystem_ids() != (uid, gid) {
|
|
76
81
|
// Continuing a reused Runtime thread under the wrong identity is
|
|
77
|
-
// unsafe. No
|
|
82
|
+
// unsafe. No I/O error can reach here: this is a failed kernel
|
|
78
83
|
// restoration of credentials that this thread previously held.
|
|
79
84
|
std::process::abort();
|
|
80
85
|
}
|
|
@@ -84,6 +89,7 @@ mod linux {
|
|
|
84
89
|
#[cfg(test)]
|
|
85
90
|
mod tests {
|
|
86
91
|
use super::*;
|
|
92
|
+
use crate::store::Store;
|
|
87
93
|
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
88
94
|
use std::sync::{Arc, Barrier};
|
|
89
95
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
@@ -117,7 +123,7 @@ mod linux {
|
|
|
117
123
|
other_observed.wait();
|
|
118
124
|
});
|
|
119
125
|
{
|
|
120
|
-
let _guard =
|
|
126
|
+
let _guard = FilesystemIdentity::for_store_root(store.root()).unwrap();
|
|
121
127
|
assert_eq!(filesystem_ids(), desired);
|
|
122
128
|
assert!(geteuid().is_root());
|
|
123
129
|
entered.wait();
|
|
@@ -129,14 +135,14 @@ mod linux {
|
|
|
129
135
|
}
|
|
130
136
|
assert_eq!(filesystem_ids(), original);
|
|
131
137
|
});
|
|
132
|
-
let failure: Result<()
|
|
133
|
-
let _guard =
|
|
134
|
-
Err(
|
|
138
|
+
let failure: io::Result<()> = (|| {
|
|
139
|
+
let _guard = FilesystemIdentity::for_store_root(store.root())?;
|
|
140
|
+
Err(io::Error::other("owned fixture failure"))
|
|
135
141
|
})();
|
|
136
142
|
assert!(failure.is_err());
|
|
137
143
|
assert_eq!(filesystem_ids(), original);
|
|
138
144
|
let panic = catch_unwind(AssertUnwindSafe(|| {
|
|
139
|
-
let _guard =
|
|
145
|
+
let _guard = FilesystemIdentity::for_store_root(store.root()).unwrap();
|
|
140
146
|
panic!("cache operation unwind");
|
|
141
147
|
}));
|
|
142
148
|
assert!(panic.is_err());
|
|
@@ -148,14 +154,14 @@ mod linux {
|
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
#[cfg(target_os = "linux")]
|
|
151
|
-
pub(
|
|
157
|
+
pub(crate) use linux::FilesystemIdentity;
|
|
152
158
|
|
|
153
159
|
#[cfg(not(target_os = "linux"))]
|
|
154
|
-
pub(
|
|
160
|
+
pub(crate) struct FilesystemIdentity;
|
|
155
161
|
|
|
156
162
|
#[cfg(not(target_os = "linux"))]
|
|
157
|
-
impl
|
|
158
|
-
pub(
|
|
163
|
+
impl FilesystemIdentity {
|
|
164
|
+
pub(crate) fn for_store_root(_: &Path) -> io::Result<Self> {
|
|
159
165
|
Ok(Self)
|
|
160
166
|
}
|
|
161
167
|
}
|
|
@@ -299,9 +299,9 @@ impl Xform {
|
|
|
299
299
|
Ok(())
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
/// The inverse read-back
|
|
303
|
-
///
|
|
304
|
-
///
|
|
302
|
+
/// The inverse read-back proves that the transform loses or duplicates no bytes.
|
|
303
|
+
/// It uses the transform's own indexing, so it cannot establish the producer's
|
|
304
|
+
/// QKV layout: that needs an independent projection-output regression.
|
|
305
305
|
pub fn reassembles(&self, src: &[u8]) -> Result<bool> {
|
|
306
306
|
match self {
|
|
307
307
|
Xform::Transpose {
|
|
@@ -105,10 +105,10 @@ pub const CONVERTERS: [Converter; 8] = [
|
|
|
105
105
|
reference: None,
|
|
106
106
|
},
|
|
107
107
|
Converter {
|
|
108
|
-
name: "h3.lora/
|
|
108
|
+
name: "h3.lora/2",
|
|
109
109
|
dialect: "safetensors.h3_lora",
|
|
110
110
|
class: Class::Permutation,
|
|
111
|
-
doc: "
|
|
111
|
+
doc: "Trainer H3 LoRA A/B and Kohya down/up factors: split contiguous Q/K/V B thirds, swap SwiGLU B halves, preserve A and alpha without arithmetic. Original base checkpoint head interleaving belongs to h3.native/2, not this adapter state-dict dialect.",
|
|
112
112
|
validated_on_real_bytes: false,
|
|
113
113
|
normalization: None,
|
|
114
114
|
lane: None,
|
|
@@ -1057,7 +1057,7 @@ fn plan_carrier(
|
|
|
1057
1057
|
cands: &Candidates<'_>,
|
|
1058
1058
|
p: &mut Plan,
|
|
1059
1059
|
) -> Result<()> {
|
|
1060
|
-
if conv.name == "h3.lora/
|
|
1060
|
+
if conv.name == "h3.lora/2" {
|
|
1061
1061
|
if allowed != ["plain/1"] {
|
|
1062
1062
|
return refuse(
|
|
1063
1063
|
Code::UNKNOWN_ENCODING,
|
|
@@ -1913,14 +1913,12 @@ pub fn h3_geometry(
|
|
|
1913
1913
|
/// The frozen golden cases for one converter: geometries whose reassembly is checked live,
|
|
1914
1914
|
/// on real byte patterns, before the converter is allowed to touch an artifact.
|
|
1915
1915
|
///
|
|
1916
|
-
///
|
|
1917
|
-
///
|
|
1918
|
-
///
|
|
1919
|
-
/// name/dtype/shape check and FAILS this, which is the whole point — the quarry pinned that
|
|
1920
|
-
/// error at ~90% numerically while nothing raised.
|
|
1916
|
+
/// `Xform::reassembles` checks that each declared transform preserves the source bytes.
|
|
1917
|
+
/// Both a contiguous split and a head-interleaved split can round-trip their own indexing;
|
|
1918
|
+
/// the producer's layout needs an independent projected-output test (see `h3_lora`).
|
|
1921
1919
|
pub fn golden_cases(c: &Converter) -> Vec<(&'static str, Xform)> {
|
|
1922
1920
|
match c.name {
|
|
1923
|
-
"h3.lora/
|
|
1921
|
+
"h3.lora/2" => vec![
|
|
1924
1922
|
(
|
|
1925
1923
|
"H3 LoRA SwiGLU B rows",
|
|
1926
1924
|
Xform::SwapHalves {
|
|
@@ -1928,12 +1926,12 @@ pub fn golden_cases(c: &Converter) -> Vec<(&'static str, Xform)> {
|
|
|
1928
1926
|
},
|
|
1929
1927
|
),
|
|
1930
1928
|
(
|
|
1931
|
-
"H3 LoRA QKV B
|
|
1929
|
+
"H3 trainer LoRA contiguous QKV B thirds",
|
|
1932
1930
|
Xform::QkvSplit {
|
|
1933
|
-
groups:
|
|
1931
|
+
groups: 1,
|
|
1934
1932
|
shares: [1, 1, 1],
|
|
1935
1933
|
take: 0,
|
|
1936
|
-
unit_bytes: H3_HEAD_DIM * 16 * 2,
|
|
1934
|
+
unit_bytes: H3_HEADS * H3_HEAD_DIM * 16 * 2,
|
|
1937
1935
|
},
|
|
1938
1936
|
),
|
|
1939
1937
|
],
|
|
@@ -201,6 +201,9 @@ pub struct FingerprintRegistry {
|
|
|
201
201
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
202
202
|
pub struct SourceProfile {
|
|
203
203
|
pub name: String,
|
|
204
|
+
/// Whether headers alone may choose this format. False requires the caller
|
|
205
|
+
/// to declare the reviewed format; shapes cannot prove semantic row layout.
|
|
206
|
+
pub auto_select: bool,
|
|
204
207
|
pub components: Vec<SourceProfileComponent>,
|
|
205
208
|
}
|
|
206
209
|
|
|
@@ -612,7 +615,7 @@ impl FingerprintRegistry {
|
|
|
612
615
|
self.source_profiles
|
|
613
616
|
.iter()
|
|
614
617
|
.map(|profile| {
|
|
615
|
-
|
|
618
|
+
let mut fields = vec![
|
|
616
619
|
(
|
|
617
620
|
"components",
|
|
618
621
|
Value::arr(
|
|
@@ -677,7 +680,11 @@ impl FingerprintRegistry {
|
|
|
677
680
|
),
|
|
678
681
|
),
|
|
679
682
|
("name", Value::str(profile.name.clone())),
|
|
680
|
-
]
|
|
683
|
+
];
|
|
684
|
+
if !profile.auto_select {
|
|
685
|
+
fields.push(("auto_select", Value::Bool(false)));
|
|
686
|
+
}
|
|
687
|
+
Value::obj(fields)
|
|
681
688
|
})
|
|
682
689
|
.collect(),
|
|
683
690
|
),
|
|
@@ -692,6 +699,11 @@ fn parse_source_profiles(value: &Value) -> Result<Vec<SourceProfile>> {
|
|
|
692
699
|
for value in as_arr("FingerprintRegistry", "source_profiles", value)? {
|
|
693
700
|
let mut fields = Fields::new("SourceProfile", value)?;
|
|
694
701
|
let name = fields.req_str("name")?.to_string();
|
|
702
|
+
let auto_select = match fields.opt("auto_select") {
|
|
703
|
+
None => true,
|
|
704
|
+
Some(Value::Bool(value)) => *value,
|
|
705
|
+
Some(_) => return refuse(Code::WRONG_TYPE, "SourceProfile.auto_select must be bool"),
|
|
706
|
+
};
|
|
695
707
|
ascii_name("source profile", &name, limits::MAX_NAME_BYTES)?;
|
|
696
708
|
let mut components = Vec::new();
|
|
697
709
|
for value in as_arr("SourceProfile", "components", fields.req("components")?)? {
|
|
@@ -838,7 +850,11 @@ fn parse_source_profiles(value: &Value) -> Result<Vec<SourceProfile>> {
|
|
|
838
850
|
format!("source profile {name:?} repeats a component"),
|
|
839
851
|
);
|
|
840
852
|
}
|
|
841
|
-
profiles.push(SourceProfile {
|
|
853
|
+
profiles.push(SourceProfile {
|
|
854
|
+
name,
|
|
855
|
+
auto_select,
|
|
856
|
+
components,
|
|
857
|
+
});
|
|
842
858
|
}
|
|
843
859
|
let mut names: Vec<&str> = profiles
|
|
844
860
|
.iter()
|
|
@@ -1044,10 +1060,12 @@ mod tests {
|
|
|
1044
1060
|
let mut registry = registry();
|
|
1045
1061
|
registry.source_profiles = vec![
|
|
1046
1062
|
SourceProfile {
|
|
1063
|
+
auto_select: true,
|
|
1047
1064
|
name: "profile/a".into(),
|
|
1048
1065
|
components: vec![source_component(Some("a/model.index.json"), None)],
|
|
1049
1066
|
},
|
|
1050
1067
|
SourceProfile {
|
|
1068
|
+
auto_select: true,
|
|
1051
1069
|
name: "profile/b".into(),
|
|
1052
1070
|
components: vec![
|
|
1053
1071
|
source_component(Some("b/model.safetensors"), None),
|
|
@@ -1070,6 +1088,7 @@ mod tests {
|
|
|
1070
1088
|
fn exact_source_members_refuses_missing_or_non_exact_profiles() {
|
|
1071
1089
|
let mut registry = registry();
|
|
1072
1090
|
registry.source_profiles = vec![SourceProfile {
|
|
1091
|
+
auto_select: true,
|
|
1073
1092
|
name: "profile/prefix".into(),
|
|
1074
1093
|
components: vec![source_component(None, Some("weights/"))],
|
|
1075
1094
|
}];
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
//! Reviewed
|
|
1
|
+
//! Reviewed trainer H3 LoRA dialects -> canonical projection factors.
|
|
2
2
|
//!
|
|
3
3
|
//! These adapters omit base Q/K norm tensors, so this dialect declares the exact
|
|
4
4
|
//! H3 geometry (56 heads of 128). Every target shape must agree before a plan is
|
|
5
|
-
//! emitted.
|
|
6
|
-
//!
|
|
5
|
+
//! emitted. Trainer QKV factors use contiguous Q/K/V thirds: the original base
|
|
6
|
+
//! checkpoint's head interleave is reordered before training, not exported again
|
|
7
|
+
//! in adapter state dictionaries. This is structural conversion, not permission
|
|
8
|
+
//! to apply a FL2VA-trained adapter to REF2VA. Runtime owns that binding and never
|
|
9
|
+
//! parses foreign keys.
|
|
7
10
|
|
|
8
11
|
use std::collections::BTreeMap;
|
|
9
12
|
|
|
@@ -229,10 +232,14 @@ pub(super) fn plan(
|
|
|
229
232
|
let transform = if leaf == "attn.qkv_proj" {
|
|
230
233
|
shape[0] /= 3;
|
|
231
234
|
Some(Xform::QkvSplit {
|
|
232
|
-
groups:
|
|
235
|
+
groups: 1,
|
|
233
236
|
shares: [1, 1, 1],
|
|
234
237
|
take: take as u8,
|
|
235
|
-
unit_bytes: checked_bytes(
|
|
238
|
+
unit_bytes: checked_bytes(
|
|
239
|
+
&b.key,
|
|
240
|
+
&[H3_HEADS * H3_HEAD_DIM, a.shape[0]],
|
|
241
|
+
b.dtype,
|
|
242
|
+
)?,
|
|
236
243
|
})
|
|
237
244
|
} else if leaf == "mlp.fc1" {
|
|
238
245
|
Some(Xform::SwapHalves {
|
|
@@ -340,9 +340,6 @@ pub fn select_members(
|
|
|
340
340
|
.collect()
|
|
341
341
|
};
|
|
342
342
|
let (own, all) = (carriers(false), carriers(true));
|
|
343
|
-
if profiles.is_empty() && all.len() == 1 {
|
|
344
|
-
return Ok(all);
|
|
345
|
-
}
|
|
346
343
|
let heads = read_heads(&resolution.select(&all)?)?;
|
|
347
344
|
let plan_over = |carriers: &[String]| -> Result<(Vec<String>, bool)> {
|
|
348
345
|
let members = resolution.select(carriers)?.members;
|