letify 2.6.3__tar.gz → 2.6.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.6.3 → letify-2.6.5}/PKG-INFO +1 -1
- {letify-2.6.3 → letify-2.6.5}/docs/SPEC.md +18 -2
- {letify-2.6.3 → letify-2.6.5}/letify/__init__.py +1 -1
- {letify-2.6.3 → letify-2.6.5}/letify/providers/modal.py +47 -4
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/session.py +33 -10
- {letify-2.6.3 → letify-2.6.5}/letify-core/Cargo.lock +3 -3
- {letify-2.6.3 → letify-2.6.5}/letify-core/Cargo.toml +1 -1
- {letify-2.6.3 → letify-2.6.5}/letify-ext/package-lock.json +2 -2
- {letify-2.6.3 → letify-2.6.5}/letify-ext/package.json +1 -1
- {letify-2.6.3 → letify-2.6.5}/pyproject.toml +1 -1
- {letify-2.6.3 → letify-2.6.5}/tests/test_providers.py +32 -1
- {letify-2.6.3 → letify-2.6.5}/tests/test_runtime.py +54 -0
- {letify-2.6.3 → letify-2.6.5}/.github/workflows/ci.yml +0 -0
- {letify-2.6.3 → letify-2.6.5}/.github/workflows/docs.yml +0 -0
- {letify-2.6.3 → letify-2.6.5}/.github/workflows/publish.yml +0 -0
- {letify-2.6.3 → letify-2.6.5}/.gitignore +0 -0
- {letify-2.6.3 → letify-2.6.5}/.researchtree +0 -0
- {letify-2.6.3 → letify-2.6.5}/CLAUDE.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/LICENSE +0 -0
- {letify-2.6.3 → letify-2.6.5}/PROJECT.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/README.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/COMPONENT.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/INTENT.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/NETWORK.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/01-getting-started.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/02-providers.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/05-concurrency.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/06-cost.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/08-releasing.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/README.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.6.3 → letify-2.6.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.6.3 → letify-2.6.5}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.6.3 → letify-2.6.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.6.3 → letify-2.6.5}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/guide/ko/README.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/locales/README_ko.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/docs/mkdocs.yml +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/README.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/cli.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/config/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/config/inventory.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/config/login.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/config/schema.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/config/secrets.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/config/writer.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/declare/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/declare/cache.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/declare/env.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/declare/function.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/declare/instance.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/declare/reduce.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/declare/replicas.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/errors.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/install.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/launcher.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/paths.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/protocol/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/protocol/codec.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/protocol/driver.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/protocol/handle.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/protocol/wire.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/protocol/worker.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/base.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/colab.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/colab_files.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/elice.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/kaggle.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/local.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/modal_adapter.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/naming.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/shell.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/tunnel.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/providers/usage.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/capability.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/client.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/cuda.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/executor.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/frames.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/guard.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/tensor.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/trace.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/device/value.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/loader.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/remoting/probe.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/render.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/bootstrap.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/broker.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/channel.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/lease.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/pool.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/runtime/telemetry.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/backends/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/backends/filesystem.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/backends/google_auth.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/backends/layout.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/backends/objects.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/cas.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/pathdata.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/sendorder.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/store/volume.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/stubs.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/tools.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/__init__.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/agent.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/announce.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/link.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/nat.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/pipeline.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/portmap.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/probe.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/quic.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/rendezvous.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/setup.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/sshopts.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify/transport/strategies.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/.gitignore +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/build.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/.vscodeignore +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/LICENSE +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/README.md +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/media/icon.svg +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/src/cli.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/src/extension.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/src/history.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/src/model.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/src/poll.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/src/render.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/test/model.test.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify-ext/tsconfig.json +0 -0
- {letify-2.6.3 → letify-2.6.5}/letify_providers.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/scripts/sync-main.sh +0 -0
- {letify-2.6.3 → letify-2.6.5}/scripts/version.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/conftest.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/fake_modal_adapter.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_cli.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_cli_output.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_colab_keepalive.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_config.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_core.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_data.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_data_streaming.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_declare.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_device.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_distributed.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_editor_sessions.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_elice.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_frames.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_install.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_kaggle.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_kaggle_link.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_link_log.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_notebook.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_parallel_placement.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_paths.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_portmap.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_protocol.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_quic.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_remoting.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_render.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_ship.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_store.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_strategies.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_stubs.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_transport.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_tunnel_setup.py +0 -0
- {letify-2.6.3 → letify-2.6.5}/tests/test_version.py +0 -0
|
@@ -807,7 +807,11 @@ A `read_until` blocks until the sandbox prints a line, and the adapter answers o
|
|
|
807
807
|
- `Modal.stop` calls `abort()` when the adapter is out of step or its lock is still held after 5 s. The call timeout watchdog calls it through `SandboxChannel._kill`, so a call past its `timeout` ends its sandbox too.
|
|
808
808
|
- The adapter's `terminate` for an id it did not create finds the sandbox with `modal.Sandbox.from_id` and terminates it. A sandbox that is already gone is not an error.
|
|
809
809
|
|
|
810
|
-
Modal also bounds a sandbox that nothing terminates. `create` passes `
|
|
810
|
+
Modal also bounds a sandbox that nothing terminates. `create` passes `idle_timeout`, the entry option `idle_timeout`, 600 s by default, after which Modal terminates a sandbox that is idle. That is the bound that stops an abandoned sandbox billing, because an abandoned one is idle by definition once its worker has no one to answer.
|
|
811
|
+
|
|
812
|
+
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.
|
|
813
|
+
|
|
814
|
+
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`.
|
|
811
815
|
|
|
812
816
|
### Modal data channel <!-- id: modal-data-channel -->
|
|
813
817
|
|
|
@@ -1059,6 +1063,16 @@ less time than the build did. The archive is smaller than the directory, so the
|
|
|
1059
1063
|
overstates the transfer and errs toward building, which is the cheaper of the two to get wrong. The
|
|
1060
1064
|
ref records the build seconds beside the archive.
|
|
1061
1065
|
|
|
1066
|
+
With no measured rate for the link, letify packs only when that directory is at most 1 GiB, and says
|
|
1067
|
+
why when it declines. Packing moves the archive as one payload that the worker and the client each
|
|
1068
|
+
hold whole, so a tree of several gigabytes is held twice in memory with nothing reporting progress:
|
|
1069
|
+
measured on a Modal A10, a 7.2 GB environment sat in that step for over 30 minutes while the session
|
|
1070
|
+
billed. Packing unconditionally was right while only an ephemeral provider packed, where the
|
|
1071
|
+
alternative was a certain full rebuild; a provider that keeps its workspace root and builds its venv
|
|
1072
|
+
elsewhere reaches the same branch, and there the alternative is a sync that may take two minutes.
|
|
1073
|
+
Making the pack stream rather than hold the archive whole is the fix that removes this limit, and
|
|
1074
|
+
until then the limit is what keeps the step from costing more than it saves.
|
|
1075
|
+
|
|
1062
1076
|
A later session restores only when the archive's size over the link measured for that session would
|
|
1063
1077
|
take less than the recorded build. Otherwise it syncs, and says why:
|
|
1064
1078
|
`letify: <runtime>: syncing rather than restoring, because 3.0 GB over 13.6 MiB/s is 210 s against a
|
|
@@ -1072,7 +1086,9 @@ the kind this rule exists to catch, and the session that syncs instead both reco
|
|
|
1072
1086
|
decides afresh whether the archive is worth keeping. The cost of that is one sync; the cost of
|
|
1073
1087
|
trusting it is the transfer this rule was measured to avoid.
|
|
1074
1088
|
|
|
1075
|
-
|
|
1089
|
+
Packing is said, because it is a step that can take minutes and had nothing reporting it:
|
|
1090
|
+
`letify: <runtime>: caching the environment, 2.1 GB`. Skipping it is said too, because it is a
|
|
1091
|
+
decision letify made rather than a thing that did not
|
|
1076
1092
|
happen: `letify: <runtime>: not caching the environment, because 9.4 GB over 13.6 MiB/s is 659 s
|
|
1077
1093
|
against a 34 s build`.
|
|
1078
1094
|
|
|
@@ -102,6 +102,26 @@ STOP_WAIT = 5.0
|
|
|
102
102
|
#: ``idle_timeout``.
|
|
103
103
|
IDLE_TIMEOUT = 600
|
|
104
104
|
|
|
105
|
+
#: The sandbox's maximum lifetime, the longest Modal accepts, measured as accepted.
|
|
106
|
+
#: Spec "Modal": not a shorter number letify picked, for the reason The declaration gives
|
|
107
|
+
#: for ``timeout`` having no default. The previous 3600 s ended two sessions of a real
|
|
108
|
+
#: training run at one hour each with nothing saying why.
|
|
109
|
+
SANDBOX_TIMEOUT = 86400
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def lifetime_detail(*, age_s: float, lifetime_s: int) -> str | None:
|
|
113
|
+
"""What to say when a sandbox died at the lifetime letify asked for, or None.
|
|
114
|
+
|
|
115
|
+
Spec "Modal": the channel otherwise reports a worker that stopped without replying and
|
|
116
|
+
lists causes, none of which is the deadline letify set itself.
|
|
117
|
+
"""
|
|
118
|
+
if lifetime_s <= 0 or age_s < lifetime_s - 60.0:
|
|
119
|
+
return None
|
|
120
|
+
return (
|
|
121
|
+
f"the Modal sandbox reached the {lifetime_s} s lifetime letify asked for; "
|
|
122
|
+
f"raise timeout on the account to run longer"
|
|
123
|
+
)
|
|
124
|
+
|
|
105
125
|
|
|
106
126
|
class VolumePathMissing(RuntimeFailure):
|
|
107
127
|
"""The adapter answered that a volume path does not exist."""
|
|
@@ -377,6 +397,18 @@ class Modal(Provider):
|
|
|
377
397
|
self._sandboxes: dict[str, str] = {}
|
|
378
398
|
self._adapter: Adapter | None = None
|
|
379
399
|
|
|
400
|
+
@property
|
|
401
|
+
def sandbox_timeout_s(self) -> int:
|
|
402
|
+
"""The sandbox's maximum lifetime, from the account or the longest Modal accepts."""
|
|
403
|
+
value = self.config.option("timeout", SANDBOX_TIMEOUT)
|
|
404
|
+
return max(1, int(value))
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def sandbox_idle_timeout_s(self) -> int:
|
|
408
|
+
"""Seconds a sandbox may be idle before Modal terminates it. Spec "Modal"."""
|
|
409
|
+
value = self.config.option("idle_timeout", IDLE_TIMEOUT)
|
|
410
|
+
return max(1, int(value))
|
|
411
|
+
|
|
380
412
|
def adapter(self) -> Adapter:
|
|
381
413
|
"""The adapter acting as this account, started on its first request."""
|
|
382
414
|
if self._adapter is None or self._adapter.closed:
|
|
@@ -440,14 +472,15 @@ class Modal(Provider):
|
|
|
440
472
|
args=["python3", "-u", "-c", BOOTSTRAP],
|
|
441
473
|
packages=list(WORKER_PACKAGES),
|
|
442
474
|
gpu=self.wire_name(runtime.instance) or None,
|
|
443
|
-
timeout=
|
|
444
|
-
idle_timeout=
|
|
475
|
+
timeout=self.sandbox_timeout_s,
|
|
476
|
+
idle_timeout=self.sandbox_idle_timeout_s,
|
|
445
477
|
ports=[] if data_port is None else [data_port],
|
|
446
478
|
)
|
|
447
479
|
sandbox = str(created["sandbox"])
|
|
448
480
|
self._sandboxes[runtime.name] = sandbox
|
|
449
481
|
return SandboxChannel(
|
|
450
|
-
adapter, sandbox, name=runtime.name, data_port=data_port, streams=streams
|
|
482
|
+
adapter, sandbox, name=runtime.name, data_port=data_port, streams=streams,
|
|
483
|
+
lifetime_s=self.sandbox_timeout_s,
|
|
451
484
|
)
|
|
452
485
|
|
|
453
486
|
def stop(self, runtime: Runtime) -> None:
|
|
@@ -598,11 +631,15 @@ class SandboxChannel(FramedChannel):
|
|
|
598
631
|
name: str,
|
|
599
632
|
data_port: int | None = None,
|
|
600
633
|
streams: int = 1,
|
|
634
|
+
lifetime_s: int = 0,
|
|
601
635
|
):
|
|
602
636
|
self.adapter = adapter
|
|
603
637
|
self.sandbox = sandbox
|
|
604
638
|
self.name = name
|
|
605
639
|
self.data_port = data_port
|
|
640
|
+
#: The sandbox's maximum lifetime, so a death at it is named rather than guessed.
|
|
641
|
+
self.lifetime_s = lifetime_s
|
|
642
|
+
self.opened_at = time.monotonic()
|
|
606
643
|
#: TCP connections the data channel carries its frames over.
|
|
607
644
|
self.streams = streams
|
|
608
645
|
self._sockets: list[socket.socket] = []
|
|
@@ -628,7 +665,13 @@ class SandboxChannel(FramedChannel):
|
|
|
628
665
|
self._await_ready()
|
|
629
666
|
|
|
630
667
|
def _raw_text(self) -> str:
|
|
631
|
-
|
|
668
|
+
text = bytes(self._raw[-2000:]).decode("utf-8", "replace")
|
|
669
|
+
# Spec "Modal": a sandbox that reached the lifetime letify asked for takes its
|
|
670
|
+
# worker with it, and the channel otherwise lists causes, none of them this one.
|
|
671
|
+
said = lifetime_detail(
|
|
672
|
+
age_s=time.monotonic() - self.opened_at, lifetime_s=self.lifetime_s
|
|
673
|
+
)
|
|
674
|
+
return f"{said}\n{text}" if said else text
|
|
632
675
|
|
|
633
676
|
def _send_worker(self) -> None:
|
|
634
677
|
# After a reexec the data connection is gone, so the source goes back over stdio.
|
|
@@ -47,6 +47,11 @@ if TYPE_CHECKING:
|
|
|
47
47
|
#: The total size of argument blob files kept under a persistent workspace root, 32 GiB.
|
|
48
48
|
BLOB_DISK_LIMIT = 32 * 1024**3
|
|
49
49
|
|
|
50
|
+
#: The largest project directory letify packs into an environment archive when it has no
|
|
51
|
+
#: measured rate to judge the transfer by. Spec "Materializing into a runtime": the pack
|
|
52
|
+
#: moves the archive as one payload the worker and the client each hold whole.
|
|
53
|
+
PACK_WITHOUT_A_RATE = 1024**3
|
|
54
|
+
|
|
50
55
|
|
|
51
56
|
def _against_the_build(size: int, rate: float, build_s: float) -> str:
|
|
52
57
|
"""The three numbers both cache decisions weigh, as they are reported.
|
|
@@ -185,6 +190,11 @@ class Runtime:
|
|
|
185
190
|
self.provider.env_root or self.workspace or self.provider.workspace_root, self.env
|
|
186
191
|
)
|
|
187
192
|
if self._worth_packing(root):
|
|
193
|
+
size = self._tree_size(root)
|
|
194
|
+
self._say(
|
|
195
|
+
"caching the environment"
|
|
196
|
+
+ (f", {size / 1_000_000_000:.1f} GB" if size else "")
|
|
197
|
+
)
|
|
188
198
|
self._environment_volumes()[0].cache_env_from(
|
|
189
199
|
self, self.env, root, platform=self.platform, build_s=self.env_build_s
|
|
190
200
|
)
|
|
@@ -687,25 +697,38 @@ class Runtime:
|
|
|
687
697
|
return None
|
|
688
698
|
return float(rate)
|
|
689
699
|
|
|
700
|
+
def _tree_size(self, root: str) -> int | None:
|
|
701
|
+
"""The byte size of the project directory on the runtime, or None when unknown."""
|
|
702
|
+
from . import bootstrap
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
size = self.eval(bootstrap.tree_size_source(root), timeout=120)
|
|
706
|
+
except Exception:
|
|
707
|
+
return None
|
|
708
|
+
return size if isinstance(size, int) and size > 0 else None
|
|
709
|
+
|
|
690
710
|
def _worth_packing(self, root: str) -> bool:
|
|
691
711
|
"""Whether moving the project directory over this session's link beats the build.
|
|
692
712
|
|
|
693
713
|
Spec "Materializing into a runtime": the directory is larger than the archive it
|
|
694
|
-
would become, so this overstates the transfer and errs toward building.
|
|
695
|
-
|
|
714
|
+
would become, so this overstates the transfer and errs toward building. With no
|
|
715
|
+
measured rate it packs only what letify is willing to hold in memory, because the
|
|
716
|
+
pack moves the archive as one payload the worker and the client each hold whole.
|
|
696
717
|
"""
|
|
697
718
|
if self.env_build_s is None:
|
|
698
719
|
return True
|
|
699
720
|
rate = self._measured_rate()
|
|
721
|
+
size = self._tree_size(root)
|
|
700
722
|
if rate is None:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
723
|
+
if size is None or size <= PACK_WITHOUT_A_RATE:
|
|
724
|
+
return True
|
|
725
|
+
self._say(
|
|
726
|
+
f"not caching the environment, because {size / 1_000_000_000:.1f} GB is over "
|
|
727
|
+
f"the {PACK_WITHOUT_A_RATE / 1024**3:.1f} GiB letify will hold in memory and "
|
|
728
|
+
f"this link was never measured"
|
|
729
|
+
)
|
|
730
|
+
return False
|
|
731
|
+
if size is None:
|
|
709
732
|
return True
|
|
710
733
|
if (size / rate) < self.env_build_s:
|
|
711
734
|
return True
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.6.
|
|
141
|
+
version = "2.6.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.6.
|
|
149
|
+
version = "2.6.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.6.
|
|
167
|
+
version = "2.6.5"
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "libc"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.6.
|
|
9
|
+
"version": "2.6.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.6.
|
|
5
|
+
"version": "2.6.5",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -2218,7 +2218,9 @@ def test_a_modal_sandbox_is_created_with_a_lifetime_and_an_idle_limit(
|
|
|
2218
2218
|
provider.open_channel(runtime)
|
|
2219
2219
|
try:
|
|
2220
2220
|
[created] = fake_modal.requests("create")
|
|
2221
|
-
|
|
2221
|
+
# Spec "Modal": the lifetime is the longest Modal accepts, not an hour letify
|
|
2222
|
+
# picked, because a deadline letify invented ends the user's work at its guess.
|
|
2223
|
+
assert created["timeout"] == 86400
|
|
2222
2224
|
assert created["idle_timeout"] == 600
|
|
2223
2225
|
finally:
|
|
2224
2226
|
provider.stop(runtime)
|
|
@@ -2910,3 +2912,32 @@ def test_colab_punch_rendezvous_completes_during_tailcat_installation(
|
|
|
2910
2912
|
release.set()
|
|
2911
2913
|
installing.join(2)
|
|
2912
2914
|
assert endpoints == [("fake", 22)]
|
|
2915
|
+
|
|
2916
|
+
|
|
2917
|
+
def test_a_modal_sandbox_is_not_given_an_hour_long_deadline(config_file) -> None:
|
|
2918
|
+
"""Spec "Modal": a deadline letify invented would end a two hour run at its guess.
|
|
2919
|
+
|
|
2920
|
+
The default was 3600 s, exactly one hour, and it ended two Modal sessions of a real
|
|
2921
|
+
training run at one hour each with nothing saying why. The declaration's own `timeout`
|
|
2922
|
+
has no default for this reason, and the sandbox's lifetime is the same question.
|
|
2923
|
+
"""
|
|
2924
|
+
from conftest import provider_of
|
|
2925
|
+
|
|
2926
|
+
from letify.providers import Modal
|
|
2927
|
+
|
|
2928
|
+
provider = provider_of(Modal, "lab")
|
|
2929
|
+
assert provider.sandbox_timeout_s == 86400, "letify picked a deadline again"
|
|
2930
|
+
# An idle sandbox is what has to be bounded, and that bound is unchanged.
|
|
2931
|
+
assert provider.sandbox_idle_timeout_s == 600
|
|
2932
|
+
# An account that wants a cap still sets one.
|
|
2933
|
+
assert provider_of(Modal, "lab", timeout=7200).sandbox_timeout_s == 7200
|
|
2934
|
+
|
|
2935
|
+
|
|
2936
|
+
def test_a_sandbox_that_reached_its_lifetime_says_so(config_file) -> None:
|
|
2937
|
+
"""Spec "Modal": the channel otherwise lists causes, none of them this one."""
|
|
2938
|
+
from letify.providers.modal import lifetime_detail
|
|
2939
|
+
|
|
2940
|
+
assert lifetime_detail(age_s=3599.5, lifetime_s=3600) is not None
|
|
2941
|
+
assert "3600" in lifetime_detail(age_s=3599.5, lifetime_s=3600)
|
|
2942
|
+
assert "raise timeout" in lifetime_detail(age_s=3601.0, lifetime_s=3600)
|
|
2943
|
+
assert lifetime_detail(age_s=120.0, lifetime_s=3600) is None
|
|
@@ -2593,3 +2593,57 @@ def test_a_persistent_provider_that_builds_its_venv_elsewhere_still_archives(
|
|
|
2593
2593
|
finally:
|
|
2594
2594
|
runtime.shutdown()
|
|
2595
2595
|
assert volume.cached_env(env, platform), "nothing was archived, so the next session resyncs"
|
|
2596
|
+
|
|
2597
|
+
|
|
2598
|
+
def test_a_large_environment_is_not_packed_when_the_link_was_never_measured(
|
|
2599
|
+
uv_project: Path, tmp_path: Path, monkeypatch, capsys
|
|
2600
|
+
) -> None:
|
|
2601
|
+
"""Spec "Materializing into a runtime": the pack holds the archive whole, twice.
|
|
2602
|
+
|
|
2603
|
+
2.6.3 made a provider that keeps its workspace root archive like an ephemeral one, and
|
|
2604
|
+
that branch packed unconditionally with no measured rate. On a Modal A10 a 7.2 GB
|
|
2605
|
+
environment sat in the pack for over 30 minutes, silently, while the session billed.
|
|
2606
|
+
"""
|
|
2607
|
+
from letify.runtime import session as sessionmod
|
|
2608
|
+
|
|
2609
|
+
provider = provider_of(PreparingLocal, "lab", persistent=False)
|
|
2610
|
+
volume = provider.volume(
|
|
2611
|
+
"cache", backend="filesystem", root=str(tmp_path / "store"), mount=str(tmp_path / "mount")
|
|
2612
|
+
)
|
|
2613
|
+
env = Env()
|
|
2614
|
+
instance = Instance(provider, gpu=None)._placed("remote")
|
|
2615
|
+
# No measured rate, as a Modal sandbox has none, and a tree over the limit.
|
|
2616
|
+
monkeypatch.setattr(sessionmod.Runtime, "_measured_rate", lambda self: None)
|
|
2617
|
+
monkeypatch.setattr(
|
|
2618
|
+
sessionmod.Runtime, "_tree_size", lambda self, root: 8 * 1024**3
|
|
2619
|
+
)
|
|
2620
|
+
runtime = provider.start(instance, env, name="lab-1", volumes=(volume,))
|
|
2621
|
+
try:
|
|
2622
|
+
platform = runtime.platform
|
|
2623
|
+
finally:
|
|
2624
|
+
runtime.shutdown()
|
|
2625
|
+
assert volume.cached_env(env, platform) is None
|
|
2626
|
+
err = capsys.readouterr().err
|
|
2627
|
+
assert "over the 1.0 GiB letify will hold in memory" in err, err
|
|
2628
|
+
|
|
2629
|
+
|
|
2630
|
+
def test_a_small_environment_is_still_packed_and_says_so(
|
|
2631
|
+
uv_project: Path, tmp_path: Path, monkeypatch, capsys
|
|
2632
|
+
) -> None:
|
|
2633
|
+
"""Spec "Materializing into a runtime": the pack can take minutes, so it is said."""
|
|
2634
|
+
from letify.runtime import session as sessionmod
|
|
2635
|
+
|
|
2636
|
+
provider = provider_of(PreparingLocal, "lab", persistent=False)
|
|
2637
|
+
volume = provider.volume(
|
|
2638
|
+
"cache", backend="filesystem", root=str(tmp_path / "store"), mount=str(tmp_path / "mount")
|
|
2639
|
+
)
|
|
2640
|
+
env = Env()
|
|
2641
|
+
instance = Instance(provider, gpu=None)._placed("remote")
|
|
2642
|
+
monkeypatch.setattr(sessionmod.Runtime, "_measured_rate", lambda self: None)
|
|
2643
|
+
runtime = provider.start(instance, env, name="lab-1", volumes=(volume,))
|
|
2644
|
+
try:
|
|
2645
|
+
platform = runtime.platform
|
|
2646
|
+
finally:
|
|
2647
|
+
runtime.shutdown()
|
|
2648
|
+
assert volume.cached_env(env, platform)
|
|
2649
|
+
assert "caching the environment" in capsys.readouterr().err
|
|
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.6.3 → letify-2.6.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.6.3 → letify-2.6.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.6.3 → letify-2.6.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.6.3 → letify-2.6.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
|
|
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
|