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.
Files changed (204) hide show
  1. {tensorfs-0.3.91 → tensorfs-0.3.92}/Cargo.lock +3 -3
  2. {tensorfs-0.3.91 → tensorfs-0.3.92}/Cargo.toml +1 -1
  3. {tensorfs-0.3.91 → tensorfs-0.3.92}/PKG-INFO +1 -1
  4. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/API.md +32 -3
  5. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/cursor.rs +1 -1
  6. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/device.rs +73 -13
  7. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/faults.rs +19 -3
  8. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/io.rs +162 -11
  9. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/plane.rs +63 -23
  10. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/common/mod.rs +28 -1
  11. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/gpu.rs +148 -9
  12. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/plane.rs +3 -1
  13. {tensorfs-0.3.91 → tensorfs-0.3.92}/pyproject.toml +1 -1
  14. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/plane.pyi +5 -1
  15. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/Cargo.toml +0 -0
  16. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/build.rs +0 -0
  17. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  18. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  19. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  20. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  21. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/asset_update.rs +0 -0
  22. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/b64.rs +0 -0
  23. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  24. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  25. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ensure.rs +0 -0
  26. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  27. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  28. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  29. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  30. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  31. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  32. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  33. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  34. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/canon.rs +0 -0
  35. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/capability.rs +0 -0
  36. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/catalog/lock.rs +0 -0
  37. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/catalog.rs +0 -0
  38. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/cbor.rs +0 -0
  39. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  40. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  41. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/corpus.rs +0 -0
  42. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  43. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  44. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  45. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  46. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
  47. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
  48. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
  49. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  50. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  51. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  52. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  53. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived.rs +0 -0
  54. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/descriptors.rs +0 -0
  55. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/disk.rs +0 -0
  56. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/dtype.rs +0 -0
  57. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/durability.rs +0 -0
  58. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure/admission.rs +0 -0
  59. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure/flight.rs +0 -0
  60. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure.rs +0 -0
  61. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/err.rs +0 -0
  62. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/fetch.rs +0 -0
  63. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/filesystem_identity.rs +0 -0
  64. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/fill.rs +0 -0
  65. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/fit.rs +0 -0
  66. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/gc.rs +0 -0
  67. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/header.rs +0 -0
  68. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/home.rs +0 -0
  69. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ids.rs +0 -0
  70. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  71. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  72. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  73. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  74. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  75. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  76. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  77. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  78. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/narrow.rs +0 -0
  79. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  80. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/anima.routes.json +0 -0
  81. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/anima.spec.json +0 -0
  82. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
  83. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
  84. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes.rs +0 -0
  85. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  86. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  87. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  88. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/jcs.rs +0 -0
  89. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/keyed_roots.rs +0 -0
  90. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/lib.rs +0 -0
  91. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/limits.rs +0 -0
  92. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/machine.rs +0 -0
  93. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/manifest.rs +0 -0
  94. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/meta.rs +0 -0
  95. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/project.rs +0 -0
  96. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/providers.rs +0 -0
  97. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/read.rs +0 -0
  98. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/receipt.rs +0 -0
  99. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/reclaim.rs +0 -0
  100. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/registry.rs +0 -0
  101. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/relation.rs +0 -0
  102. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  103. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/repository.rs +0 -0
  104. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/sha256.rs +0 -0
  105. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  106. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/spec.rs +0 -0
  107. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/staging.rs +0 -0
  108. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/stats.rs +0 -0
  109. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/storage.rs +0 -0
  110. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/store.rs +0 -0
  111. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/api.rs +0 -0
  112. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/decisions.rs +0 -0
  113. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/download.rs +0 -0
  114. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/http.rs +0 -0
  115. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  116. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  117. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  118. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  119. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  120. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  121. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  122. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/push.rs +0 -0
  123. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/sources.rs +0 -0
  124. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/tests/tail.rs +0 -0
  125. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  126. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/unsynced.rs +0 -0
  127. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/src/vectors.rs +0 -0
  128. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  129. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  130. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/as_is.rs +0 -0
  131. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/catalog_owner_probe.c +0 -0
  132. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  133. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/civitai_anima.rs +0 -0
  134. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
  135. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/ensure.rs +0 -0
  136. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  137. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  138. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fetch_faults.rs +0 -0
  139. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  140. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fit.rs +0 -0
  141. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  142. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  143. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  144. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  145. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  146. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  147. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  148. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  149. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  150. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  151. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/home.rs +0 -0
  152. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
  153. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  154. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  155. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/keyed_roots.rs +0 -0
  156. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/liveness.rs +0 -0
  157. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  158. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  159. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  160. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  161. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  162. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  163. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  164. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
  165. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  166. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  167. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  168. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_download.rs +0 -0
  169. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  170. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  171. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  172. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/staging.rs +0 -0
  173. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  174. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  175. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  176. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  177. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/version.rs +0 -0
  178. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-core/tests/write_through.rs +0 -0
  179. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/Cargo.toml +0 -0
  180. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/g1_vmm.rs +0 -0
  181. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/g2_overlap.rs +0 -0
  182. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/gpu_bench/mod.rs +0 -0
  183. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/cuda.rs +0 -0
  184. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/host.rs +0 -0
  185. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/layout.rs +0 -0
  186. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/src/lib.rs +0 -0
  187. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/census.rs +0 -0
  188. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/host_tier.rs +0 -0
  189. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/Cargo.toml +0 -0
  190. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/docs.rs +0 -0
  191. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/ensure.rs +0 -0
  192. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/errors.rs +0 -0
  193. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/ingest.rs +0 -0
  194. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/lib.rs +0 -0
  195. {tensorfs-0.3.91 → tensorfs-0.3.92}/crates/tensorfs-py/src/store.rs +0 -0
  196. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/__init__.py +0 -0
  197. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_derived_channel.py +0 -0
  198. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_ext.pyi +0 -0
  199. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_source_channel.py +0 -0
  200. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/_tensors.py +0 -0
  201. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/derived.py +0 -0
  202. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/errors.py +0 -0
  203. {tensorfs-0.3.91 → tensorfs-0.3.92}/python/tensorfs/plane.py +0 -0
  204. {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.91"
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.91"
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.91"
477
+ version = "0.3.92"
478
478
  dependencies = [
479
479
  "pyo3",
480
480
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-plane", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.91"
6
+ version = "0.3.92"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.91
3
+ Version: 0.3.92
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -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, after it if it is filling, else disk → staging buffers → device.
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
- "misses", "evictions", "evicted_bytes", "trims"}],
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 { from_host },
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 from_host: bool,
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 r = enqueue_copy(m, stream, bounces, waits, dst, len, src);
511
- let Err(e) = r else { return r };
512
- let d = cuda::driver()?;
513
- // SAFETY: our own stream.
514
- let drained = crate::faults::hit("drain").and_then(|_| {
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) => Err(Error::Poisoned(format!("a failed copy ({e}) could not be drained: {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, from_host: true })
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, from_host: false })
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 path, armed only by tests (cargo feature `faults`). Without the
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
- //! Each mode's bytes are counted.
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). `None` where the
58
- /// kernel lacks it (< 6.5): the caller treats the range as cold.
59
- fn cached(f: &File, off: u64, len: u64) -> Option<bool> {
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
- Some(st.nr_cache >= (off + len).div_ceil(page) - off / page)
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 f = Arc::new(Fds { buffered, direct });
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.buffered, o.off, item.len).unwrap_or(false) => f,
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 };