tensorfs 0.3.90__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.90 → tensorfs-0.3.92}/Cargo.lock +3 -3
  2. {tensorfs-0.3.90 → tensorfs-0.3.92}/Cargo.toml +1 -1
  3. {tensorfs-0.3.90 → tensorfs-0.3.92}/PKG-INFO +1 -1
  4. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/API.md +48 -8
  5. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/cuda.rs +2 -0
  6. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/cursor.rs +5 -1
  7. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/device.rs +79 -14
  8. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/faults.rs +19 -3
  9. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/io.rs +170 -13
  10. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/plane.rs +120 -41
  11. tensorfs-0.3.92/crates/tensorfs-plane/tests/census.rs +189 -0
  12. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/common/mod.rs +28 -1
  13. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/gpu.rs +153 -11
  14. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/src/plane.rs +3 -1
  15. {tensorfs-0.3.90 → tensorfs-0.3.92}/pyproject.toml +1 -1
  16. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/_ext.pyi +1 -0
  17. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/errors.py +2 -0
  18. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/plane.pyi +5 -1
  19. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/Cargo.toml +0 -0
  20. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/build.rs +0 -0
  21. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  22. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  23. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  24. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  25. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/asset_update.rs +0 -0
  26. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/b64.rs +0 -0
  27. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  28. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  29. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ensure.rs +0 -0
  30. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  31. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  32. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  33. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  34. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  35. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  36. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  37. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  38. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/canon.rs +0 -0
  39. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/capability.rs +0 -0
  40. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/catalog/lock.rs +0 -0
  41. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/catalog.rs +0 -0
  42. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/cbor.rs +0 -0
  43. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  44. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  45. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/corpus.rs +0 -0
  46. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  47. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  48. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  49. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  50. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
  51. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
  52. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
  53. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  54. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  55. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  56. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  57. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/derived.rs +0 -0
  58. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/descriptors.rs +0 -0
  59. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/disk.rs +0 -0
  60. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/dtype.rs +0 -0
  61. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/durability.rs +0 -0
  62. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure/admission.rs +0 -0
  63. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure/flight.rs +0 -0
  64. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ensure.rs +0 -0
  65. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/err.rs +0 -0
  66. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/fetch.rs +0 -0
  67. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/filesystem_identity.rs +0 -0
  68. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/fill.rs +0 -0
  69. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/fit.rs +0 -0
  70. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/gc.rs +0 -0
  71. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/header.rs +0 -0
  72. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/home.rs +0 -0
  73. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ids.rs +0 -0
  74. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  75. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  76. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  77. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  78. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  79. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  80. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  81. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  82. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/narrow.rs +0 -0
  83. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  84. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/anima.routes.json +0 -0
  85. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/anima.spec.json +0 -0
  86. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
  87. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
  88. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/routes.rs +0 -0
  89. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  90. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  91. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  92. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/jcs.rs +0 -0
  93. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/keyed_roots.rs +0 -0
  94. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/lib.rs +0 -0
  95. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/limits.rs +0 -0
  96. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/machine.rs +0 -0
  97. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/manifest.rs +0 -0
  98. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/meta.rs +0 -0
  99. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/project.rs +0 -0
  100. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/providers.rs +0 -0
  101. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/read.rs +0 -0
  102. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/receipt.rs +0 -0
  103. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/reclaim.rs +0 -0
  104. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/registry.rs +0 -0
  105. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/relation.rs +0 -0
  106. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  107. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/repository.rs +0 -0
  108. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/sha256.rs +0 -0
  109. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  110. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/spec.rs +0 -0
  111. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/staging.rs +0 -0
  112. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/stats.rs +0 -0
  113. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/storage.rs +0 -0
  114. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/store.rs +0 -0
  115. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/api.rs +0 -0
  116. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/decisions.rs +0 -0
  117. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/download.rs +0 -0
  118. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/http.rs +0 -0
  119. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  120. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  121. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  122. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  123. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  124. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  125. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  126. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/push.rs +0 -0
  127. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/sources.rs +0 -0
  128. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/tests/tail.rs +0 -0
  129. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  130. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/unsynced.rs +0 -0
  131. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/src/vectors.rs +0 -0
  132. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  133. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  134. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/as_is.rs +0 -0
  135. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/catalog_owner_probe.c +0 -0
  136. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  137. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/civitai_anima.rs +0 -0
  138. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
  139. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/ensure.rs +0 -0
  140. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  141. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  142. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fetch_faults.rs +0 -0
  143. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  144. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fit.rs +0 -0
  145. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  146. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  147. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  148. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  149. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  150. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  151. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  152. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  153. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  154. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  155. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/home.rs +0 -0
  156. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
  157. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  158. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  159. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/keyed_roots.rs +0 -0
  160. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/liveness.rs +0 -0
  161. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  162. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  163. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  164. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  165. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  166. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  167. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  168. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
  169. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  170. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  171. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  172. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_download.rs +0 -0
  173. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  174. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  175. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  176. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/staging.rs +0 -0
  177. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  178. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  179. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  180. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  181. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/version.rs +0 -0
  182. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-core/tests/write_through.rs +0 -0
  183. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/Cargo.toml +0 -0
  184. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/g1_vmm.rs +0 -0
  185. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/g2_overlap.rs +0 -0
  186. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/bin/gpu_bench/mod.rs +0 -0
  187. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/host.rs +0 -0
  188. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/layout.rs +0 -0
  189. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/src/lib.rs +0 -0
  190. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-plane/tests/host_tier.rs +0 -0
  191. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/Cargo.toml +0 -0
  192. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/src/docs.rs +0 -0
  193. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/src/ensure.rs +0 -0
  194. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/src/errors.rs +0 -0
  195. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/src/ingest.rs +0 -0
  196. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/src/lib.rs +0 -0
  197. {tensorfs-0.3.90 → tensorfs-0.3.92}/crates/tensorfs-py/src/store.rs +0 -0
  198. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/__init__.py +0 -0
  199. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/_derived_channel.py +0 -0
  200. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/_source_channel.py +0 -0
  201. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/_tensors.py +0 -0
  202. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/derived.py +0 -0
  203. {tensorfs-0.3.90 → tensorfs-0.3.92}/python/tensorfs/plane.py +0 -0
  204. {tensorfs-0.3.90 → 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.90"
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.90"
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.90"
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.90"
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.90
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
@@ -1,4 +1,4 @@
1
- # tensorfs.plane — Python API (v3.1, 2026-10-02)
1
+ # tensorfs.plane — Python API (v3.2, 2026-10-02)
2
2
 
3
3
  The weight plane moves bytes; cozy-runtime decides. The plane never measures free memory,
4
4
  never picks victims among equals, never reorders a schedule. It ships inside the `tensorfs`
@@ -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",
@@ -240,10 +257,16 @@ stream cannot drain is `Poisoned`.
240
257
  **Poison is latched plane-wide**: an ambiguous driver outcome (a failed unmap or rollback, an
241
258
  undrainable copy failure, a driver mapping that disagrees with the books) or a lease dropped
242
259
  unreleased makes every later call raise `Poisoned` with the first cause; the runtime replaces
243
- the executor. A poisoned plane never unmaps, unregisters or frees a range (copies may still
244
- touch it): `ws.close()`, `cur.close()` and `p.close()` raise `Poisoned` and the process keeps
245
- the ranges until it exits. Capacity never poisons (`BudgetExceeded`, `Shortfall` leave the
246
- plane usable).
260
+ the executor. A poisoned plane never unmaps, unregisters or frees a range while it lives
261
+ (copies may still touch it): `ws.close()`, `cur.close()` and `p.close()` raise `Poisoned`.
262
+ Capacity never poisons (`BudgetExceeded`, `Shortfall` leave the plane usable).
263
+
264
+ **A plane collected without `close()`** (its handles dropped in any order, or poisoned) is
265
+ torn down when its last handle goes: its threads stop, and once every device has run all
266
+ queued work, its device ranges are unmapped, its handles released and its pinned RAM let go as
267
+ `close()` would. Only a device that cannot be synchronized keeps its ranges and the pinned
268
+ tier until the process exits. Closing is still the contract: collection order decides when
269
+ an unclosed plane ends.
247
270
 
248
271
  ## Host tier across processes
249
272
 
@@ -274,6 +297,23 @@ plane usable).
274
297
 
275
298
  ## Changelog
276
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
+
312
+ - v3.2 2026-10-02 (W3: fd and thread growth per weight set): a plane collected unclosed or
313
+ poisoned is torn down at its last handle (threads, device memory, pinned memfds) once its
314
+ devices are synchronized; before, its reader threads stayed and a poisoned plane kept its
315
+ memfds for the life of the process.
316
+
277
317
  - v3.1 2026-10-02 (Xid 31; W3 run 2675): a failed copy is reported only after its stream drained;
278
318
  an undrainable one poisons; a poisoned plane never unmaps (close raises `Poisoned`).
279
319
  `verify_bytes()` after release raises `Invalid`. Pinned-tier claims: close and eviction punch
@@ -106,6 +106,7 @@ pub struct Driver {
106
106
  pub ctx_get_current: F<*mut CUcontext>,
107
107
  pub ctx_set_current: F<CUcontext>,
108
108
  pub ctx_synchronize: unsafe extern "C" fn() -> CUresult,
109
+ pub mem_get_info: unsafe extern "C" fn(*mut usize, *mut usize) -> CUresult,
109
110
  pub stream_create: unsafe extern "C" fn(*mut CUstream, c_uint) -> CUresult,
110
111
  pub stream_destroy: F<CUstream>,
111
112
  pub stream_synchronize: F<CUstream>,
@@ -189,6 +190,7 @@ impl Driver {
189
190
  ctx_get_current: load!(lib, b"cuCtxGetCurrent"),
190
191
  ctx_set_current: load!(lib, b"cuCtxSetCurrent"),
191
192
  ctx_synchronize: load!(lib, b"cuCtxSynchronize"),
193
+ mem_get_info: load!(lib, b"cuMemGetInfo_v2"),
192
194
  stream_create: load!(lib, b"cuStreamCreate"),
193
195
  stream_destroy: load!(lib, b"cuStreamDestroy_v2"),
194
196
  stream_synchronize: load!(lib, b"cuStreamSynchronize"),
@@ -72,6 +72,10 @@ pub(crate) struct Cur {
72
72
  }
73
73
 
74
74
  impl Cur {
75
+ /// (device slot, ring base, ring length, ring handles), for a plane torn down unclosed.
76
+ pub(crate) fn into_ring(self) -> (usize, u64, u64, Vec<Chunk>) {
77
+ (self.dev, self.ring_base, self.ring_len, self.ring_chunks)
78
+ }
75
79
  fn streamed_total(&self) -> u64 {
76
80
  if self.total == u64::MAX {
77
81
  return u64::MAX;
@@ -162,7 +166,7 @@ fn submit(inner: &Arc<Inner>, c: &mut Core, id: u64, k: u64, off: u64, waits: Ve
162
166
  c.timings.push_back(Timing {
163
167
  dev: d,
164
168
  ws: ws.0,
165
- kind: TimingKind::Copy { from_host },
169
+ kind: TimingKind::Copy { from: cp.from, host_ns: cp.host_ns },
166
170
  a: cp.start.clone(),
167
171
  b: cp.ready.clone(),
168
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 {
@@ -431,8 +448,13 @@ impl Mover {
431
448
  }
432
449
  }
433
450
 
434
- pub fn stop(&self) {
451
+ /// Close the queue: the thread drains its stream, frees its buffers and exits by itself.
452
+ pub fn detach(&self) {
435
453
  self.tx.lock().unwrap().take();
454
+ }
455
+
456
+ pub fn stop(&self) {
457
+ self.detach();
436
458
  if let Some(t) = self.thread.lock().unwrap().take() {
437
459
  let _ = t.join();
438
460
  }
@@ -469,8 +491,16 @@ fn mover_main(m: MoverCtx, rx: Receiver<CopyJob>, ok: Sender<Result<()>>) {
469
491
  };
470
492
  while let Ok(job) = rx.recv() {
471
493
  let done = job.done;
472
- 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());
473
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
+ }
474
504
  }
475
505
  if let Ok(d) = cuda::driver() {
476
506
  // SAFETY: our stream and buffers; the stream is drained first.
@@ -493,6 +523,9 @@ struct Waiter {
493
523
  /// no reader still writes a bounce: a failed copy is drained before it is reported, so no
494
524
  /// caller unmaps, unregisters or reuses a range a copy still touches. A drain that itself fails
495
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.
496
529
  fn run_copy(
497
530
  m: &MoverCtx,
498
531
  stream: CUstream,
@@ -501,20 +534,36 @@ fn run_copy(
501
534
  dst: u64,
502
535
  len: u64,
503
536
  src: CopySrc,
504
- ) -> Result<Copied> {
505
- let r = enqueue_copy(m, stream, bounces, waits, dst, len, src);
506
- let Err(e) = r else { return r };
507
- let d = cuda::driver()?;
508
- // SAFETY: our own stream.
509
- 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.
510
555
  cuda::check("cuStreamSynchronize", unsafe { (d.stream_synchronize)(stream) })
511
556
  });
512
557
  match drained {
513
- Ok(()) => Err(e),
514
- 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
+ }
515
563
  }
516
564
  }
517
565
 
566
+ #[allow(clippy::too_many_arguments)]
518
567
  fn enqueue_copy(
519
568
  m: &MoverCtx,
520
569
  stream: CUstream,
@@ -523,6 +572,7 @@ fn enqueue_copy(
523
572
  dst: u64,
524
573
  len: u64,
525
574
  src: CopySrc,
575
+ maps: &[Mapping],
526
576
  ) -> Result<Copied> {
527
577
  let d = cuda::driver()?;
528
578
  let start = m.events.get(true)?;
@@ -545,7 +595,22 @@ fn enqueue_copy(
545
595
  })?;
546
596
  crate::faults::hit("host-copy-recorded")?;
547
597
  ready.record(stream)?;
548
- 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 })
549
614
  }
550
615
  CopySrc::Disk { source, items, base } => {
551
616
  let cap = m.staging.1;
@@ -632,7 +697,7 @@ fn enqueue_copy(
632
697
  start.record(stream)?;
633
698
  }
634
699
  ready.record(stream)?;
635
- Ok(Copied { ready, start, bytes: len, from_host: false })
700
+ Ok(Copied { ready, start, bytes: len, from: CopyFrom::Disk, host_ns: 0 })
636
701
  }
637
702
  }
638
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 };
@@ -278,10 +429,16 @@ impl Readers {
278
429
  self.q.1.notify_one();
279
430
  }
280
431
 
281
- /// Finish queued jobs, then stop the threads.
282
- pub fn shutdown(&self) {
432
+ /// Let the threads exit once the queue is empty; waits for nothing (the caller may be one
433
+ /// of them).
434
+ pub fn stop(&self) {
283
435
  self.q.0.lock().unwrap().stop = true;
284
436
  self.q.1.notify_all();
437
+ }
438
+
439
+ /// Finish queued jobs, then stop the threads.
440
+ pub fn shutdown(&self) {
441
+ self.stop();
285
442
  for t in self.threads.lock().unwrap().drain(..) {
286
443
  let _ = t.join();
287
444
  }