letify 2.4.6__tar.gz → 2.4.7__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 (202) hide show
  1. {letify-2.4.6 → letify-2.4.7}/PKG-INFO +1 -1
  2. {letify-2.4.6 → letify-2.4.7}/letify/__init__.py +1 -1
  3. {letify-2.4.6 → letify-2.4.7}/letify/providers/kaggle.py +22 -4
  4. {letify-2.4.6 → letify-2.4.7}/letify/runtime/bootstrap.py +8 -2
  5. {letify-2.4.6 → letify-2.4.7}/letify/runtime/session.py +16 -6
  6. {letify-2.4.6 → letify-2.4.7}/letify-core/Cargo.lock +3 -3
  7. {letify-2.4.6 → letify-2.4.7}/letify-core/Cargo.toml +1 -1
  8. {letify-2.4.6 → letify-2.4.7}/letify-ext/package-lock.json +2 -2
  9. {letify-2.4.6 → letify-2.4.7}/letify-ext/package.json +1 -1
  10. {letify-2.4.6 → letify-2.4.7}/pyproject.toml +1 -1
  11. {letify-2.4.6 → letify-2.4.7}/tests/test_kaggle.py +38 -3
  12. {letify-2.4.6 → letify-2.4.7}/tests/test_kaggle_link.py +41 -0
  13. {letify-2.4.6 → letify-2.4.7}/.github/workflows/ci.yml +0 -0
  14. {letify-2.4.6 → letify-2.4.7}/.github/workflows/docs.yml +0 -0
  15. {letify-2.4.6 → letify-2.4.7}/.github/workflows/publish.yml +0 -0
  16. {letify-2.4.6 → letify-2.4.7}/.gitignore +0 -0
  17. {letify-2.4.6 → letify-2.4.7}/.researchtree +0 -0
  18. {letify-2.4.6 → letify-2.4.7}/LICENSE +0 -0
  19. {letify-2.4.6 → letify-2.4.7}/README.md +0 -0
  20. {letify-2.4.6 → letify-2.4.7}/docs/guide/01-getting-started.md +0 -0
  21. {letify-2.4.6 → letify-2.4.7}/docs/guide/02-providers.md +0 -0
  22. {letify-2.4.6 → letify-2.4.7}/docs/guide/03-execution-modes.md +0 -0
  23. {letify-2.4.6 → letify-2.4.7}/docs/guide/04-environments-and-data.md +0 -0
  24. {letify-2.4.6 → letify-2.4.7}/docs/guide/05-concurrency.md +0 -0
  25. {letify-2.4.6 → letify-2.4.7}/docs/guide/06-cost.md +0 -0
  26. {letify-2.4.6 → letify-2.4.7}/docs/guide/07-troubleshooting.md +0 -0
  27. {letify-2.4.6 → letify-2.4.7}/docs/guide/08-releasing.md +0 -0
  28. {letify-2.4.6 → letify-2.4.7}/docs/guide/README.md +0 -0
  29. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  30. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  31. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  32. {letify-2.4.6 → letify-2.4.7}/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
  33. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  34. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  35. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  36. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  37. {letify-2.4.6 → letify-2.4.7}/docs/guide/ko/README.md +0 -0
  38. {letify-2.4.6 → letify-2.4.7}/docs/locales/README_ko.md +0 -0
  39. {letify-2.4.6 → letify-2.4.7}/docs/mkdocs.yml +0 -0
  40. {letify-2.4.6 → letify-2.4.7}/examples/README.md +0 -0
  41. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/.letify.example +0 -0
  42. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/00_smoke.py +0 -0
  43. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/01_sweep.py +0 -0
  44. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/02_resume.py +0 -0
  45. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/03_watch.py +0 -0
  46. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/README.md +0 -0
  47. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/README_ko.md +0 -0
  48. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/common.py +0 -0
  49. {letify-2.4.6 → letify-2.4.7}/examples/nvfp4-lora/recipe.py +0 -0
  50. {letify-2.4.6 → letify-2.4.7}/letify/cli.py +0 -0
  51. {letify-2.4.6 → letify-2.4.7}/letify/config/__init__.py +0 -0
  52. {letify-2.4.6 → letify-2.4.7}/letify/config/inventory.py +0 -0
  53. {letify-2.4.6 → letify-2.4.7}/letify/config/login.py +0 -0
  54. {letify-2.4.6 → letify-2.4.7}/letify/config/schema.py +0 -0
  55. {letify-2.4.6 → letify-2.4.7}/letify/config/secrets.py +0 -0
  56. {letify-2.4.6 → letify-2.4.7}/letify/config/writer.py +0 -0
  57. {letify-2.4.6 → letify-2.4.7}/letify/declare/__init__.py +0 -0
  58. {letify-2.4.6 → letify-2.4.7}/letify/declare/cache.py +0 -0
  59. {letify-2.4.6 → letify-2.4.7}/letify/declare/env.py +0 -0
  60. {letify-2.4.6 → letify-2.4.7}/letify/declare/function.py +0 -0
  61. {letify-2.4.6 → letify-2.4.7}/letify/declare/instance.py +0 -0
  62. {letify-2.4.6 → letify-2.4.7}/letify/declare/replicas.py +0 -0
  63. {letify-2.4.6 → letify-2.4.7}/letify/errors.py +0 -0
  64. {letify-2.4.6 → letify-2.4.7}/letify/install.py +0 -0
  65. {letify-2.4.6 → letify-2.4.7}/letify/launcher.py +0 -0
  66. {letify-2.4.6 → letify-2.4.7}/letify/paths.py +0 -0
  67. {letify-2.4.6 → letify-2.4.7}/letify/protocol/__init__.py +0 -0
  68. {letify-2.4.6 → letify-2.4.7}/letify/protocol/codec.py +0 -0
  69. {letify-2.4.6 → letify-2.4.7}/letify/protocol/driver.py +0 -0
  70. {letify-2.4.6 → letify-2.4.7}/letify/protocol/handle.py +0 -0
  71. {letify-2.4.6 → letify-2.4.7}/letify/protocol/wire.py +0 -0
  72. {letify-2.4.6 → letify-2.4.7}/letify/protocol/worker.py +0 -0
  73. {letify-2.4.6 → letify-2.4.7}/letify/providers/__init__.py +0 -0
  74. {letify-2.4.6 → letify-2.4.7}/letify/providers/base.py +0 -0
  75. {letify-2.4.6 → letify-2.4.7}/letify/providers/colab.py +0 -0
  76. {letify-2.4.6 → letify-2.4.7}/letify/providers/colab_files.py +0 -0
  77. {letify-2.4.6 → letify-2.4.7}/letify/providers/colab_keepalive.py +0 -0
  78. {letify-2.4.6 → letify-2.4.7}/letify/providers/elice.py +0 -0
  79. {letify-2.4.6 → letify-2.4.7}/letify/providers/kaggle_adapter.py +0 -0
  80. {letify-2.4.6 → letify-2.4.7}/letify/providers/local.py +0 -0
  81. {letify-2.4.6 → letify-2.4.7}/letify/providers/modal.py +0 -0
  82. {letify-2.4.6 → letify-2.4.7}/letify/providers/modal_adapter.py +0 -0
  83. {letify-2.4.6 → letify-2.4.7}/letify/providers/naming.py +0 -0
  84. {letify-2.4.6 → letify-2.4.7}/letify/providers/shell.py +0 -0
  85. {letify-2.4.6 → letify-2.4.7}/letify/providers/tunnel.py +0 -0
  86. {letify-2.4.6 → letify-2.4.7}/letify/providers/usage.py +0 -0
  87. {letify-2.4.6 → letify-2.4.7}/letify/remoting/__init__.py +0 -0
  88. {letify-2.4.6 → letify-2.4.7}/letify/remoting/capability.py +0 -0
  89. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/__init__.py +0 -0
  90. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/client.py +0 -0
  91. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/cuda.py +0 -0
  92. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/executor.py +0 -0
  93. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/frames.py +0 -0
  94. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/guard.py +0 -0
  95. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/tensor.py +0 -0
  96. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/trace.py +0 -0
  97. {letify-2.4.6 → letify-2.4.7}/letify/remoting/device/value.py +0 -0
  98. {letify-2.4.6 → letify-2.4.7}/letify/remoting/loader.py +0 -0
  99. {letify-2.4.6 → letify-2.4.7}/letify/remoting/probe.py +0 -0
  100. {letify-2.4.6 → letify-2.4.7}/letify/render.py +0 -0
  101. {letify-2.4.6 → letify-2.4.7}/letify/runtime/__init__.py +0 -0
  102. {letify-2.4.6 → letify-2.4.7}/letify/runtime/channel.py +0 -0
  103. {letify-2.4.6 → letify-2.4.7}/letify/runtime/lease.py +0 -0
  104. {letify-2.4.6 → letify-2.4.7}/letify/runtime/pool.py +0 -0
  105. {letify-2.4.6 → letify-2.4.7}/letify/runtime/telemetry.py +0 -0
  106. {letify-2.4.6 → letify-2.4.7}/letify/store/__init__.py +0 -0
  107. {letify-2.4.6 → letify-2.4.7}/letify/store/backends/__init__.py +0 -0
  108. {letify-2.4.6 → letify-2.4.7}/letify/store/backends/filesystem.py +0 -0
  109. {letify-2.4.6 → letify-2.4.7}/letify/store/backends/google_auth.py +0 -0
  110. {letify-2.4.6 → letify-2.4.7}/letify/store/backends/layout.py +0 -0
  111. {letify-2.4.6 → letify-2.4.7}/letify/store/backends/objects.py +0 -0
  112. {letify-2.4.6 → letify-2.4.7}/letify/store/cas.py +0 -0
  113. {letify-2.4.6 → letify-2.4.7}/letify/store/pathdata.py +0 -0
  114. {letify-2.4.6 → letify-2.4.7}/letify/store/sendorder.py +0 -0
  115. {letify-2.4.6 → letify-2.4.7}/letify/store/volume.py +0 -0
  116. {letify-2.4.6 → letify-2.4.7}/letify/stubs.py +0 -0
  117. {letify-2.4.6 → letify-2.4.7}/letify/tools.py +0 -0
  118. {letify-2.4.6 → letify-2.4.7}/letify/transport/__init__.py +0 -0
  119. {letify-2.4.6 → letify-2.4.7}/letify/transport/agent.py +0 -0
  120. {letify-2.4.6 → letify-2.4.7}/letify/transport/announce.py +0 -0
  121. {letify-2.4.6 → letify-2.4.7}/letify/transport/link.py +0 -0
  122. {letify-2.4.6 → letify-2.4.7}/letify/transport/nat.py +0 -0
  123. {letify-2.4.6 → letify-2.4.7}/letify/transport/pipeline.py +0 -0
  124. {letify-2.4.6 → letify-2.4.7}/letify/transport/portmap.py +0 -0
  125. {letify-2.4.6 → letify-2.4.7}/letify/transport/probe.py +0 -0
  126. {letify-2.4.6 → letify-2.4.7}/letify/transport/quic.py +0 -0
  127. {letify-2.4.6 → letify-2.4.7}/letify/transport/rendezvous.py +0 -0
  128. {letify-2.4.6 → letify-2.4.7}/letify/transport/setup.py +0 -0
  129. {letify-2.4.6 → letify-2.4.7}/letify/transport/sshopts.py +0 -0
  130. {letify-2.4.6 → letify-2.4.7}/letify/transport/strategies.py +0 -0
  131. {letify-2.4.6 → letify-2.4.7}/letify-core/.gitignore +0 -0
  132. {letify-2.4.6 → letify-2.4.7}/letify-core/build.py +0 -0
  133. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-agent/Cargo.toml +0 -0
  134. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-agent/src/driver.rs +0 -0
  135. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-agent/src/main.rs +0 -0
  136. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-driver/Cargo.toml +0 -0
  137. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-driver/src/client.rs +0 -0
  138. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-driver/src/exports.rs +0 -0
  139. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-driver/src/image.rs +0 -0
  140. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-driver/src/lib.rs +0 -0
  141. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-driver/src/status.rs +0 -0
  142. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-driver/src/table.rs +0 -0
  143. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-quic/Cargo.toml +0 -0
  144. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-quic/src/main.rs +0 -0
  145. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-wire/Cargo.toml +0 -0
  146. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-wire/src/lib.rs +0 -0
  147. {letify-2.4.6 → letify-2.4.7}/letify-core/letify-wire/tests/throughput.rs +0 -0
  148. {letify-2.4.6 → letify-2.4.7}/letify-core/rust-toolchain.toml +0 -0
  149. {letify-2.4.6 → letify-2.4.7}/letify-ext/.vscodeignore +0 -0
  150. {letify-2.4.6 → letify-2.4.7}/letify-ext/LICENSE +0 -0
  151. {letify-2.4.6 → letify-2.4.7}/letify-ext/README.md +0 -0
  152. {letify-2.4.6 → letify-2.4.7}/letify-ext/media/icon.svg +0 -0
  153. {letify-2.4.6 → letify-2.4.7}/letify-ext/src/cli.ts +0 -0
  154. {letify-2.4.6 → letify-2.4.7}/letify-ext/src/extension.ts +0 -0
  155. {letify-2.4.6 → letify-2.4.7}/letify-ext/src/history.ts +0 -0
  156. {letify-2.4.6 → letify-2.4.7}/letify-ext/src/model.ts +0 -0
  157. {letify-2.4.6 → letify-2.4.7}/letify-ext/src/poll.ts +0 -0
  158. {letify-2.4.6 → letify-2.4.7}/letify-ext/src/render.ts +0 -0
  159. {letify-2.4.6 → letify-2.4.7}/letify-ext/test/cli.test.ts +0 -0
  160. {letify-2.4.6 → letify-2.4.7}/letify-ext/test/manifest.test.ts +0 -0
  161. {letify-2.4.6 → letify-2.4.7}/letify-ext/test/model.test.ts +0 -0
  162. {letify-2.4.6 → letify-2.4.7}/letify-ext/test/regressions.test.ts +0 -0
  163. {letify-2.4.6 → letify-2.4.7}/letify-ext/tsconfig.json +0 -0
  164. {letify-2.4.6 → letify-2.4.7}/letify_providers.py +0 -0
  165. {letify-2.4.6 → letify-2.4.7}/scripts/sync-main.sh +0 -0
  166. {letify-2.4.6 → letify-2.4.7}/scripts/version.py +0 -0
  167. {letify-2.4.6 → letify-2.4.7}/tests/conftest.py +0 -0
  168. {letify-2.4.6 → letify-2.4.7}/tests/fake_kaggle_adapter.py +0 -0
  169. {letify-2.4.6 → letify-2.4.7}/tests/fake_modal_adapter.py +0 -0
  170. {letify-2.4.6 → letify-2.4.7}/tests/test_cli.py +0 -0
  171. {letify-2.4.6 → letify-2.4.7}/tests/test_cli_output.py +0 -0
  172. {letify-2.4.6 → letify-2.4.7}/tests/test_colab_keepalive.py +0 -0
  173. {letify-2.4.6 → letify-2.4.7}/tests/test_config.py +0 -0
  174. {letify-2.4.6 → letify-2.4.7}/tests/test_core.py +0 -0
  175. {letify-2.4.6 → letify-2.4.7}/tests/test_data.py +0 -0
  176. {letify-2.4.6 → letify-2.4.7}/tests/test_data_streaming.py +0 -0
  177. {letify-2.4.6 → letify-2.4.7}/tests/test_declare.py +0 -0
  178. {letify-2.4.6 → letify-2.4.7}/tests/test_device.py +0 -0
  179. {letify-2.4.6 → letify-2.4.7}/tests/test_distributed.py +0 -0
  180. {letify-2.4.6 → letify-2.4.7}/tests/test_editor_sessions.py +0 -0
  181. {letify-2.4.6 → letify-2.4.7}/tests/test_elice.py +0 -0
  182. {letify-2.4.6 → letify-2.4.7}/tests/test_frames.py +0 -0
  183. {letify-2.4.6 → letify-2.4.7}/tests/test_install.py +0 -0
  184. {letify-2.4.6 → letify-2.4.7}/tests/test_kaggle_cookie.py +0 -0
  185. {letify-2.4.6 → letify-2.4.7}/tests/test_link_log.py +0 -0
  186. {letify-2.4.6 → letify-2.4.7}/tests/test_notebook.py +0 -0
  187. {letify-2.4.6 → letify-2.4.7}/tests/test_parallel_placement.py +0 -0
  188. {letify-2.4.6 → letify-2.4.7}/tests/test_paths.py +0 -0
  189. {letify-2.4.6 → letify-2.4.7}/tests/test_portmap.py +0 -0
  190. {letify-2.4.6 → letify-2.4.7}/tests/test_protocol.py +0 -0
  191. {letify-2.4.6 → letify-2.4.7}/tests/test_providers.py +0 -0
  192. {letify-2.4.6 → letify-2.4.7}/tests/test_quic.py +0 -0
  193. {letify-2.4.6 → letify-2.4.7}/tests/test_remoting.py +0 -0
  194. {letify-2.4.6 → letify-2.4.7}/tests/test_render.py +0 -0
  195. {letify-2.4.6 → letify-2.4.7}/tests/test_runtime.py +0 -0
  196. {letify-2.4.6 → letify-2.4.7}/tests/test_ship.py +0 -0
  197. {letify-2.4.6 → letify-2.4.7}/tests/test_store.py +0 -0
  198. {letify-2.4.6 → letify-2.4.7}/tests/test_strategies.py +0 -0
  199. {letify-2.4.6 → letify-2.4.7}/tests/test_stubs.py +0 -0
  200. {letify-2.4.6 → letify-2.4.7}/tests/test_transport.py +0 -0
  201. {letify-2.4.6 → letify-2.4.7}/tests/test_tunnel_setup.py +0 -0
  202. {letify-2.4.6 → letify-2.4.7}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.4.6
3
+ Version: 2.4.7
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
@@ -64,7 +64,7 @@ distributed: Final = Host.distributed
64
64
  # The enum class stays importable from letify.declare.instance for letify's own use.
65
65
  del Host
66
66
 
67
- __version__ = "2.4.6"
67
+ __version__ = "2.4.7"
68
68
 
69
69
 
70
70
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -1508,10 +1508,15 @@ class Kaggle(Provider):
1508
1508
  )
1509
1509
  return bridge
1510
1510
  self._links[name] = link
1511
- return PersistentChannel(
1512
- link.ssh_command(f"{self.remote_python} -u -c {_shlex.quote(BOOTSTRAP)}"),
1513
- name=name,
1514
- )
1511
+ # Spec "Kaggle runtimes": the kernel finds libcuda through LD_LIBRARY_PATH and a
1512
+ # worker started over SSH does not inherit it, so it is read through the bridge and
1513
+ # set in the command's environment. The loader reads it when the process starts, so
1514
+ # setting it from inside the worker would be too late.
1515
+ remote = f"{self.remote_python} -u -c {_shlex.quote(BOOTSTRAP)}"
1516
+ libraries = self.kernel_library_path(bridge)
1517
+ if libraries:
1518
+ remote = f"env LD_LIBRARY_PATH={_shlex.quote(libraries)} {remote}"
1519
+ return PersistentChannel(link.ssh_command(remote), name=name)
1515
1520
 
1516
1521
  @property
1517
1522
  def min_session_interval_s(self) -> float:
@@ -1538,6 +1543,19 @@ class Kaggle(Provider):
1538
1543
  value = self.config.option("transfer_connection_cost_s")
1539
1544
  return max(0.0, float(value)) if isinstance(value, (int, float)) else 10.0
1540
1545
 
1546
+ def kernel_library_path(self, bridge: Any) -> str:
1547
+ """The kernel's own ``LD_LIBRARY_PATH``, asked through the bridge, or empty.
1548
+
1549
+ Spec "Kaggle runtimes". Never raises: a session whose bridge cannot answer is one
1550
+ letify should still try, and the card check refuses it when CUDA cannot start.
1551
+ """
1552
+ source = "import os\n__letify_value__ = os.environ.get('LD_LIBRARY_PATH') or ''\n"
1553
+ try:
1554
+ value, _ = bridge.request({"op": "eval", "source": source}, timeout=60)
1555
+ except Exception:
1556
+ return ""
1557
+ return value.strip() if isinstance(value, str) else ""
1558
+
1541
1559
  def link(self, runtime: Runtime | None = None) -> Any:
1542
1560
  """The link carrying this runtime, or None when the bridge is carrying it.
1543
1561
 
@@ -150,15 +150,21 @@ CARD_SOURCE = (
150
150
  # works rather than standing in for it: a live session had the cards and the control
151
151
  # device and still could not start CUDA. ctypes needs no torch, which the session does
152
152
  # not have yet at this point. Spec "Kaggle".
153
- "__letify_cuda = -1\n"
153
+ # A CUresult is 0 or positive, so a sentinel of -1 would read as one. The load and
154
+ # the call are reported apart instead. Spec "Kaggle".
155
+ "__letify_libcuda = False\n"
156
+ "__letify_cuda = None\n"
154
157
  "try:\n"
155
158
  " import ctypes as _letify_ctypes\n"
156
- " __letify_cuda = _letify_ctypes.CDLL('libcuda.so.1').cuInit(0)\n"
159
+ " _letify_lib = _letify_ctypes.CDLL('libcuda.so.1')\n"
160
+ " __letify_libcuda = True\n"
161
+ " __letify_cuda = _letify_lib.cuInit(0)\n"
157
162
  "except Exception:\n"
158
163
  " pass\n"
159
164
  "__letify_value__ = {\n"
160
165
  " 'cards': __letify_cards,\n"
161
166
  " 'driver': os.path.exists('/dev/nvidiactl'),\n"
167
+ " 'libcuda': __letify_libcuda,\n"
162
168
  " 'cuinit': __letify_cuda,\n"
163
169
  "}\n"
164
170
  )
@@ -721,11 +721,12 @@ class Runtime:
721
721
  # Reading /proc is not worth failing a session over when it cannot be read.
722
722
  return
723
723
  if not isinstance(found, dict):
724
- found = {"cards": found, "driver": True, "cuinit": 0}
724
+ found = {"cards": found, "driver": True, "libcuda": True, "cuinit": 0}
725
725
  cards = [str(card) for card in found.get("cards") or ()]
726
726
  driver = bool(found.get("driver"))
727
- cuinit = found.get("cuinit", 0)
728
- started = cuinit == 0
727
+ libcuda = bool(found.get("libcuda", True))
728
+ cuinit = found.get("cuinit")
729
+ started = libcuda and cuinit == 0
729
730
  matched = [card for card in cards if fragment.casefold() in card.casefold()]
730
731
  if len(matched) == count and len(cards) == count and driver and started:
731
732
  return
@@ -738,11 +739,20 @@ class Runtime:
738
739
  f"{arrived}, and no usable driver: /dev/nvidiactl is not there, so CUDA "
739
740
  f"cannot start"
740
741
  )
742
+ elif cards and not libcuda:
743
+ # Spec "Kaggle": the userspace driver library is not there at all, which is a
744
+ # different fault from CUDA refusing to start, and reads as a GPU session on a
745
+ # CPU image rather than as a CUDA error.
746
+ arrived = (
747
+ f"{arrived}, and no libcuda.so.1 to load: the cards are attached and the "
748
+ f"userspace driver library is missing, so nothing on this session can use "
749
+ f"them"
750
+ )
741
751
  elif cards and not started:
742
752
  arrived = (
743
- f"{arrived}, and a driver that does not start: cuInit returned {cuinit}, "
744
- f"which is the call CUDA initialization makes, so nothing on this session "
745
- f"can use those cards"
753
+ f"{arrived}, and a driver that does not start: libcuda.so.1 loaded and "
754
+ f"cuInit returned {cuinit}, which is the call CUDA initialization makes, "
755
+ f"so nothing on this session can use those cards"
746
756
  )
747
757
  message = (
748
758
  f"{self.name}: {self.provider.alias} was asked for {count} x {asked} and the "
@@ -138,7 +138,7 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify-agent"
141
- version = "2.4.6"
141
+ version = "2.4.7"
142
142
  dependencies = [
143
143
  "letify-wire",
144
144
  "libloading",
@@ -146,7 +146,7 @@ dependencies = [
146
146
 
147
147
  [[package]]
148
148
  name = "letify-driver"
149
- version = "2.4.6"
149
+ version = "2.4.7"
150
150
  dependencies = [
151
151
  "letify-wire",
152
152
  ]
@@ -164,7 +164,7 @@ dependencies = [
164
164
 
165
165
  [[package]]
166
166
  name = "letify-wire"
167
- version = "2.4.6"
167
+ version = "2.4.7"
168
168
 
169
169
  [[package]]
170
170
  name = "libc"
@@ -8,7 +8,7 @@ resolver = "3"
8
8
  members = ["letify-wire", "letify-driver", "letify-agent", "letify-quic"]
9
9
 
10
10
  [workspace.package]
11
- version = "2.4.6"
11
+ version = "2.4.7"
12
12
  edition = "2024"
13
13
  license = "MIT"
14
14
  repository = "https://github.com/DarkPyonix/letify"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "letify-status",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.4.6",
9
+ "version": "2.4.7",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "devDependencies": {
12
12
  "@types/node": "^18.19.0",
@@ -2,7 +2,7 @@
2
2
  "name": "letify-status",
3
3
  "displayName": "letify Status",
4
4
  "description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
5
- "version": "2.4.6",
5
+ "version": "2.4.7",
6
6
  "publisher": "darkpyonix",
7
7
  "license": "SEE LICENSE IN LICENSE",
8
8
  "repository": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "letify"
3
- version = "2.4.6"
3
+ version = "2.4.7"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1489,7 +1489,7 @@ def test_a_session_with_the_cards_and_a_driver_passes() -> None:
1489
1489
  session.name = "letify-t4-ok"
1490
1490
  session.provider = provider
1491
1491
  session.instance = type("I", (), {"gpu": "T4", "accelerator": "T4"})()
1492
- answer = {"cards": ["Tesla T4", "Tesla T4"], "driver": True}
1492
+ answer = {"cards": ["Tesla T4", "Tesla T4"], "driver": True, "cuinit": 0}
1493
1493
  monkey = lambda self, source, timeout=None: answer # noqa: E731
1494
1494
  Runtime.eval, original = monkey, Runtime.eval
1495
1495
  try:
@@ -1512,7 +1512,9 @@ def test_a_session_whose_cuinit_fails_is_refused() -> None:
1512
1512
  session.name = "letify-t4-fc7e27"
1513
1513
  session.provider = provider
1514
1514
  session.instance = type("I", (), {"gpu": "T4", "accelerator": "T4"})()
1515
- answer = {"cards": ["Tesla T4", "Tesla T4"], "driver": True, "cuinit": 100}
1515
+ answer = {
1516
+ "cards": ["Tesla T4", "Tesla T4"], "driver": True, "libcuda": True, "cuinit": 100
1517
+ }
1516
1518
  monkey = lambda self, source, timeout=None: answer # noqa: E731
1517
1519
  Runtime.eval, original = monkey, Runtime.eval
1518
1520
  try:
@@ -1534,10 +1536,43 @@ def test_a_session_whose_cuinit_succeeds_passes() -> None:
1534
1536
  session.name = "letify-t4-ok"
1535
1537
  session.provider = provider
1536
1538
  session.instance = type("I", (), {"gpu": "T4", "accelerator": "T4"})()
1537
- answer = {"cards": ["Tesla T4", "Tesla T4"], "driver": True, "cuinit": 0}
1539
+ answer = {
1540
+ "cards": ["Tesla T4", "Tesla T4"], "driver": True, "libcuda": True, "cuinit": 0
1541
+ }
1538
1542
  monkey = lambda self, source, timeout=None: answer # noqa: E731
1539
1543
  Runtime.eval, original = monkey, Runtime.eval
1540
1544
  try:
1541
1545
  session.check_devices()
1542
1546
  finally:
1543
1547
  Runtime.eval = original
1548
+
1549
+
1550
+ def test_a_missing_libcuda_is_not_reported_as_a_cuda_error() -> None:
1551
+ """Spec "Kaggle": a CUresult is 0 or positive, so a sentinel would read as one.
1552
+
1553
+ A live refusal said `cuInit returned -1`, which is not a CUresult at all: it was the
1554
+ value for "libcuda.so.1 did not load", a different fault that reads as a GPU session
1555
+ on a CPU image.
1556
+ """
1557
+ from letify.errors import InsufficientDevices
1558
+ from letify.runtime.session import Runtime
1559
+
1560
+ provider = kaggle_provider()
1561
+ session = object.__new__(Runtime)
1562
+ session.name = "letify-t4-d3012a"
1563
+ session.provider = provider
1564
+ session.instance = type("I", (), {"gpu": "T4", "accelerator": "T4"})()
1565
+ answer = {
1566
+ "cards": ["Tesla T4", "Tesla T4"], "driver": True, "libcuda": False, "cuinit": None
1567
+ }
1568
+ monkey = lambda self, source, timeout=None: answer # noqa: E731
1569
+ Runtime.eval, original = monkey, Runtime.eval
1570
+ try:
1571
+ with pytest.raises(InsufficientDevices) as failure:
1572
+ session.check_devices()
1573
+ finally:
1574
+ Runtime.eval = original
1575
+ message = str(failure.value)
1576
+ assert "no libcuda.so.1 to load" in message
1577
+ assert "cuInit returned" not in message
1578
+ assert "-1" not in message
@@ -317,3 +317,44 @@ def test_kaggle_does_not_offer_the_retired_p100() -> None:
317
317
  message = str(failure.value)
318
318
  assert "2026-09-15" in message
319
319
  assert "T4" in message
320
+
321
+
322
+ def test_the_worker_carries_the_kernel_s_library_path(monkeypatch) -> None:
323
+ """Spec "Kaggle runtimes": the loader reads LD_LIBRARY_PATH when the process starts.
324
+
325
+ A GPU session has its cards, its /dev/nvidia* nodes and a libcuda.so.1 at
326
+ /usr/local/nvidia/lib64 that ldconfig does not list. The kernel finds it through that
327
+ variable; a worker started over SSH does not inherit it, which the kernel bridge never
328
+ suffered and the punched link introduced.
329
+ """
330
+ built: list[list[str]] = []
331
+ provider = kaggle()
332
+
333
+ class Bridge:
334
+ def request(self, message, timeout=None):
335
+ source = message.get("source", "")
336
+ if "LD_LIBRARY_PATH" in source:
337
+ return "/usr/local/nvidia/lib64:/usr/local/cuda/lib64", None
338
+ return "", None
339
+
340
+ class Link:
341
+ strategy = "tcp_punch"
342
+ rtt_ms = 120.0
343
+
344
+ def ssh_command(self, remote_command=None):
345
+ return ["ssh", "-p", "40000", "root@127.0.0.1", remote_command or ""]
346
+
347
+ class Fake:
348
+ def __init__(self, command, name=None, **kwargs):
349
+ built.append(command)
350
+ self.name = name
351
+
352
+ monkeypatch.setattr(provider, "link_over", lambda channel, name=None: Link())
353
+ monkeypatch.setattr("letify.runtime.channel.PersistentChannel", Fake)
354
+ provider.channel_over(Bridge(), name="k-1")
355
+
356
+ remote = built[0][-1]
357
+ assert "LD_LIBRARY_PATH=" in remote
358
+ assert "/usr/local/nvidia/lib64" in remote
359
+ # Before the interpreter, or the loader has already read the environment.
360
+ assert remote.index("LD_LIBRARY_PATH=") < remote.index("python3")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes