letify 2.8.4__tar.gz → 2.8.5__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.
- {letify-2.8.4 → letify-2.8.5}/PKG-INFO +1 -1
- {letify-2.8.4 → letify-2.8.5}/docs/SPEC.md +5 -1
- {letify-2.8.4 → letify-2.8.5}/letify/__init__.py +1 -1
- {letify-2.8.4 → letify-2.8.5}/letify/providers/modal.py +75 -11
- {letify-2.8.4 → letify-2.8.5}/letify/providers/modal_adapter.py +1 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/channel.py +11 -2
- {letify-2.8.4 → letify-2.8.5}/letify-core/Cargo.lock +3 -3
- {letify-2.8.4 → letify-2.8.5}/letify-core/Cargo.toml +1 -1
- {letify-2.8.4 → letify-2.8.5}/letify-ext/package-lock.json +2 -2
- {letify-2.8.4 → letify-2.8.5}/letify-ext/package.json +1 -1
- {letify-2.8.4 → letify-2.8.5}/pyproject.toml +1 -1
- {letify-2.8.4 → letify-2.8.5}/tests/test_providers.py +103 -0
- {letify-2.8.4 → letify-2.8.5}/.github/workflows/ci.yml +0 -0
- {letify-2.8.4 → letify-2.8.5}/.github/workflows/docs.yml +0 -0
- {letify-2.8.4 → letify-2.8.5}/.github/workflows/publish.yml +0 -0
- {letify-2.8.4 → letify-2.8.5}/.gitignore +0 -0
- {letify-2.8.4 → letify-2.8.5}/.researchtree +0 -0
- {letify-2.8.4 → letify-2.8.5}/CLAUDE.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/LICENSE +0 -0
- {letify-2.8.4 → letify-2.8.5}/PROJECT.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/README.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/COMPONENT.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/INTENT.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/NETWORK.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/01-getting-started.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/02-providers.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/05-concurrency.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/06-cost.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/08-releasing.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/README.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.8.4 → letify-2.8.5}/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
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/guide/ko/README.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/locales/README_ko.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/docs/mkdocs.yml +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/README.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/cli.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/config/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/config/inventory.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/config/login.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/config/schema.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/config/secrets.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/config/writer.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/declare/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/declare/cache.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/declare/env.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/declare/function.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/declare/instance.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/declare/reduce.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/declare/replicas.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/errors.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/install.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/launcher.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/paths.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/protocol/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/protocol/codec.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/protocol/driver.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/protocol/handle.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/protocol/wire.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/protocol/worker.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/base.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/colab.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/colab_files.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/elice.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/kaggle.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/local.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/naming.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/shell.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/tunnel.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/providers/usage.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/capability.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/client.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/cuda.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/executor.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/frames.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/guard.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/tensor.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/trace.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/device/value.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/loader.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/remoting/probe.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/render.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/bootstrap.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/broker.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/lease.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/pool.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/session.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/runtime/telemetry.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/backends/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/backends/drive.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/backends/filesystem.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/backends/google_auth.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/backends/layout.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/backends/objects.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/cas.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/handoff.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/pathdata.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/sendorder.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/store/volume.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/stubs.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/tools.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/__init__.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/agent.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/announce.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/link.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/nat.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/pipeline.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/portmap.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/probe.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/quic.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/rendezvous.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/setup.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/sshopts.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify/transport/strategies.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/.gitignore +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/build.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/.vscodeignore +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/LICENSE +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/README.md +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/media/icon.svg +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/src/cli.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/src/extension.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/src/history.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/src/model.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/src/poll.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/src/render.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/test/model.test.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify-ext/tsconfig.json +0 -0
- {letify-2.8.4 → letify-2.8.5}/letify_providers.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/scripts/sync-main.sh +0 -0
- {letify-2.8.4 → letify-2.8.5}/scripts/version.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/conftest.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/fake_modal_adapter.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_cli.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_cli_output.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_colab_keepalive.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_config.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_core.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_data.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_data_streaming.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_declare.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_device.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_distributed.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_drive_handoff.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_drive_store.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_editor_sessions.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_elice.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_frames.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_install.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_kaggle.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_kaggle_link.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_link_log.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_notebook.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_parallel_placement.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_paths.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_portmap.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_protocol.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_quic.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_remoting.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_render.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_runtime.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_ship.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_store.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_strategies.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_stubs.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_transport.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_tunnel_setup.py +0 -0
- {letify-2.8.4 → letify-2.8.5}/tests/test_version.py +0 -0
|
@@ -777,7 +777,7 @@ The protocol is one JSON object per line. letify sends `{"id": <int>, "op": <nam
|
|
|
777
777
|
| Op | Fields | Value |
|
|
778
778
|
|---|---|---|
|
|
779
779
|
| `hello` | none | `{"modal": <installed Modal version>}` |
|
|
780
|
-
| `create` | `app`, `args`, `packages`, `gpu`, `timeout`, `idle_timeout`, `volumes`, `ports` | `{"sandbox": <id>}`. Runs `app` as an ephemeral app on first use, builds `debian_slim` with `packages` installed, and starts `args` in a sandbox. Each port in `ports` is exposed with Modal `encrypted_ports` |
|
|
780
|
+
| `create` | `app`, `args`, `packages`, `gpu`, `timeout`, `idle_timeout`, `memory`, `volumes`, `ports` | `{"sandbox": <id>}`. Runs `app` as an ephemeral app on first use, builds `debian_slim` with `packages` installed, and starts `args` in a sandbox. Each port in `ports` is exposed with Modal `encrypted_ports` |
|
|
781
781
|
| `tunnel` | `sandbox`, `port` | `{"host": <text>, "port": <int>, "tls": <bool>}`. The address that reaches `port` inside the sandbox, from `Sandbox.tunnels()`. `tls` is true when the connection has to be made with TLS, which an encrypted port needs |
|
|
782
782
|
| `write` | `sandbox`, `data` | `null`. `data` is base64 of at most 1 MiB. Writes the decoded bytes to the sandbox's standard input and drains it. Modal refuses a write that would buffer more than 2 MiB, so the channel splits a larger frame into `write` requests of 1 MiB, in order |
|
|
783
783
|
| `read_until` | `sandbox`, `prefixes` | `{"lines": [...], "eof": <bool>}`. The sandbox's stdout lines up to and including the first that starts with one of `prefixes`, or every line left when the stream ends |
|
|
@@ -812,8 +812,12 @@ Modal also bounds a sandbox that nothing terminates. `create` passes `idle_timeo
|
|
|
812
812
|
|
|
813
813
|
It passes `timeout`, the entry option `timeout`, as the sandbox's maximum lifetime, and the default is the longest Modal accepts, 86400 s, measured as accepted. It is not a shorter number letify picked, for the reason The declaration gives for `timeout` having no default: a deadline letify invented would end a two hour training run at whatever hour it guessed. The default was 3600 s, exactly one hour, and it ended two Modal sessions of a real training run at one hour each with nothing saying why, which is that sentence happening. An account that wants a hard cap sets `timeout` itself.
|
|
814
814
|
|
|
815
|
+
It passes `memory`, the entry option `memory_gib` converted to MiB, as the sandbox's memory request, and passes nothing when the account does not set it, so Modal's default applies. A training process whose RAM grows past what the sandbox gets is killed with SIGKILL, which the channel sees as a worker that stopped without replying; an account whose runs need more sets `memory_gib`, for example `memory_gib = 64`. A value that is not a positive number raises `ConfigError` naming `memory_gib`.
|
|
816
|
+
|
|
815
817
|
A sandbox that reaches that lifetime takes its worker with it, so the channel sees a worker that stopped without replying. Where the session's age is within a minute of the lifetime letify asked for, the failure says so instead of listing causes: `the Modal sandbox reached the <seconds> s lifetime letify asked for; raise timeout on the account to run longer`.
|
|
816
818
|
|
|
819
|
+
Otherwise a Modal worker that stopped without replying is followed by one `poll` of its sandbox from a second adapter process, waiting at most 30 s for the exit code, and the failure names what the poll found instead of listing causes: `the Modal sandbox exited with code <n>` for a sandbox that ended, with `, which is SIGKILL, most often the container running out of memory` added for 137, and `the Modal sandbox is still running, so the worker process died inside it` for one still running. A poll that fails or times out leaves the message as it was. The message is what tells an out-of-memory kill from a preemption, which a training run that died twice in one day could not.
|
|
820
|
+
|
|
817
821
|
### Modal data channel <!-- id: modal-data-channel -->
|
|
818
822
|
|
|
819
823
|
> Once the worker has said hello over the sandbox's standard input and output, the channel moves its frames to a TCP connection through a Modal encrypted port. Standard input and output stay the control path and the fallback.
|
|
@@ -31,7 +31,7 @@ import tempfile
|
|
|
31
31
|
import threading
|
|
32
32
|
import time
|
|
33
33
|
import weakref
|
|
34
|
-
from collections.abc import Mapping
|
|
34
|
+
from collections.abc import Callable, Mapping
|
|
35
35
|
from typing import IO, TYPE_CHECKING, Any
|
|
36
36
|
|
|
37
37
|
from ..config import ProviderConfig
|
|
@@ -133,6 +133,22 @@ def lifetime_detail(*, age_s: float, lifetime_s: int) -> str | None:
|
|
|
133
133
|
)
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def exit_cause(found: tuple[bool, int | None] | None) -> str | None:
|
|
137
|
+
"""What a poll of a sandbox whose worker stopped says, or None when it is unknown.
|
|
138
|
+
|
|
139
|
+
Spec "Modal": the exit code tells an out of memory kill from a preemption.
|
|
140
|
+
"""
|
|
141
|
+
if found is None:
|
|
142
|
+
return None
|
|
143
|
+
ended, code = found
|
|
144
|
+
if not ended:
|
|
145
|
+
return "the Modal sandbox is still running, so the worker process died inside it"
|
|
146
|
+
text = f"the Modal sandbox exited with code {code}"
|
|
147
|
+
if code == 137:
|
|
148
|
+
text += ", which is SIGKILL, most often the container running out of memory"
|
|
149
|
+
return text
|
|
150
|
+
|
|
151
|
+
|
|
136
152
|
class VolumePathMissing(RuntimeFailure):
|
|
137
153
|
"""The adapter answered that a volume path does not exist."""
|
|
138
154
|
|
|
@@ -419,6 +435,21 @@ class Modal(Provider):
|
|
|
419
435
|
value = self.config.option("idle_timeout", IDLE_TIMEOUT)
|
|
420
436
|
return max(1, int(value))
|
|
421
437
|
|
|
438
|
+
@property
|
|
439
|
+
def sandbox_memory_mib(self) -> int | None:
|
|
440
|
+
"""The sandbox's memory request in MiB from ``memory_gib``, or None for Modal's own.
|
|
441
|
+
|
|
442
|
+
Spec "Modal": a training process past what the sandbox gets is killed with SIGKILL.
|
|
443
|
+
"""
|
|
444
|
+
value = self.config.option("memory_gib")
|
|
445
|
+
if value is None:
|
|
446
|
+
return None
|
|
447
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or value <= 0:
|
|
448
|
+
raise ConfigError(
|
|
449
|
+
f"{self.alias}: memory_gib has to be a positive number of GiB, not {value!r}"
|
|
450
|
+
)
|
|
451
|
+
return int(value * 1024)
|
|
452
|
+
|
|
422
453
|
def adapter(self) -> Adapter:
|
|
423
454
|
"""The adapter acting as this account, started on its first request."""
|
|
424
455
|
if self._adapter is None or self._adapter.closed:
|
|
@@ -484,6 +515,7 @@ class Modal(Provider):
|
|
|
484
515
|
gpu=self.wire_name(runtime.instance) or None,
|
|
485
516
|
timeout=self.sandbox_timeout_s,
|
|
486
517
|
idle_timeout=self.sandbox_idle_timeout_s,
|
|
518
|
+
memory=self.sandbox_memory_mib,
|
|
487
519
|
ports=[] if data_port is None else [data_port],
|
|
488
520
|
)
|
|
489
521
|
sandbox = str(created["sandbox"])
|
|
@@ -491,17 +523,14 @@ class Modal(Provider):
|
|
|
491
523
|
return SandboxChannel(
|
|
492
524
|
adapter, sandbox, name=runtime.name, data_port=data_port, streams=streams,
|
|
493
525
|
lifetime_s=self.sandbox_timeout_s,
|
|
526
|
+
probe=lambda: self.poll_sandbox(sandbox),
|
|
494
527
|
)
|
|
495
528
|
|
|
496
|
-
def
|
|
497
|
-
"""Poll
|
|
529
|
+
def poll_sandbox(self, sandbox: str) -> tuple[bool, int | None] | None:
|
|
530
|
+
"""Poll a sandbox once from a second adapter: ``(ended, exit code)``, or None if unknown.
|
|
498
531
|
|
|
499
|
-
|
|
500
|
-
request, so a short-lived one asks. Any failure answers unknown.
|
|
532
|
+
A short-lived adapter asks, because the one carrying the call may be blocked.
|
|
501
533
|
"""
|
|
502
|
-
sandbox = self._sandboxes.get(runtime.name)
|
|
503
|
-
if sandbox is None:
|
|
504
|
-
return RuntimeState()
|
|
505
534
|
other = Adapter.for_account(self.alias)
|
|
506
535
|
# A poll that hangs ends the second adapter, which makes the request raise.
|
|
507
536
|
limit = threading.Timer(POLL_LIMIT, other.abort)
|
|
@@ -510,11 +539,26 @@ class Modal(Provider):
|
|
|
510
539
|
try:
|
|
511
540
|
code = other.request("poll", sandbox=sandbox)
|
|
512
541
|
except (LetifyError, ValueError, OSError):
|
|
513
|
-
return
|
|
542
|
+
return None
|
|
514
543
|
finally:
|
|
515
544
|
limit.cancel()
|
|
516
545
|
other.close()
|
|
517
|
-
|
|
546
|
+
return (code is not None, code)
|
|
547
|
+
|
|
548
|
+
def runtime_state(self, runtime: Runtime) -> RuntimeState:
|
|
549
|
+
"""Poll the sandbox from a second adapter process, never the one carrying the call.
|
|
550
|
+
|
|
551
|
+
Spec "Lifetime", the lease. The adapter carrying the call may be blocked in a
|
|
552
|
+
request, so a short-lived one asks. Any failure answers unknown.
|
|
553
|
+
"""
|
|
554
|
+
sandbox = self._sandboxes.get(runtime.name)
|
|
555
|
+
if sandbox is None:
|
|
556
|
+
return RuntimeState()
|
|
557
|
+
found = self.poll_sandbox(sandbox)
|
|
558
|
+
if found is None:
|
|
559
|
+
return RuntimeState()
|
|
560
|
+
ended, code = found
|
|
561
|
+
if not ended:
|
|
518
562
|
return RuntimeState("running")
|
|
519
563
|
return RuntimeState(
|
|
520
564
|
"ended", f"the Modal sandbox ended (exit code {code}) while the call was running"
|
|
@@ -686,8 +730,11 @@ class SandboxChannel(FramedChannel):
|
|
|
686
730
|
data_port: int | None = None,
|
|
687
731
|
streams: int = 1,
|
|
688
732
|
lifetime_s: int = 0,
|
|
733
|
+
probe: Callable[[], tuple[bool, int | None] | None] | None = None,
|
|
689
734
|
):
|
|
690
735
|
self.adapter = adapter
|
|
736
|
+
#: Polls the sandbox from a second adapter, see ``Modal.poll_sandbox``.
|
|
737
|
+
self.probe = probe
|
|
691
738
|
self.sandbox = sandbox
|
|
692
739
|
self.name = name
|
|
693
740
|
self.data_port = data_port
|
|
@@ -716,6 +763,7 @@ class SandboxChannel(FramedChannel):
|
|
|
716
763
|
wire.chunks_readinto(self._read_chunks),
|
|
717
764
|
self._emit,
|
|
718
765
|
death_detail=self._raw_text,
|
|
766
|
+
death_cause=self._exit_cause,
|
|
719
767
|
)
|
|
720
768
|
self._connection = self._stdio
|
|
721
769
|
self._send_worker()
|
|
@@ -730,6 +778,17 @@ class SandboxChannel(FramedChannel):
|
|
|
730
778
|
)
|
|
731
779
|
return f"{said}\n{text}" if said else text
|
|
732
780
|
|
|
781
|
+
def _exit_cause(self) -> str | None:
|
|
782
|
+
"""What one poll of the sandbox found, or None to keep the usual causes.
|
|
783
|
+
|
|
784
|
+
Spec "Modal": a lifetime death is already named by ``_raw_text``, so it is not polled.
|
|
785
|
+
"""
|
|
786
|
+
if self.probe is None or lifetime_detail(
|
|
787
|
+
age_s=time.monotonic() - self.opened_at, lifetime_s=self.lifetime_s
|
|
788
|
+
):
|
|
789
|
+
return None
|
|
790
|
+
return exit_cause(self.probe())
|
|
791
|
+
|
|
733
792
|
def _send_worker(self) -> None:
|
|
734
793
|
# After a reexec the data connection is gone, so the source goes back over stdio.
|
|
735
794
|
if self._connection is not self._stdio:
|
|
@@ -821,7 +880,12 @@ class SandboxChannel(FramedChannel):
|
|
|
821
880
|
)
|
|
822
881
|
carrier.timeout = DATA_CONNECT_TIMEOUT
|
|
823
882
|
connection = Connection(
|
|
824
|
-
self.name,
|
|
883
|
+
self.name,
|
|
884
|
+
carrier.send,
|
|
885
|
+
carrier.recv_into,
|
|
886
|
+
self._emit,
|
|
887
|
+
death_detail=self._raw_text,
|
|
888
|
+
death_cause=self._exit_cause,
|
|
825
889
|
)
|
|
826
890
|
# The connect timeout still applies, so a missing hello fails here.
|
|
827
891
|
connection.wait_hello()
|
|
@@ -109,6 +109,7 @@ class Adapter:
|
|
|
109
109
|
gpu=request.get("gpu") or None,
|
|
110
110
|
timeout=int(request["timeout"]),
|
|
111
111
|
idle_timeout=int(request["idle_timeout"]) if request.get("idle_timeout") else None,
|
|
112
|
+
memory=int(request["memory"]) if request.get("memory") else None,
|
|
112
113
|
volumes={
|
|
113
114
|
str(path): modal.Volume.from_name(str(name), create_if_missing=True)
|
|
114
115
|
for path, name in (request.get("volumes") or {}).items()
|
|
@@ -258,6 +258,7 @@ class Connection:
|
|
|
258
258
|
emit: Callable[[str, bytes], None],
|
|
259
259
|
*,
|
|
260
260
|
death_detail: Callable[[], str] = lambda: "",
|
|
261
|
+
death_cause: Callable[[], str | None] = lambda: None,
|
|
261
262
|
poll_fd: int | None = None,
|
|
262
263
|
):
|
|
263
264
|
self.name = name
|
|
@@ -276,6 +277,8 @@ class Connection:
|
|
|
276
277
|
self._receiver = wire.Receiver(readinto)
|
|
277
278
|
self._emit = emit
|
|
278
279
|
self._death_detail = death_detail
|
|
280
|
+
#: Names what ended the worker when the provider can tell, replacing the usual causes.
|
|
281
|
+
self._death_cause = death_cause
|
|
279
282
|
#: Guards ``_reading`` and wakes waiters when a reply, a hello or a failure arrives.
|
|
280
283
|
self._turn = threading.Condition()
|
|
281
284
|
self._reading = False
|
|
@@ -383,10 +386,16 @@ class Connection:
|
|
|
383
386
|
return
|
|
384
387
|
if message is None:
|
|
385
388
|
detail = self._death_detail()
|
|
389
|
+
cause = self._death_cause()
|
|
390
|
+
causes = (
|
|
391
|
+
f": {cause}."
|
|
392
|
+
if cause
|
|
393
|
+
else ". The usual causes are an out of memory kill, a preempted session, "
|
|
394
|
+
"or a crash below Python."
|
|
395
|
+
)
|
|
386
396
|
message = (
|
|
387
397
|
f"{self.name}: the worker stopped without replying, so the process died "
|
|
388
|
-
f"before it finished
|
|
389
|
-
f"preempted session, or a crash below Python.\n--- last remote output ---\n"
|
|
398
|
+
f"before it finished{causes}\n--- last remote output ---\n"
|
|
390
399
|
f"{self._tail.text()[-2000:]}{detail[-2000:]}"
|
|
391
400
|
)
|
|
392
401
|
with self._turn:
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.8.
|
|
141
|
+
version = "2.8.5"
|
|
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.8.
|
|
149
|
+
version = "2.8.5"
|
|
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.8.
|
|
167
|
+
version = "2.8.5"
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "libc"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.8.
|
|
9
|
+
"version": "2.8.5",
|
|
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.8.
|
|
5
|
+
"version": "2.8.5",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -2368,6 +2368,39 @@ def test_a_modal_sandbox_is_created_with_a_lifetime_and_an_idle_limit(
|
|
|
2368
2368
|
provider.stop(runtime)
|
|
2369
2369
|
|
|
2370
2370
|
|
|
2371
|
+
def test_a_modal_sandbox_requests_the_memory_the_account_sets(isolated_home, fake_modal) -> None:
|
|
2372
|
+
# Spec "Modal": memory_gib becomes the sandbox's memory request in MiB.
|
|
2373
|
+
provider = provider_of(Modal, "m", memory_gib=64)
|
|
2374
|
+
runtime = modal_runtime(provider)
|
|
2375
|
+
provider.open_channel(runtime)
|
|
2376
|
+
try:
|
|
2377
|
+
[created] = fake_modal.requests("create")
|
|
2378
|
+
assert created["memory"] == 64 * 1024
|
|
2379
|
+
finally:
|
|
2380
|
+
provider.stop(runtime)
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def test_a_modal_sandbox_requests_no_memory_when_the_account_sets_none(
|
|
2384
|
+
isolated_home, fake_modal
|
|
2385
|
+
) -> None:
|
|
2386
|
+
provider = provider_of(Modal, "m")
|
|
2387
|
+
runtime = modal_runtime(provider)
|
|
2388
|
+
provider.open_channel(runtime)
|
|
2389
|
+
try:
|
|
2390
|
+
[created] = fake_modal.requests("create")
|
|
2391
|
+
assert created.get("memory") is None
|
|
2392
|
+
finally:
|
|
2393
|
+
provider.stop(runtime)
|
|
2394
|
+
|
|
2395
|
+
|
|
2396
|
+
@pytest.mark.parametrize("value", [0, -2, "lots", True])
|
|
2397
|
+
def test_a_memory_gib_that_is_not_a_positive_number_is_refused(config_file, value) -> None:
|
|
2398
|
+
from letify.errors import ConfigError
|
|
2399
|
+
|
|
2400
|
+
with pytest.raises(ConfigError, match="memory_gib"):
|
|
2401
|
+
_ = provider_of(Modal, "lab", memory_gib=value).sandbox_memory_mib
|
|
2402
|
+
|
|
2403
|
+
|
|
2371
2404
|
# -- Modal data channel ----------------------------------------------------------
|
|
2372
2405
|
|
|
2373
2406
|
|
|
@@ -3205,3 +3238,73 @@ def test_stopping_a_colab_orphan_unassigns_its_endpoint(
|
|
|
3205
3238
|
assert provider.stop_orphan("gpu-a") is True
|
|
3206
3239
|
assert cli.unassigned == ["gpu-a"]
|
|
3207
3240
|
assert provider.orphans() == []
|
|
3241
|
+
|
|
3242
|
+
|
|
3243
|
+
# -- Modal: a worker that stopped names what the sandbox did (spec "Modal") -----------
|
|
3244
|
+
|
|
3245
|
+
|
|
3246
|
+
def _died_modal_call(fake_modal, provider, *, ended: int | None) -> str:
|
|
3247
|
+
"""Kill the worker behind a Modal channel, make a call, and return the failure text."""
|
|
3248
|
+
import os
|
|
3249
|
+
import signal
|
|
3250
|
+
import time
|
|
3251
|
+
|
|
3252
|
+
from letify.errors import ProtocolError
|
|
3253
|
+
|
|
3254
|
+
runtime = modal_runtime(provider)
|
|
3255
|
+
channel = provider.open_channel(runtime)
|
|
3256
|
+
channel.start()
|
|
3257
|
+
[pid] = fake_modal.sandbox_pids()
|
|
3258
|
+
if ended is not None:
|
|
3259
|
+
fake_modal.end_sandbox("sb-1", ended)
|
|
3260
|
+
os.kill(pid, signal.SIGKILL)
|
|
3261
|
+
try:
|
|
3262
|
+
with pytest.raises(ProtocolError) as caught:
|
|
3263
|
+
channel.call(time.sleep, (600,), {}, timeout=20)
|
|
3264
|
+
finally:
|
|
3265
|
+
channel.close()
|
|
3266
|
+
provider.stop(runtime)
|
|
3267
|
+
return str(caught.value)
|
|
3268
|
+
|
|
3269
|
+
|
|
3270
|
+
def test_a_worker_killed_with_exit_137_names_the_out_of_memory_kill(
|
|
3271
|
+
isolated_home, fake_modal
|
|
3272
|
+
) -> None:
|
|
3273
|
+
text = _died_modal_call(fake_modal, provider_of(Modal, "m"), ended=137)
|
|
3274
|
+
assert "the Modal sandbox exited with code 137, which is SIGKILL" in text
|
|
3275
|
+
assert "most often the container running out of memory" in text
|
|
3276
|
+
assert "The usual causes" not in text
|
|
3277
|
+
|
|
3278
|
+
|
|
3279
|
+
def test_a_worker_whose_sandbox_exited_with_1_names_the_code_only(
|
|
3280
|
+
isolated_home, fake_modal
|
|
3281
|
+
) -> None:
|
|
3282
|
+
text = _died_modal_call(fake_modal, provider_of(Modal, "m"), ended=1)
|
|
3283
|
+
assert "the Modal sandbox exited with code 1" in text
|
|
3284
|
+
assert "SIGKILL" not in text
|
|
3285
|
+
assert "The usual causes" not in text
|
|
3286
|
+
|
|
3287
|
+
|
|
3288
|
+
def test_a_worker_that_died_inside_a_running_sandbox_says_so(isolated_home, fake_modal) -> None:
|
|
3289
|
+
text = _died_modal_call(fake_modal, provider_of(Modal, "m"), ended=None)
|
|
3290
|
+
assert "the Modal sandbox is still running, so the worker process died inside it" in text
|
|
3291
|
+
assert "The usual causes" not in text
|
|
3292
|
+
|
|
3293
|
+
|
|
3294
|
+
def test_a_failed_poll_leaves_the_worker_death_message_as_it_was(
|
|
3295
|
+
isolated_home, fake_modal
|
|
3296
|
+
) -> None:
|
|
3297
|
+
fake_modal.fail("poll")
|
|
3298
|
+
text = _died_modal_call(fake_modal, provider_of(Modal, "m"), ended=137)
|
|
3299
|
+
assert "The usual causes are an out of memory kill" in text
|
|
3300
|
+
assert "the Modal sandbox exited" not in text
|
|
3301
|
+
|
|
3302
|
+
|
|
3303
|
+
def test_a_sandbox_at_its_lifetime_keeps_the_lifetime_message_without_a_poll(
|
|
3304
|
+
isolated_home, fake_modal
|
|
3305
|
+
) -> None:
|
|
3306
|
+
provider = provider_of(Modal, "m", timeout=1)
|
|
3307
|
+
text = _died_modal_call(fake_modal, provider, ended=137)
|
|
3308
|
+
assert "lifetime letify asked for" in text
|
|
3309
|
+
assert "exited with code" not in text
|
|
3310
|
+
assert fake_modal.requests("poll") == []
|
|
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
|
{letify-2.8.4 → letify-2.8.5}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-2.8.4 → letify-2.8.5}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-2.8.4 → letify-2.8.5}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-2.8.4 → letify-2.8.5}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md"
RENAMED
|
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
|