tensorfs 0.3.91__tar.gz → 0.3.92__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.91 → tensorfs-0.3.92}/Cargo.lock +3 -3
- {tensorfs-0.3.91 → tensorfs-0.3.92}/Cargo.toml +1 -1
- {tensorfs-0.3.91 → tensorfs-0.3.92}/PKG-INFO +1 -1
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/API.md +32 -3
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/cursor.rs +1 -1
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/device.rs +73 -13
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/faults.rs +19 -3
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/io.rs +162 -11
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/plane.rs +63 -23
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/common/mod.rs +28 -1
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/gpu.rs +148 -9
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/plane.rs +3 -1
- {tensorfs-0.3.91 → tensorfs-0.3.92}/pyproject.toml +1 -1
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/plane.pyi +5 -1
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ensure.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/catalog/lock.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/catalog.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/descriptors.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/disk.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure/admission.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure/flight.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/filesystem_identity.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/narrow.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/anima.routes.json +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/anima.spec.json +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/keyed_roots.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/lib.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/decisions.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/download.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/http.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/pull.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/sources.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/tests/tail.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/tests.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/unsynced.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/as_is.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/catalog_owner_probe.c +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/civitai_anima.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/ensure.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fetch_faults.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/keyed_roots.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_download.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/Cargo.toml +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/g1_vmm.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/g2_overlap.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/gpu_bench/mod.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/cuda.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/host.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/layout.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/lib.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/census.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/host_tier.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/ensure.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/ingest.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/store.rs +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_ext.pyi +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/plane.py +0 -0
- {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/py.typed +0 -0
|
@@ -449,7 +449,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
449
449
|
|
|
450
450
|
[[package]]
|
|
451
451
|
name = "tensorfs-core"
|
|
452
|
-
version = "0.3.
|
|
452
|
+
version = "0.3.92"
|
|
453
453
|
dependencies = [
|
|
454
454
|
"fs2",
|
|
455
455
|
"libc",
|
|
@@ -464,7 +464,7 @@ dependencies = [
|
|
|
464
464
|
|
|
465
465
|
[[package]]
|
|
466
466
|
name = "tensorfs-plane"
|
|
467
|
-
version = "0.3.
|
|
467
|
+
version = "0.3.92"
|
|
468
468
|
dependencies = [
|
|
469
469
|
"libc",
|
|
470
470
|
"libloading",
|
|
@@ -474,7 +474,7 @@ dependencies = [
|
|
|
474
474
|
|
|
475
475
|
[[package]]
|
|
476
476
|
name = "tensorfs-py"
|
|
477
|
-
version = "0.3.
|
|
477
|
+
version = "0.3.92"
|
|
478
478
|
dependencies = [
|
|
479
479
|
"pyo3",
|
|
480
480
|
"tensorfs-core",
|
|
@@ -99,7 +99,18 @@ ts = p.replicate(ws, devices, regions=None, priority=0, pin=False) # [Ticket]
|
|
|
99
99
|
wait meanwhile; unregister on eviction ~0.03 ms/MiB): want the pinned tier at admission or
|
|
100
100
|
between stages, never while this process computes. A host-only plane registers nothing.
|
|
101
101
|
- device: map the regions (slab-sized handles), then copy from the pinned tier if it holds the
|
|
102
|
-
region,
|
|
102
|
+
region; else, for a `want` of bytes the page cache holds, straight from read-only mappings
|
|
103
|
+
of their objects (no host copy, no wait for a pinned fill, which then runs beside it;
|
|
104
|
+
counted in `host_copy_bytes` and, as its part, `mapped_copy_bytes`); else after a pinned
|
|
105
|
+
fill that is running (one disk read
|
|
106
|
+
serves both); else disk → staging buffers → device. A demand miss (`acquire`) and a
|
|
107
|
+
streaming ring never map. An object that cannot be mapped (not ext4, xfs, btrfs, tmpfs or
|
|
108
|
+
overlayfs; not at its declared length; pages gone from the cache) sends its region through
|
|
109
|
+
staging (`map_refused`). A mapping's fault is a signal, not an error: a disk read error on a
|
|
110
|
+
page evicted between the check and the copy kills the process with SIGBUS. The cache query
|
|
111
|
+
runs on the caller before the fill is queued and opens each object of the wanted regions
|
|
112
|
+
(a record lookup, a rehash when the record is missing): a device `want` can block on a
|
|
113
|
+
first open.
|
|
103
114
|
- `replicate`: one pinned fill (if the pinned budget allows), then one copy per device.
|
|
104
115
|
- A `want` of regions already resident only updates their priority/pin: no bytes move and
|
|
105
116
|
nothing is evicted. To re-prioritise without any chance of a fill, use:
|
|
@@ -195,13 +206,19 @@ are equal-sized).
|
|
|
195
206
|
p.stats() -> {
|
|
196
207
|
"host": {"budget", "used", "ready_bytes", "registered_bytes", "memfd_bytes",
|
|
197
208
|
"cached_bytes", "direct_bytes", "buffered_bytes", "inline_bytes", "direct_refused",
|
|
198
|
-
"disk_read_bytes",
|
|
209
|
+
"map_refused", "disk_read_bytes",
|
|
199
210
|
"fills", "fill_bytes", "fill_ns", "evictions", "evicted_bytes", "register_failed",
|
|
200
211
|
"trims"},
|
|
201
212
|
"devices": [{"ordinal", "uuid", "granularity", "exportable", "budget", "committed", "mapped",
|
|
202
213
|
"idle", "leased_bytes", "held_bytes", "over_budget_unreleasable", "reserved_va",
|
|
203
214
|
"copies", "host_copy_bytes", "host_copy_ns", "disk_copy_bytes", "disk_copy_ns",
|
|
204
|
-
"
|
|
215
|
+
"mapped_copy_bytes", "mapped_copy_ns", "misses", "evictions", "evicted_bytes",
|
|
216
|
+
"trims"}],
|
|
217
|
+
# host_copy_* counts every copy from host memory with no staging: the pinned tier's and the
|
|
218
|
+
# mappings' (mapped_copy_* is that part of it; subtract it for the tier alone, whose rate a
|
|
219
|
+
# reader of the host pair otherwise sees blended). host + disk copies are every byte that
|
|
220
|
+
# landed on the device. A pinned fill served by the page cache counts in fill_bytes and
|
|
221
|
+
# cached_bytes, never in disk_read_bytes.
|
|
205
222
|
"sets": [{"id", "name", "nbytes", "regions", "host_ready_bytes", "host_memfd_bytes",
|
|
206
223
|
"evictions", "evicted_bytes", "devices": [{"ordinal", "ready_bytes"}]}],
|
|
207
224
|
"stages": [{"ws", "device", "tag", "acquires", "skipped", "misses", "late", "late_bytes",
|
|
@@ -280,6 +297,18 @@ an unclosed plane ends.
|
|
|
280
297
|
|
|
281
298
|
## Changelog
|
|
282
299
|
|
|
300
|
+
- v3.3 2026-10-02: `stats()["host"]["disk_read_bytes"]` exists. The plane has counted it
|
|
301
|
+
since v3.1 and this file listed it, but the Python facade never carried it: a caller read 0
|
|
302
|
+
whatever the disk did, so a "nothing read from disk" check on it could not fail.
|
|
303
|
+
|
|
304
|
+
- v3.3 2026-10-02 (warm loads bound by the plane's own host copy): a device `want` of bytes
|
|
305
|
+
the page cache holds copies straight from mappings of their objects, beside any pinned
|
|
306
|
+
fill. `host_copy_*` counts them with the pinned tier's copies (host + disk copies stay every
|
|
307
|
+
byte that landed on the device); `mapped_copy_*` is their part of it. The cache query now also answers where `cachestat` cannot:
|
|
308
|
+
overlayfs and a seccomp filter that refuses it. Pod-visible: there the pinned fill copies
|
|
309
|
+
freshly downloaded weights from the page cache where it re-read them by O_DIRECT, so the
|
|
310
|
+
tier and the cache copy coexist until the kernel reclaims the cache.
|
|
311
|
+
|
|
283
312
|
- v3.2 2026-10-02 (W3: fd and thread growth per weight set): a plane collected unclosed or
|
|
284
313
|
poisoned is torn down at its last handle (threads, device memory, pinned memfds) once its
|
|
285
314
|
devices are synchronized; before, its reader threads stayed and a poisoned plane kept its
|
|
@@ -166,7 +166,7 @@ fn submit(inner: &Arc<Inner>, c: &mut Core, id: u64, k: u64, off: u64, waits: Ve
|
|
|
166
166
|
c.timings.push_back(Timing {
|
|
167
167
|
dev: d,
|
|
168
168
|
ws: ws.0,
|
|
169
|
-
kind: TimingKind::Copy {
|
|
169
|
+
kind: TimingKind::Copy { from: cp.from, host_ns: cp.host_ns },
|
|
170
170
|
a: cp.start.clone(),
|
|
171
171
|
b: cp.ready.clone(),
|
|
172
172
|
bytes: cp.bytes,
|
|
@@ -11,7 +11,7 @@ use std::sync::{Arc, Condvar, Mutex};
|
|
|
11
11
|
use std::thread::JoinHandle;
|
|
12
12
|
|
|
13
13
|
use crate::cuda::{self, AccessDesc, AllocationProp, CUcontext, CUevent, CUstream, CtxGuard, Handle};
|
|
14
|
-
use crate::io::{Batch, Dst, IoTally, Mode, Readers, Source};
|
|
14
|
+
use crate::io::{Batch, Dst, IoTally, Mapping, Mode, Readers, Source};
|
|
15
15
|
use crate::layout::{Item, ItemSource, REGION_ALIGN};
|
|
16
16
|
use crate::{Error, Result};
|
|
17
17
|
|
|
@@ -368,14 +368,31 @@ pub enum CopySrc {
|
|
|
368
368
|
items: Vec<Item>,
|
|
369
369
|
base: u64,
|
|
370
370
|
},
|
|
371
|
+
/// Bytes the page cache holds, read through mappings of their objects: no host copy. An
|
|
372
|
+
/// object that cannot be mapped sends the whole copy through staging instead.
|
|
373
|
+
Mapped {
|
|
374
|
+
source: Arc<Source>,
|
|
375
|
+
items: Vec<Item>,
|
|
376
|
+
base: u64,
|
|
377
|
+
},
|
|
371
378
|
}
|
|
372
379
|
unsafe impl Send for CopySrc {}
|
|
373
380
|
|
|
381
|
+
/// Where a copy's bytes came from, for its counters.
|
|
382
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
383
|
+
pub enum CopyFrom {
|
|
384
|
+
Pinned,
|
|
385
|
+
Disk,
|
|
386
|
+
Mapped,
|
|
387
|
+
}
|
|
388
|
+
|
|
374
389
|
pub struct Copied {
|
|
375
390
|
pub ready: EvRef,
|
|
376
391
|
pub start: EvRef,
|
|
377
392
|
pub bytes: u64,
|
|
378
|
-
pub
|
|
393
|
+
pub from: CopyFrom,
|
|
394
|
+
/// Host time before `start` that belongs to the copy: mapping and faulting its source in.
|
|
395
|
+
pub host_ns: u64,
|
|
379
396
|
}
|
|
380
397
|
|
|
381
398
|
pub struct CopyJob {
|
|
@@ -474,8 +491,16 @@ fn mover_main(m: MoverCtx, rx: Receiver<CopyJob>, ok: Sender<Result<()>>) {
|
|
|
474
491
|
};
|
|
475
492
|
while let Ok(job) = rx.recv() {
|
|
476
493
|
let done = job.done;
|
|
477
|
-
let r = run_copy(&m, stream, &mut bounces, job.waits, job.dst, job.len, job.src);
|
|
494
|
+
let (r, maps) = run_copy(&m, stream, &mut bounces, job.waits, job.dst, job.len, job.src);
|
|
495
|
+
// The driver stages a pageable source after the call returns, so a mapped copy's
|
|
496
|
+
// mappings outlive its event. The region is reported first: its consumer queues behind
|
|
497
|
+
// the event with no host wait.
|
|
498
|
+
let mapped = r.as_ref().ok().filter(|_| !maps.is_empty()).map(|cp| cp.ready.clone());
|
|
478
499
|
done(r);
|
|
500
|
+
match mapped.map(|ready| ready.sync()) {
|
|
501
|
+
Some(Err(_)) => std::mem::forget(maps), // the copy's end is unknown: it may still read them
|
|
502
|
+
_ => drop(maps),
|
|
503
|
+
}
|
|
479
504
|
}
|
|
480
505
|
if let Ok(d) = cuda::driver() {
|
|
481
506
|
// SAFETY: our stream and buffers; the stream is drained first.
|
|
@@ -498,6 +523,9 @@ struct Waiter {
|
|
|
498
523
|
/// no reader still writes a bounce: a failed copy is drained before it is reported, so no
|
|
499
524
|
/// caller unmaps, unregisters or reuses a range a copy still touches. A drain that itself fails
|
|
500
525
|
/// is ambiguous device state: `Poisoned`.
|
|
526
|
+
///
|
|
527
|
+
/// The mappings a queued mapped copy reads come back with it, for the caller to hold past its
|
|
528
|
+
/// event. A failed one's are unmapped here, after the drain, and never if the drain failed.
|
|
501
529
|
fn run_copy(
|
|
502
530
|
m: &MoverCtx,
|
|
503
531
|
stream: CUstream,
|
|
@@ -506,20 +534,36 @@ fn run_copy(
|
|
|
506
534
|
dst: u64,
|
|
507
535
|
len: u64,
|
|
508
536
|
src: CopySrc,
|
|
509
|
-
) -> Result<Copied> {
|
|
510
|
-
let
|
|
511
|
-
let
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
537
|
+
) -> (Result<Copied>, Vec<Mapping>) {
|
|
538
|
+
let began = std::time::Instant::now();
|
|
539
|
+
let (src, maps) = match src {
|
|
540
|
+
CopySrc::Mapped { source, items, base } => match source.map(&items, &m.tally) {
|
|
541
|
+
Ok(maps) => (CopySrc::Mapped { source, items, base }, maps),
|
|
542
|
+
Err(_) => {
|
|
543
|
+
// Nothing is queued yet: the same bytes go through the staging buffers.
|
|
544
|
+
m.tally.map_refused.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
545
|
+
(CopySrc::Disk { source, items, base }, Vec::new())
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
src => (src, Vec::new()),
|
|
549
|
+
};
|
|
550
|
+
let host_ns = if maps.is_empty() { 0 } else { began.elapsed().as_nanos() as u64 };
|
|
551
|
+
let r = enqueue_copy(m, stream, bounces, waits, dst, len, src, &maps).map(|cp| Copied { host_ns, ..cp });
|
|
552
|
+
let Err(e) = r else { return (r, maps) };
|
|
553
|
+
let drained = crate::faults::hit("drain").and_then(|_| cuda::driver()).and_then(|d| {
|
|
554
|
+
// SAFETY: our own stream.
|
|
515
555
|
cuda::check("cuStreamSynchronize", unsafe { (d.stream_synchronize)(stream) })
|
|
516
556
|
});
|
|
517
557
|
match drained {
|
|
518
|
-
Ok(()) => Err(e),
|
|
519
|
-
Err(drain) =>
|
|
558
|
+
Ok(()) => (Err(e), Vec::new()),
|
|
559
|
+
Err(drain) => {
|
|
560
|
+
std::mem::forget(maps); // a copy may still read them
|
|
561
|
+
(Err(Error::Poisoned(format!("a failed copy ({e}) could not be drained: {drain}"))), Vec::new())
|
|
562
|
+
}
|
|
520
563
|
}
|
|
521
564
|
}
|
|
522
565
|
|
|
566
|
+
#[allow(clippy::too_many_arguments)]
|
|
523
567
|
fn enqueue_copy(
|
|
524
568
|
m: &MoverCtx,
|
|
525
569
|
stream: CUstream,
|
|
@@ -528,6 +572,7 @@ fn enqueue_copy(
|
|
|
528
572
|
dst: u64,
|
|
529
573
|
len: u64,
|
|
530
574
|
src: CopySrc,
|
|
575
|
+
maps: &[Mapping],
|
|
531
576
|
) -> Result<Copied> {
|
|
532
577
|
let d = cuda::driver()?;
|
|
533
578
|
let start = m.events.get(true)?;
|
|
@@ -550,7 +595,22 @@ fn enqueue_copy(
|
|
|
550
595
|
})?;
|
|
551
596
|
crate::faults::hit("host-copy-recorded")?;
|
|
552
597
|
ready.record(stream)?;
|
|
553
|
-
Ok(Copied { ready, start, bytes: len,
|
|
598
|
+
Ok(Copied { ready, start, bytes: len, from: CopyFrom::Pinned, host_ns: 0 })
|
|
599
|
+
}
|
|
600
|
+
CopySrc::Mapped { items, base, .. } => {
|
|
601
|
+
wait_all(&waits)?;
|
|
602
|
+
crate::faults::gate("copy", stream)?;
|
|
603
|
+
start.record(stream)?;
|
|
604
|
+
for (it, map) in items.iter().zip(maps).filter(|(it, _)| it.len > 0) {
|
|
605
|
+
crate::faults::hit("mapped-copy")?;
|
|
606
|
+
// SAFETY: the mapping covers `it.len` bytes and the mover keeps it past this
|
|
607
|
+
// copy's event.
|
|
608
|
+
cuda::check("cuMemcpyHtoDAsync", unsafe {
|
|
609
|
+
(d.memcpy_htod_async)(dst + (it.offset - base), map.ptr() as *const libc::c_void, it.len as usize, stream)
|
|
610
|
+
})?;
|
|
611
|
+
}
|
|
612
|
+
ready.record(stream)?;
|
|
613
|
+
Ok(Copied { ready, start, bytes: len, from: CopyFrom::Mapped, host_ns: 0 })
|
|
554
614
|
}
|
|
555
615
|
CopySrc::Disk { source, items, base } => {
|
|
556
616
|
let cap = m.staging.1;
|
|
@@ -637,7 +697,7 @@ fn enqueue_copy(
|
|
|
637
697
|
start.record(stream)?;
|
|
638
698
|
}
|
|
639
699
|
ready.record(stream)?;
|
|
640
|
-
Ok(Copied { ready, start, bytes: len,
|
|
700
|
+
Ok(Copied { ready, start, bytes: len, from: CopyFrom::Disk, host_ns: 0 })
|
|
641
701
|
}
|
|
642
702
|
}
|
|
643
703
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//! Fault points on the copy
|
|
2
|
-
//! feature every point is a no-op.
|
|
1
|
+
//! Fault points on the copy and fill paths, armed only by tests (cargo feature `faults`).
|
|
2
|
+
//! Without the feature every point is a no-op.
|
|
3
3
|
|
|
4
4
|
#[cfg(feature = "faults")]
|
|
5
5
|
mod imp {
|
|
@@ -74,12 +74,24 @@ mod imp {
|
|
|
74
74
|
// SAFETY: our stream; the host function only waits on the flag.
|
|
75
75
|
cuda::check("cuLaunchHostFunc", unsafe { (d.launch_host_func)(stream, hold, p) })
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
/// Holds the calling thread at `point` until the gate armed there opens.
|
|
79
|
+
pub(crate) fn wait(point: &'static str) {
|
|
80
|
+
let f = {
|
|
81
|
+
let mut g = GATES.lock().unwrap();
|
|
82
|
+
let Some(i) = g.iter().position(|(p, _)| *p == point) else { return };
|
|
83
|
+
g.remove(i).1
|
|
84
|
+
};
|
|
85
|
+
while !f.load(Ordering::Acquire) {
|
|
86
|
+
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
#[cfg(feature = "faults")]
|
|
80
92
|
pub use imp::{arm, arm_gate, Gate};
|
|
81
93
|
#[cfg(feature = "faults")]
|
|
82
|
-
pub(crate) use imp::{gate, hit};
|
|
94
|
+
pub(crate) use imp::{gate, hit, wait};
|
|
83
95
|
|
|
84
96
|
#[cfg(not(feature = "faults"))]
|
|
85
97
|
#[inline(always)]
|
|
@@ -92,3 +104,7 @@ pub(crate) fn hit(_point: &'static str) -> crate::Result<()> {
|
|
|
92
104
|
pub(crate) fn gate(_point: &'static str, _stream: crate::cuda::CUstream) -> crate::Result<()> {
|
|
93
105
|
Ok(())
|
|
94
106
|
}
|
|
107
|
+
|
|
108
|
+
#[cfg(not(feature = "faults"))]
|
|
109
|
+
#[inline(always)]
|
|
110
|
+
pub(crate) fn wait(_point: &'static str) {}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//! descriptors the plane opens through the store's own verification (`open_verified`). A
|
|
3
3
|
//! pinned-tier fill copies what the page cache already holds and reads the rest by O_DIRECT
|
|
4
4
|
//! into the destination (no second host copy), else through `tensorfs_core::read::read_into`.
|
|
5
|
-
//!
|
|
5
|
+
//! A device copy of bytes the page cache holds reads them through a read-only mapping, with no
|
|
6
|
+
//! host copy at all. Each mode's bytes are counted.
|
|
6
7
|
|
|
7
8
|
use std::collections::HashMap;
|
|
8
9
|
use std::ffi::CString;
|
|
@@ -29,6 +30,8 @@ pub struct IoTally {
|
|
|
29
30
|
pub inline_bytes: AtomicU64,
|
|
30
31
|
/// Objects whose filesystem refused O_DIRECT: their bytes went buffered.
|
|
31
32
|
pub direct_refused: AtomicU64,
|
|
33
|
+
/// Device copies whose objects could not be mapped: they went through staging.
|
|
34
|
+
pub map_refused: AtomicU64,
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/// How a fill wants its bytes: `Cache` for the pinned tier (O_DIRECT for what the page cache
|
|
@@ -42,6 +45,94 @@ pub enum Mode {
|
|
|
42
45
|
struct Fds {
|
|
43
46
|
buffered: File,
|
|
44
47
|
direct: Option<File>,
|
|
48
|
+
fs: Fs,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// What the plane may do with an object by its filesystem. A fault on a mapping is a signal,
|
|
52
|
+
/// not an error, so only local filesystems are mapped.
|
|
53
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
54
|
+
enum Fs {
|
|
55
|
+
Local,
|
|
56
|
+
/// Local too, but its files report no page cache of their own (a container's root).
|
|
57
|
+
Overlay,
|
|
58
|
+
Other,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl Fs {
|
|
62
|
+
fn of(f: &File) -> Fs {
|
|
63
|
+
// SAFETY: out-param of the right type on our own descriptor.
|
|
64
|
+
let mut fs: libc::statfs = unsafe { std::mem::zeroed() };
|
|
65
|
+
if unsafe { libc::fstatfs(f.as_raw_fd(), &mut fs) } != 0 {
|
|
66
|
+
return Fs::Other;
|
|
67
|
+
}
|
|
68
|
+
#[allow(clippy::unnecessary_cast)] // f_type is not an i64 on every target
|
|
69
|
+
match fs.f_type as i64 {
|
|
70
|
+
0x794c7630 => Fs::Overlay,
|
|
71
|
+
// ext4, xfs, btrfs, tmpfs
|
|
72
|
+
0xEF53 | 0x58465342 | 0x9123683E | 0x01021994 => Fs::Local,
|
|
73
|
+
_ => Fs::Other,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// One item's bytes for a device copy, with no host copy: a populated read-only mapping of its
|
|
79
|
+
/// object range (unmapped on drop), or the inline bytes themselves.
|
|
80
|
+
pub enum Mapping {
|
|
81
|
+
Inline(Arc<[u8]>),
|
|
82
|
+
File { base: *mut u8, span: usize, at: usize },
|
|
83
|
+
}
|
|
84
|
+
// SAFETY: a read-only mapping (or shared immutable bytes) owned by whoever holds the value.
|
|
85
|
+
unsafe impl Send for Mapping {}
|
|
86
|
+
|
|
87
|
+
impl Mapping {
|
|
88
|
+
pub fn ptr(&self) -> *const u8 {
|
|
89
|
+
match self {
|
|
90
|
+
Mapping::Inline(b) => b.as_ptr(),
|
|
91
|
+
// SAFETY: `at` lies inside the mapping.
|
|
92
|
+
Mapping::File { base, at, .. } => unsafe { base.add(*at) },
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// `[off, off+len)` of `f`, which the page cache must hold: a cold range is refused, never
|
|
97
|
+
/// read from disk through the mapping. Populated and checked again, so the copy that
|
|
98
|
+
/// follows is not the range's first reader.
|
|
99
|
+
fn file(f: &File, off: u64, len: u64) -> Result<Mapping> {
|
|
100
|
+
if !resident(f, off, len) {
|
|
101
|
+
return Err(Error::Io("the page cache let go of the range".into()));
|
|
102
|
+
}
|
|
103
|
+
let at = (off % 4096) as usize;
|
|
104
|
+
let span = at + len as usize;
|
|
105
|
+
// SAFETY: a fresh read-only shared mapping of our descriptor.
|
|
106
|
+
let p = unsafe {
|
|
107
|
+
libc::mmap(
|
|
108
|
+
std::ptr::null_mut(),
|
|
109
|
+
span,
|
|
110
|
+
libc::PROT_READ,
|
|
111
|
+
libc::MAP_SHARED | libc::MAP_POPULATE,
|
|
112
|
+
f.as_raw_fd(),
|
|
113
|
+
(off - at as u64) as libc::off_t,
|
|
114
|
+
)
|
|
115
|
+
};
|
|
116
|
+
if p == libc::MAP_FAILED {
|
|
117
|
+
return Err(Error::Io(format!("mmap: {}", std::io::Error::last_os_error())));
|
|
118
|
+
}
|
|
119
|
+
let m = Mapping::File { base: p as *mut u8, span, at };
|
|
120
|
+
let mut pages = vec![0u8; span.div_ceil(4096)];
|
|
121
|
+
// SAFETY: the vector holds one byte per page of the mapping.
|
|
122
|
+
if unsafe { libc::mincore(p, span, pages.as_mut_ptr()) } != 0 || pages.iter().any(|b| b & 1 == 0) {
|
|
123
|
+
return Err(Error::Io("the page cache let go of a mapped range".into()));
|
|
124
|
+
}
|
|
125
|
+
Ok(m)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
impl Drop for Mapping {
|
|
130
|
+
fn drop(&mut self) {
|
|
131
|
+
if let Mapping::File { base, span, .. } = self {
|
|
132
|
+
// SAFETY: our own mapping, no longer read by anyone (the holder's contract).
|
|
133
|
+
unsafe { libc::munmap(*base as *mut libc::c_void, *span) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
45
136
|
}
|
|
46
137
|
|
|
47
138
|
pub struct Source {
|
|
@@ -54,9 +145,11 @@ pub struct Source {
|
|
|
54
145
|
}
|
|
55
146
|
|
|
56
147
|
/// Whether the page cache holds every page of `[off, off+len)`, from `cachestat(2)`: a pure
|
|
57
|
-
/// query (unlike an `RWF_NOWAIT` read, which starts readahead on a miss).
|
|
58
|
-
/// kernel
|
|
59
|
-
|
|
148
|
+
/// query (unlike an `RWF_NOWAIT` read, which starts readahead on a miss). Where that cannot
|
|
149
|
+
/// answer, the pages are counted through a mapping: a kernel without it (< 6.5), a container
|
|
150
|
+
/// whose seccomp filter refuses it, and overlayfs (a container's root), whose files report no
|
|
151
|
+
/// pages of their own.
|
|
152
|
+
fn cached(f: &Fds, off: u64, len: u64) -> bool {
|
|
60
153
|
#[repr(C)]
|
|
61
154
|
struct Range {
|
|
62
155
|
off: u64,
|
|
@@ -75,12 +168,31 @@ fn cached(f: &File, off: u64, len: u64) -> Option<bool> {
|
|
|
75
168
|
let r = Range { off, len };
|
|
76
169
|
let mut st = Stat::default();
|
|
77
170
|
// SAFETY: the documented cachestat ABI; both structs outlive the call.
|
|
78
|
-
let rc = unsafe { libc::syscall(SYS_CACHESTAT, f.as_raw_fd(), &r, &mut st, 0) };
|
|
79
|
-
if rc != 0 {
|
|
80
|
-
return None;
|
|
81
|
-
}
|
|
171
|
+
let rc = unsafe { libc::syscall(SYS_CACHESTAT, f.buffered.as_raw_fd(), &r, &mut st, 0) };
|
|
82
172
|
let page = 4096u64;
|
|
83
|
-
|
|
173
|
+
if rc == 0 && st.nr_cache >= (off + len).div_ceil(page) - off / page {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
(rc != 0 || f.fs == Fs::Overlay) && resident(&f.buffered, off, len)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Every page of `[off, off+len)` is in core, by `mincore(2)` over a mapping (never faults).
|
|
180
|
+
fn resident(f: &File, off: u64, len: u64) -> bool {
|
|
181
|
+
let start = off - off % 4096;
|
|
182
|
+
let n = (off + len - start) as usize;
|
|
183
|
+
if n == 0 {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
// SAFETY: a read-only shared mapping of our descriptor, unmapped below.
|
|
187
|
+
let p = unsafe { libc::mmap(std::ptr::null_mut(), n, libc::PROT_READ, libc::MAP_SHARED, f.as_raw_fd(), start as libc::off_t) };
|
|
188
|
+
if p == libc::MAP_FAILED {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
let mut pages = vec![0u8; n.div_ceil(4096)];
|
|
192
|
+
// SAFETY: the vector holds one byte per page of the mapping.
|
|
193
|
+
let ok = unsafe { libc::mincore(p, n, pages.as_mut_ptr()) } == 0 && pages.iter().all(|b| b & 1 == 1);
|
|
194
|
+
unsafe { libc::munmap(p, n) };
|
|
195
|
+
ok
|
|
84
196
|
}
|
|
85
197
|
|
|
86
198
|
impl Source {
|
|
@@ -138,11 +250,50 @@ impl Source {
|
|
|
138
250
|
} else {
|
|
139
251
|
None
|
|
140
252
|
};
|
|
141
|
-
let
|
|
253
|
+
let fs = Fs::of(&buffered);
|
|
254
|
+
let f = Arc::new(Fds { buffered, direct, fs });
|
|
142
255
|
self.fds.lock().unwrap().insert(obj.sha256.clone(), f.clone());
|
|
143
256
|
Ok(f)
|
|
144
257
|
}
|
|
145
258
|
|
|
259
|
+
/// Whether the page cache holds every object byte of `items`. An object that will not open
|
|
260
|
+
/// counts as not held: its reader reports the error.
|
|
261
|
+
pub(crate) fn cached(&self, items: &[Item], tally: &IoTally) -> bool {
|
|
262
|
+
items.iter().all(|it| match &it.source {
|
|
263
|
+
ItemSource::Inline(_) => true,
|
|
264
|
+
ItemSource::Object(o) => {
|
|
265
|
+
self.fds(&o.obj, tally).is_ok_and(|f| cached(&f, o.off, it.len))
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// `items` mapped for a device copy. Refuses (the caller stages instead) an object on a
|
|
271
|
+
/// filesystem where a fault could be a network error, one not at its declared length, and
|
|
272
|
+
/// pages the cache no longer holds.
|
|
273
|
+
pub fn map(&self, items: &[Item], tally: &IoTally) -> Result<Vec<Mapping>> {
|
|
274
|
+
crate::faults::hit("map")?;
|
|
275
|
+
let guard = self.lease.read().unwrap();
|
|
276
|
+
let lease = guard.as_ref().ok_or(Error::Closed)?;
|
|
277
|
+
let one = |it: &Item| -> Result<Mapping> {
|
|
278
|
+
let o = match &it.source {
|
|
279
|
+
ItemSource::Inline(b) => return Ok(Mapping::Inline(b.clone())),
|
|
280
|
+
ItemSource::Object(_) if it.len == 0 => return Ok(Mapping::Inline(Arc::from([]))),
|
|
281
|
+
ItemSource::Object(o) => o,
|
|
282
|
+
};
|
|
283
|
+
lease.covers(o)?;
|
|
284
|
+
let fds = self.fds(&o.obj, tally)?;
|
|
285
|
+
if fds.fs == Fs::Other {
|
|
286
|
+
return Err(Error::Invalid(format!("{}: not on a local filesystem", o.obj.sha256)));
|
|
287
|
+
}
|
|
288
|
+
let size = fds.buffered.metadata().map_err(|e| Error::Io(format!("fstat {}: {e}", o.obj.sha256)))?.len();
|
|
289
|
+
if size != o.obj.length {
|
|
290
|
+
return Err(Error::Io(format!("{} is {size} bytes, declared {}", o.obj.sha256, o.obj.length)));
|
|
291
|
+
}
|
|
292
|
+
Mapping::file(&fds.buffered, o.off, it.len)
|
|
293
|
+
};
|
|
294
|
+
items.iter().map(one).collect()
|
|
295
|
+
}
|
|
296
|
+
|
|
146
297
|
/// Fill `item.len` bytes at `dst`. `Mode::Cache` reads a range the page cache fully
|
|
147
298
|
/// holds by copy and everything else by O_DIRECT when alignment allows, which may DMA up
|
|
148
299
|
/// to the next `PART_ALIGN` boundary past the item: the layout reserves that as padding.
|
|
@@ -169,7 +320,7 @@ impl Source {
|
|
|
169
320
|
let end = o.off + item.len;
|
|
170
321
|
let aligned = (dst as u64).wrapping_sub(o.off).is_multiple_of(a) && (end.is_multiple_of(a) || item.part_end);
|
|
171
322
|
let direct = match (mode, &fds.direct) {
|
|
172
|
-
(Mode::Cache, Some(f)) if aligned && !cached(&fds
|
|
323
|
+
(Mode::Cache, Some(f)) if aligned && !cached(&fds, o.off, item.len) => f,
|
|
173
324
|
_ => {
|
|
174
325
|
read::read_into(lease, o, buf)?;
|
|
175
326
|
let t = if mode == Mode::Buffered { &tally.buffered_bytes } else { &tally.cached_bytes };
|