letify 2.5.3__tar.gz → 2.6.0__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 (210) hide show
  1. {letify-2.5.3 → letify-2.6.0}/PKG-INFO +1 -1
  2. {letify-2.5.3 → letify-2.6.0}/docs/SPEC.md +25 -1
  3. {letify-2.5.3 → letify-2.6.0}/letify/__init__.py +1 -1
  4. {letify-2.5.3 → letify-2.6.0}/letify/cli.py +9 -2
  5. {letify-2.5.3 → letify-2.6.0}/letify/protocol/worker.py +44 -1
  6. {letify-2.5.3 → letify-2.6.0}/letify/runtime/session.py +1 -1
  7. {letify-2.5.3 → letify-2.6.0}/letify/store/pathdata.py +360 -51
  8. {letify-2.5.3 → letify-2.6.0}/letify-core/Cargo.lock +3 -3
  9. {letify-2.5.3 → letify-2.6.0}/letify-core/Cargo.toml +1 -1
  10. {letify-2.5.3 → letify-2.6.0}/letify-ext/package-lock.json +2 -2
  11. {letify-2.5.3 → letify-2.6.0}/letify-ext/package.json +1 -1
  12. {letify-2.5.3 → letify-2.6.0}/pyproject.toml +1 -1
  13. {letify-2.5.3 → letify-2.6.0}/tests/test_cli.py +14 -0
  14. {letify-2.5.3 → letify-2.6.0}/tests/test_data_streaming.py +43 -0
  15. {letify-2.5.3 → letify-2.6.0}/tests/test_parallel_placement.py +114 -1
  16. {letify-2.5.3 → letify-2.6.0}/.github/workflows/ci.yml +0 -0
  17. {letify-2.5.3 → letify-2.6.0}/.github/workflows/docs.yml +0 -0
  18. {letify-2.5.3 → letify-2.6.0}/.github/workflows/publish.yml +0 -0
  19. {letify-2.5.3 → letify-2.6.0}/.gitignore +0 -0
  20. {letify-2.5.3 → letify-2.6.0}/.researchtree +0 -0
  21. {letify-2.5.3 → letify-2.6.0}/CLAUDE.md +0 -0
  22. {letify-2.5.3 → letify-2.6.0}/LICENSE +0 -0
  23. {letify-2.5.3 → letify-2.6.0}/PROJECT.md +0 -0
  24. {letify-2.5.3 → letify-2.6.0}/README.md +0 -0
  25. {letify-2.5.3 → letify-2.6.0}/docs/COMPONENT.md +0 -0
  26. {letify-2.5.3 → letify-2.6.0}/docs/INTENT.md +0 -0
  27. {letify-2.5.3 → letify-2.6.0}/docs/NETWORK.md +0 -0
  28. {letify-2.5.3 → letify-2.6.0}/docs/guide/01-getting-started.md +0 -0
  29. {letify-2.5.3 → letify-2.6.0}/docs/guide/02-providers.md +0 -0
  30. {letify-2.5.3 → letify-2.6.0}/docs/guide/03-execution-modes.md +0 -0
  31. {letify-2.5.3 → letify-2.6.0}/docs/guide/04-environments-and-data.md +0 -0
  32. {letify-2.5.3 → letify-2.6.0}/docs/guide/05-concurrency.md +0 -0
  33. {letify-2.5.3 → letify-2.6.0}/docs/guide/06-cost.md +0 -0
  34. {letify-2.5.3 → letify-2.6.0}/docs/guide/07-troubleshooting.md +0 -0
  35. {letify-2.5.3 → letify-2.6.0}/docs/guide/08-releasing.md +0 -0
  36. {letify-2.5.3 → letify-2.6.0}/docs/guide/README.md +0 -0
  37. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  38. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  39. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  40. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
  41. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  42. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  43. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  44. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  45. {letify-2.5.3 → letify-2.6.0}/docs/guide/ko/README.md +0 -0
  46. {letify-2.5.3 → letify-2.6.0}/docs/locales/README_ko.md +0 -0
  47. {letify-2.5.3 → letify-2.6.0}/docs/mkdocs.yml +0 -0
  48. {letify-2.5.3 → letify-2.6.0}/examples/README.md +0 -0
  49. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/.letify.example +0 -0
  50. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/00_smoke.py +0 -0
  51. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/01_sweep.py +0 -0
  52. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/02_resume.py +0 -0
  53. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/03_watch.py +0 -0
  54. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/README.md +0 -0
  55. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/README_ko.md +0 -0
  56. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/common.py +0 -0
  57. {letify-2.5.3 → letify-2.6.0}/examples/nvfp4-lora/recipe.py +0 -0
  58. {letify-2.5.3 → letify-2.6.0}/letify/config/__init__.py +0 -0
  59. {letify-2.5.3 → letify-2.6.0}/letify/config/inventory.py +0 -0
  60. {letify-2.5.3 → letify-2.6.0}/letify/config/login.py +0 -0
  61. {letify-2.5.3 → letify-2.6.0}/letify/config/schema.py +0 -0
  62. {letify-2.5.3 → letify-2.6.0}/letify/config/secrets.py +0 -0
  63. {letify-2.5.3 → letify-2.6.0}/letify/config/writer.py +0 -0
  64. {letify-2.5.3 → letify-2.6.0}/letify/declare/__init__.py +0 -0
  65. {letify-2.5.3 → letify-2.6.0}/letify/declare/cache.py +0 -0
  66. {letify-2.5.3 → letify-2.6.0}/letify/declare/env.py +0 -0
  67. {letify-2.5.3 → letify-2.6.0}/letify/declare/function.py +0 -0
  68. {letify-2.5.3 → letify-2.6.0}/letify/declare/instance.py +0 -0
  69. {letify-2.5.3 → letify-2.6.0}/letify/declare/reduce.py +0 -0
  70. {letify-2.5.3 → letify-2.6.0}/letify/declare/replicas.py +0 -0
  71. {letify-2.5.3 → letify-2.6.0}/letify/errors.py +0 -0
  72. {letify-2.5.3 → letify-2.6.0}/letify/install.py +0 -0
  73. {letify-2.5.3 → letify-2.6.0}/letify/launcher.py +0 -0
  74. {letify-2.5.3 → letify-2.6.0}/letify/paths.py +0 -0
  75. {letify-2.5.3 → letify-2.6.0}/letify/protocol/__init__.py +0 -0
  76. {letify-2.5.3 → letify-2.6.0}/letify/protocol/codec.py +0 -0
  77. {letify-2.5.3 → letify-2.6.0}/letify/protocol/driver.py +0 -0
  78. {letify-2.5.3 → letify-2.6.0}/letify/protocol/handle.py +0 -0
  79. {letify-2.5.3 → letify-2.6.0}/letify/protocol/wire.py +0 -0
  80. {letify-2.5.3 → letify-2.6.0}/letify/providers/__init__.py +0 -0
  81. {letify-2.5.3 → letify-2.6.0}/letify/providers/base.py +0 -0
  82. {letify-2.5.3 → letify-2.6.0}/letify/providers/colab.py +0 -0
  83. {letify-2.5.3 → letify-2.6.0}/letify/providers/colab_files.py +0 -0
  84. {letify-2.5.3 → letify-2.6.0}/letify/providers/colab_keepalive.py +0 -0
  85. {letify-2.5.3 → letify-2.6.0}/letify/providers/elice.py +0 -0
  86. {letify-2.5.3 → letify-2.6.0}/letify/providers/kaggle.py +0 -0
  87. {letify-2.5.3 → letify-2.6.0}/letify/providers/kaggle_adapter.py +0 -0
  88. {letify-2.5.3 → letify-2.6.0}/letify/providers/local.py +0 -0
  89. {letify-2.5.3 → letify-2.6.0}/letify/providers/modal.py +0 -0
  90. {letify-2.5.3 → letify-2.6.0}/letify/providers/modal_adapter.py +0 -0
  91. {letify-2.5.3 → letify-2.6.0}/letify/providers/naming.py +0 -0
  92. {letify-2.5.3 → letify-2.6.0}/letify/providers/shell.py +0 -0
  93. {letify-2.5.3 → letify-2.6.0}/letify/providers/tunnel.py +0 -0
  94. {letify-2.5.3 → letify-2.6.0}/letify/providers/usage.py +0 -0
  95. {letify-2.5.3 → letify-2.6.0}/letify/remoting/__init__.py +0 -0
  96. {letify-2.5.3 → letify-2.6.0}/letify/remoting/capability.py +0 -0
  97. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/__init__.py +0 -0
  98. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/client.py +0 -0
  99. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/cuda.py +0 -0
  100. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/executor.py +0 -0
  101. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/frames.py +0 -0
  102. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/guard.py +0 -0
  103. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/tensor.py +0 -0
  104. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/trace.py +0 -0
  105. {letify-2.5.3 → letify-2.6.0}/letify/remoting/device/value.py +0 -0
  106. {letify-2.5.3 → letify-2.6.0}/letify/remoting/loader.py +0 -0
  107. {letify-2.5.3 → letify-2.6.0}/letify/remoting/probe.py +0 -0
  108. {letify-2.5.3 → letify-2.6.0}/letify/render.py +0 -0
  109. {letify-2.5.3 → letify-2.6.0}/letify/runtime/__init__.py +0 -0
  110. {letify-2.5.3 → letify-2.6.0}/letify/runtime/bootstrap.py +0 -0
  111. {letify-2.5.3 → letify-2.6.0}/letify/runtime/broker.py +0 -0
  112. {letify-2.5.3 → letify-2.6.0}/letify/runtime/channel.py +0 -0
  113. {letify-2.5.3 → letify-2.6.0}/letify/runtime/lease.py +0 -0
  114. {letify-2.5.3 → letify-2.6.0}/letify/runtime/pool.py +0 -0
  115. {letify-2.5.3 → letify-2.6.0}/letify/runtime/telemetry.py +0 -0
  116. {letify-2.5.3 → letify-2.6.0}/letify/store/__init__.py +0 -0
  117. {letify-2.5.3 → letify-2.6.0}/letify/store/backends/__init__.py +0 -0
  118. {letify-2.5.3 → letify-2.6.0}/letify/store/backends/filesystem.py +0 -0
  119. {letify-2.5.3 → letify-2.6.0}/letify/store/backends/google_auth.py +0 -0
  120. {letify-2.5.3 → letify-2.6.0}/letify/store/backends/layout.py +0 -0
  121. {letify-2.5.3 → letify-2.6.0}/letify/store/backends/objects.py +0 -0
  122. {letify-2.5.3 → letify-2.6.0}/letify/store/cas.py +0 -0
  123. {letify-2.5.3 → letify-2.6.0}/letify/store/sendorder.py +0 -0
  124. {letify-2.5.3 → letify-2.6.0}/letify/store/volume.py +0 -0
  125. {letify-2.5.3 → letify-2.6.0}/letify/stubs.py +0 -0
  126. {letify-2.5.3 → letify-2.6.0}/letify/tools.py +0 -0
  127. {letify-2.5.3 → letify-2.6.0}/letify/transport/__init__.py +0 -0
  128. {letify-2.5.3 → letify-2.6.0}/letify/transport/agent.py +0 -0
  129. {letify-2.5.3 → letify-2.6.0}/letify/transport/announce.py +0 -0
  130. {letify-2.5.3 → letify-2.6.0}/letify/transport/link.py +0 -0
  131. {letify-2.5.3 → letify-2.6.0}/letify/transport/nat.py +0 -0
  132. {letify-2.5.3 → letify-2.6.0}/letify/transport/pipeline.py +0 -0
  133. {letify-2.5.3 → letify-2.6.0}/letify/transport/portmap.py +0 -0
  134. {letify-2.5.3 → letify-2.6.0}/letify/transport/probe.py +0 -0
  135. {letify-2.5.3 → letify-2.6.0}/letify/transport/quic.py +0 -0
  136. {letify-2.5.3 → letify-2.6.0}/letify/transport/rendezvous.py +0 -0
  137. {letify-2.5.3 → letify-2.6.0}/letify/transport/setup.py +0 -0
  138. {letify-2.5.3 → letify-2.6.0}/letify/transport/sshopts.py +0 -0
  139. {letify-2.5.3 → letify-2.6.0}/letify/transport/strategies.py +0 -0
  140. {letify-2.5.3 → letify-2.6.0}/letify-core/.gitignore +0 -0
  141. {letify-2.5.3 → letify-2.6.0}/letify-core/build.py +0 -0
  142. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-agent/Cargo.toml +0 -0
  143. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-agent/src/driver.rs +0 -0
  144. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-agent/src/main.rs +0 -0
  145. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-driver/Cargo.toml +0 -0
  146. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-driver/src/client.rs +0 -0
  147. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-driver/src/exports.rs +0 -0
  148. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-driver/src/image.rs +0 -0
  149. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-driver/src/lib.rs +0 -0
  150. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-driver/src/status.rs +0 -0
  151. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-driver/src/table.rs +0 -0
  152. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-quic/Cargo.toml +0 -0
  153. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-quic/src/main.rs +0 -0
  154. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-wire/Cargo.toml +0 -0
  155. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-wire/src/lib.rs +0 -0
  156. {letify-2.5.3 → letify-2.6.0}/letify-core/letify-wire/tests/throughput.rs +0 -0
  157. {letify-2.5.3 → letify-2.6.0}/letify-core/rust-toolchain.toml +0 -0
  158. {letify-2.5.3 → letify-2.6.0}/letify-ext/.vscodeignore +0 -0
  159. {letify-2.5.3 → letify-2.6.0}/letify-ext/LICENSE +0 -0
  160. {letify-2.5.3 → letify-2.6.0}/letify-ext/README.md +0 -0
  161. {letify-2.5.3 → letify-2.6.0}/letify-ext/media/icon.svg +0 -0
  162. {letify-2.5.3 → letify-2.6.0}/letify-ext/src/cli.ts +0 -0
  163. {letify-2.5.3 → letify-2.6.0}/letify-ext/src/extension.ts +0 -0
  164. {letify-2.5.3 → letify-2.6.0}/letify-ext/src/history.ts +0 -0
  165. {letify-2.5.3 → letify-2.6.0}/letify-ext/src/model.ts +0 -0
  166. {letify-2.5.3 → letify-2.6.0}/letify-ext/src/poll.ts +0 -0
  167. {letify-2.5.3 → letify-2.6.0}/letify-ext/src/render.ts +0 -0
  168. {letify-2.5.3 → letify-2.6.0}/letify-ext/test/cli.test.ts +0 -0
  169. {letify-2.5.3 → letify-2.6.0}/letify-ext/test/manifest.test.ts +0 -0
  170. {letify-2.5.3 → letify-2.6.0}/letify-ext/test/model.test.ts +0 -0
  171. {letify-2.5.3 → letify-2.6.0}/letify-ext/test/regressions.test.ts +0 -0
  172. {letify-2.5.3 → letify-2.6.0}/letify-ext/tsconfig.json +0 -0
  173. {letify-2.5.3 → letify-2.6.0}/letify_providers.py +0 -0
  174. {letify-2.5.3 → letify-2.6.0}/scripts/sync-main.sh +0 -0
  175. {letify-2.5.3 → letify-2.6.0}/scripts/version.py +0 -0
  176. {letify-2.5.3 → letify-2.6.0}/tests/conftest.py +0 -0
  177. {letify-2.5.3 → letify-2.6.0}/tests/fake_kaggle_adapter.py +0 -0
  178. {letify-2.5.3 → letify-2.6.0}/tests/fake_modal_adapter.py +0 -0
  179. {letify-2.5.3 → letify-2.6.0}/tests/test_cli_output.py +0 -0
  180. {letify-2.5.3 → letify-2.6.0}/tests/test_colab_keepalive.py +0 -0
  181. {letify-2.5.3 → letify-2.6.0}/tests/test_config.py +0 -0
  182. {letify-2.5.3 → letify-2.6.0}/tests/test_core.py +0 -0
  183. {letify-2.5.3 → letify-2.6.0}/tests/test_data.py +0 -0
  184. {letify-2.5.3 → letify-2.6.0}/tests/test_declare.py +0 -0
  185. {letify-2.5.3 → letify-2.6.0}/tests/test_device.py +0 -0
  186. {letify-2.5.3 → letify-2.6.0}/tests/test_distributed.py +0 -0
  187. {letify-2.5.3 → letify-2.6.0}/tests/test_editor_sessions.py +0 -0
  188. {letify-2.5.3 → letify-2.6.0}/tests/test_elice.py +0 -0
  189. {letify-2.5.3 → letify-2.6.0}/tests/test_frames.py +0 -0
  190. {letify-2.5.3 → letify-2.6.0}/tests/test_install.py +0 -0
  191. {letify-2.5.3 → letify-2.6.0}/tests/test_kaggle.py +0 -0
  192. {letify-2.5.3 → letify-2.6.0}/tests/test_kaggle_cookie.py +0 -0
  193. {letify-2.5.3 → letify-2.6.0}/tests/test_kaggle_link.py +0 -0
  194. {letify-2.5.3 → letify-2.6.0}/tests/test_link_log.py +0 -0
  195. {letify-2.5.3 → letify-2.6.0}/tests/test_notebook.py +0 -0
  196. {letify-2.5.3 → letify-2.6.0}/tests/test_paths.py +0 -0
  197. {letify-2.5.3 → letify-2.6.0}/tests/test_portmap.py +0 -0
  198. {letify-2.5.3 → letify-2.6.0}/tests/test_protocol.py +0 -0
  199. {letify-2.5.3 → letify-2.6.0}/tests/test_providers.py +0 -0
  200. {letify-2.5.3 → letify-2.6.0}/tests/test_quic.py +0 -0
  201. {letify-2.5.3 → letify-2.6.0}/tests/test_remoting.py +0 -0
  202. {letify-2.5.3 → letify-2.6.0}/tests/test_render.py +0 -0
  203. {letify-2.5.3 → letify-2.6.0}/tests/test_runtime.py +0 -0
  204. {letify-2.5.3 → letify-2.6.0}/tests/test_ship.py +0 -0
  205. {letify-2.5.3 → letify-2.6.0}/tests/test_store.py +0 -0
  206. {letify-2.5.3 → letify-2.6.0}/tests/test_strategies.py +0 -0
  207. {letify-2.5.3 → letify-2.6.0}/tests/test_stubs.py +0 -0
  208. {letify-2.5.3 → letify-2.6.0}/tests/test_transport.py +0 -0
  209. {letify-2.5.3 → letify-2.6.0}/tests/test_tunnel_setup.py +0 -0
  210. {letify-2.5.3 → letify-2.6.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.5.3
3
+ Version: 2.6.0
4
4
  Summary: Declarations that become infrastructure.
5
5
  Project-URL: Homepage, https://github.com/DarkPyonix/letify
6
6
  Project-URL: Documentation, https://github.com/DarkPyonix/letify/tree/main/docs
@@ -316,7 +316,11 @@ Separately, and without needing a threshold at all, a refusal says how long ago
316
316
 
317
317
  The record therefore notes the process as well as the ref, `<username>/<slug>` and the pid separated by a tab, so a notebook a live process is still using is never deleted. A ref whose pid is still running on this machine is reported as in use and skipped. A ref with no pid, written by a letify older than this, cannot be checked and counts as an orphan, because the record is per machine and an older letify is not running.
318
318
 
319
- `letify stop` with no `--orphans` deletes nothing and says so, because a bare `stop` reads as ending everything. The command lists what it will delete and asks before deleting, `-y` answers yes, and `--alias` limits it to one account. Each ref is deleted with the same CLI call `stop` uses and dropped from the record only when the delete succeeds, so a failure leaves the ref to be tried again rather than losing track of it. The exit status is 1 when any delete failed.
319
+ `letify stop` with no `--orphans` deletes nothing and says why rather than only that: a session whose
320
+ creating process is still running ends when that process ends, because taking the session from under
321
+ it leaves it calling a runtime that is gone. So the line names the way out, `send SIGTERM to the
322
+ process that started it, which letify handles by closing its sessions`, and then what `--orphans` is
323
+ for. A bare `stop` otherwise reads as ending everything. The command lists what it will delete and asks before deleting, `-y` answers yes, and `--alias` limits it to one account. Each ref is deleted with the same CLI call `stop` uses and dropped from the record only when the delete succeeds, so a failure leaves the ref to be tried again rather than losing track of it. The exit status is 1 when any delete failed.
320
324
 
321
325
  **Deleting the notebook, through the CLI only.** `stop` deletes the notebook with the official CLI, `kaggle kernels delete -y <username>/<slug>`, authenticated by the account's API token in `KAGGLE_API_TOKEN`; deletion of a notebook created for a cookie-authenticated interactive session is confirmed on the live account. The cookie is never asked, not even as a fallback, because deleting a notebook is not part of running or ending the interactive session. The call never fails the runtime, because the accelerator quota is already released by cancelling the run before it is tried. When it cannot run at all, for want of a token, of uv, or of the slug Kaggle did not return, or when the command itself fails, `stop` prints one warning line naming the notebook by its slug or id, never its cookie, so the user can delete it by hand on kaggle.com. The chain that starts the session, all on `https://www.kaggle.com/api/i/`, authenticated by the cookie:
322
326
 
@@ -1125,6 +1129,25 @@ So a blob above `transfer_parallel_mib`, 64 MiB by default, on a link whose meas
1125
1129
 
1126
1130
  The round trip is a condition because the gain depends on it. A single stream is window limited only when the window takes a long time to fill, so on a short link it already carries everything the link will carry and the extra connections are pure cost. Measured to a lab server over forward SSH at 0.25 ms, 256 MiB went at 36.1 MiB/s on one stream and 33.5 MiB/s on four, about 7% worse, which is the cost of opening the channels and their worker processes. The link reports what the probe measured, and a link with no measurement, such as a provider fallback, is treated as short and keeps one stream. A provider whose channel is not a connection it can open more of keeps one stream too: Modal's sandbox reads and writes through its own API, and a Kaggle session that never punched is carried by a kernel bridge, so neither has a second connection to offer. A Kaggle session that did punch is reached over SSH and does: each transfer stream asks the link for its own SSH command, which lands on a local forwarding port of its own and so makes the link punch again.
1127
1131
 
1132
+ The connections carry different blobs at the same time as well as different ranges of one, because a
1133
+ call whose blobs are each under the split threshold would otherwise send them one after another and
1134
+ reach one connection's ceiling however many files it has. Measured on a Kaggle runtime at a 190 ms
1135
+ round trip, 20 files of 10 MB each went at 5.0 MiB/s on one connection, which is the 0.9 MiB window
1136
+ over that round trip. So the sender runs `transfer_streams` senders over the pool rather than one, and
1137
+ the condition for using more than one connection is the link's round trip alone: the size threshold
1138
+ decides whether one blob is split, not whether blobs may overlap. A blob the worker asked for by
1139
+ `data_want` still goes next, because the queue is reordered before a sender takes from it.
1140
+
1141
+ The connections are opened once per call and reused by every blob of it, not opened and closed per
1142
+ blob. A call that sends a hundred blobs over a punched link paid a hundred rounds of punching when
1143
+ each blob opened its own, which is both the cost the estimate below weighs and a hundred chances for
1144
+ a punch to be refused. They are closed when the call's transfer ends.
1145
+
1146
+ Reuse is what makes the cost worth paying, so the estimate charges it only while the connections are
1147
+ not yet open. The first blob large enough to want them pays the punch and weighs it; every blob after
1148
+ it finds them open and weighs nothing, so a blob under the size threshold still goes on one
1149
+ connection, but a call's second large blob is split whenever splitting is faster at all.
1150
+
1128
1151
  A connection that costs something to open has to pay for itself, so a provider reports `transfer_connection_cost_s`, the seconds one more connection takes. It is 0 for a provider that dials an address, and about 10 for one reached by a hole punch, because that is what a punch measures. The streams are opened only when the transfer is expected to finish sooner with them: the estimate uses the probe's upload rate, assumes the streams scale and that their connections are opened at the same time, and keeps one stream when `size / (rate x streams) + cost` is not less than `size / rate`. A measured case: a Kaggle runtime at a 44.7 ms round trip carried 128 MiB at 21.0 MiB/s on one stream and 9.5 MiB/s on four, 2.2 times worse, because three more punches cost more than the transfer saved. The same account at a 193 ms round trip carried 1.57 MiB/s on one raw SSH connection and 6.10 MiB/s on four, so the gain is real where the link is slow enough and the blob large enough for it to be worth 10 s.
1129
1152
 
1130
1153
  The connections have to be real ones. Every SSH command letify builds carries `ControlMaster=auto`, which multiplexes further sessions onto the first connection, so opening more channels that way would add streams inside one TCP connection and gain nothing. A transfer channel therefore sets `ControlPath=none` and `ControlMaster=no`, and a punched link redials for each, so each transfer channel is its own kernel connection.
@@ -1239,6 +1262,7 @@ A `stat` cannot take that snapshot first, because it is on a data loader's hot p
1239
1262
  #### When a blob does not arrive <!-- id: project-data-streaming-failures -->
1240
1263
 
1241
1264
  - **A blob that never arrives.** A `data_want` unanswered for `data_wait_timeout` seconds on the account, 600 by default, fails that open with `RuntimeFailure` naming the relative path and the digest. The call is not killed, because the body may handle it; the failure is infrastructure, so a retry of the call is allowed.
1265
+ - **A sender that stops.** The background sender raising leaves every blob still queued unsendable. letify says so once, `letify: data sending stopped after <n> of <n> files, <n> not sent: <error>`, and tells the runtime, which wakes every reader waiting on one of those blobs with `RuntimeFailure` naming the file and that reason, and fails a reader that arrives later at once. Waiting it out instead costs `data_wait_timeout` once for every blob still queued, which is how a hundred queued shards become an hour of silence.
1242
1266
  - **A client that disconnects mid stream.** The session ends as any lost channel ends it. Every blocked reader is woken with `RuntimeFailure`, the call ends with it, and no partial file is ever placed or read. A `.partial` file is removed when the worker next starts.
1243
1267
  - **A call that finishes before the background transfer.** The transfer is cancelled, both the channel sender and any bucket upload not yet started. A blob already committed to the cache stays and counts as held for the next call, and a partial one is removed. The write-back and the `data_evict` request follow the cancellation, in that order.
1244
1268
  - **A bucket-backed ephemeral provider.** A pull that fails is retried twice with the same downscoped headers, then a third time with headers the local process derives again, and a pull that still fails fails the open with `RuntimeFailure`. A `data_want` for a digest not yet uploaded moves that object to the front of the upload queue, and the worker pulls it when the local process sends its `data_pull`.
@@ -66,7 +66,7 @@ distributed: Final = Host.distributed
66
66
  # The enum class stays importable from letify.declare.instance for letify's own use.
67
67
  del Host
68
68
 
69
- __version__ = "2.5.3"
69
+ __version__ = "2.6.0"
70
70
 
71
71
 
72
72
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -755,8 +755,15 @@ def _dispatch(args: argparse.Namespace) -> int:
755
755
 
756
756
  if args.command == "stop":
757
757
  if not args.orphans:
758
- # A bare `stop` reads as ending everything, so it ends nothing.
759
- print("nothing was stopped. Pass --orphans to delete what a dead process left")
758
+ # Spec "Kaggle", stopping what a dead process left behind: a bare stop reads as
759
+ # ending everything, so it ends nothing, and says why rather than only that.
760
+ print(
761
+ "nothing was stopped. A session whose process is still running ends when "
762
+ "that process ends, because taking it away leaves that process calling a "
763
+ "runtime that is gone: send SIGTERM to the process that started it, which "
764
+ "letify handles by closing its sessions. Pass --orphans to delete what a "
765
+ "dead process left."
766
+ )
760
767
  return 0
761
768
  rows = let.orphans(args.alias)
762
769
  found = [(row["alias"], ref) for row in rows for ref in row.get("orphans", ())]
@@ -364,6 +364,8 @@ def _op_call(request):
364
364
  import shutil
365
365
  _data_unregister(state)
366
366
  _data_check_cache(data, state["placed"])
367
+ _DATA_FAILED.pop(data["dir"], None)
368
+ _DATA_FAILED.pop("", None)
367
369
  _DATA_POLL_SEEN.pop(data["dir"], None)
368
370
  _DATA_POLL_SENT.pop(data["dir"], None)
369
371
  shutil.rmtree(data["dir"], ignore_errors=True)
@@ -1047,6 +1049,9 @@ def _data_register(data):
1047
1049
  "linked": 0,
1048
1050
  "order": {},
1049
1051
  "arrived": 0,
1052
+ # Spec "When a blob does not arrive": set when the local sender stops, so a
1053
+ # reader fails at once with its reason rather than waiting out the timeout.
1054
+ "failed": _DATA_FAILED.get(data["dir"]) or _DATA_FAILED.get(""),
1050
1055
  "wait": float(data.get("wait") or 600.0),
1051
1056
  "observe": bool(data.get("observe", True)),
1052
1057
  "prefetch": int(data.get("prefetch") or 64),
@@ -1238,6 +1243,32 @@ def _data_arrived(blobs, digest):
1238
1243
  _DATA_READY.notify_all()
1239
1244
 
1240
1245
 
1246
+ #: Reasons a sender stopped, by call directory, for a call that registers afterwards.
1247
+ _DATA_FAILED = {}
1248
+
1249
+
1250
+ def _op_data_failed(request):
1251
+ """The local sender stopped, so the blobs it had left will never arrive.
1252
+
1253
+ Spec "When a blob does not arrive": every reader waiting on one of them is woken with
1254
+ the reason, and a reader that arrives later fails on it at once, because waiting out
1255
+ the per blob timeout costs that timeout once for every blob still queued.
1256
+ """
1257
+ reason = request.get("reason") or "the local process stopped sending"
1258
+ where = request.get("dir") or ""
1259
+ with _DATA_READY:
1260
+ # Remembered by directory as well as set on the live call, because the sender can
1261
+ # stop before the call request has been read, and the call then registers after
1262
+ # this and would otherwise inherit nothing. Spec "When a blob does not arrive".
1263
+ _DATA_FAILED[where] = reason
1264
+ for state in _DATA_CALLS.values():
1265
+ if where and state["dir"] != where:
1266
+ continue
1267
+ state["failed"] = reason
1268
+ _DATA_READY.notify_all()
1269
+ return {"ok": True, "value": True}
1270
+
1271
+
1241
1272
  def _data_want(digests, blocking=False):
1242
1273
  """Ask the local process for these blobs, in this order. Nothing replies to it."""
1243
1274
  if not digests:
@@ -1304,6 +1335,10 @@ def _data_wait(path):
1304
1335
  entry = state["pending"].get(path)
1305
1336
  if entry is None:
1306
1337
  return
1338
+ if state["failed"]:
1339
+ raise _DataFailure(
1340
+ "%s was never sent: %s" % (os.path.basename(path), state["failed"])
1341
+ )
1307
1342
  digest, size = entry
1308
1343
  index = state["order"].get(digest)
1309
1344
  if index is not None and index > state["arrived"] + _DATA_MISS_AHEAD:
@@ -1321,6 +1356,11 @@ def _data_wait(path):
1321
1356
  try:
1322
1357
  with _DATA_READY:
1323
1358
  while path in state["pending"] and state["dir"] in _DATA_CALLS:
1359
+ if state["failed"]:
1360
+ raise _DataFailure(
1361
+ "%s was never sent: %s"
1362
+ % (os.path.basename(path), state["failed"])
1363
+ )
1324
1364
  left = deadline - time.monotonic()
1325
1365
  if left <= 0:
1326
1366
  raise _DataFailure(
@@ -2289,6 +2329,7 @@ _OPS = {
2289
2329
  "data_put": _op_data_put,
2290
2330
  "data_begin": _op_data_begin,
2291
2331
  "data_drop": _op_data_drop,
2332
+ "data_failed": _op_data_failed,
2292
2333
  "data_put_at": _op_data_put_at,
2293
2334
  "data_seal": _op_data_seal,
2294
2335
  "data_pull": _op_data_pull,
@@ -2314,7 +2355,9 @@ _OPS = {
2314
2355
  }
2315
2356
 
2316
2357
  #: Answered by the frame reader at once, so they work while a call runs.
2317
- _LIGHT = ("stat", "lease", "reduce_peers")
2358
+ # ``data_failed`` is light because it must not queue behind the call it is about: the call
2359
+ # is blocked on a blob the sender will never send, so a queued answer would deadlock.
2360
+ _LIGHT = ("stat", "lease", "reduce_peers", "data_failed")
2318
2361
 
2319
2362
  #: Answered by several replies, the last one marked. These send their own replies.
2320
2363
  _STREAMING = ("data_stream",)
@@ -419,7 +419,7 @@ class Runtime:
419
419
  needed = [d for d in plan.missing if d in ship_digests and d not in wave]
420
420
  if needed:
421
421
  wave = needed + wave
422
- stream = pathdata.Stream(self, blobs, plan)
422
+ stream = pathdata.Stream(self, blobs, plan, collector.call_dir)
423
423
  if not plan.missing:
424
424
  # Nothing to stream: the runtime holds every file, so the call places them all
425
425
  # before the body starts. It needs no manifest, no patch and no observation.