letify 2.6.0__tar.gz → 2.6.2__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.0 → letify-2.6.2}/PKG-INFO +1 -1
- {letify-2.6.0 → letify-2.6.2}/docs/SPEC.md +3 -1
- {letify-2.6.0 → letify-2.6.2}/letify/__init__.py +1 -1
- {letify-2.6.0 → letify-2.6.2}/letify/protocol/worker.py +37 -2
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/bootstrap.py +12 -3
- {letify-2.6.0 → letify-2.6.2}/letify-core/Cargo.lock +3 -3
- {letify-2.6.0 → letify-2.6.2}/letify-core/Cargo.toml +1 -1
- {letify-2.6.0 → letify-2.6.2}/letify-ext/package-lock.json +2 -2
- {letify-2.6.0 → letify-2.6.2}/letify-ext/package.json +1 -1
- {letify-2.6.0 → letify-2.6.2}/pyproject.toml +1 -1
- {letify-2.6.0 → letify-2.6.2}/tests/test_data_streaming.py +73 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_runtime.py +41 -0
- {letify-2.6.0 → letify-2.6.2}/.github/workflows/ci.yml +0 -0
- {letify-2.6.0 → letify-2.6.2}/.github/workflows/docs.yml +0 -0
- {letify-2.6.0 → letify-2.6.2}/.github/workflows/publish.yml +0 -0
- {letify-2.6.0 → letify-2.6.2}/.gitignore +0 -0
- {letify-2.6.0 → letify-2.6.2}/.researchtree +0 -0
- {letify-2.6.0 → letify-2.6.2}/CLAUDE.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/LICENSE +0 -0
- {letify-2.6.0 → letify-2.6.2}/PROJECT.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/README.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/COMPONENT.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/INTENT.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/NETWORK.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/01-getting-started.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/02-providers.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/05-concurrency.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/06-cost.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/08-releasing.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/README.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.6.0 → letify-2.6.2}/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.0 → letify-2.6.2}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.6.0 → letify-2.6.2}/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.0 → letify-2.6.2}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/guide/ko/README.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/locales/README_ko.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/docs/mkdocs.yml +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/README.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/cli.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/config/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/config/inventory.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/config/login.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/config/schema.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/config/secrets.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/config/writer.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/declare/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/declare/cache.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/declare/env.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/declare/function.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/declare/instance.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/declare/reduce.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/declare/replicas.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/errors.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/install.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/launcher.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/paths.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/protocol/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/protocol/codec.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/protocol/driver.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/protocol/handle.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/protocol/wire.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/base.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/colab.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/colab_files.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/elice.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/kaggle.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/local.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/modal.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/modal_adapter.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/naming.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/shell.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/tunnel.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/providers/usage.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/capability.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/client.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/cuda.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/executor.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/frames.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/guard.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/tensor.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/trace.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/device/value.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/loader.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/remoting/probe.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/render.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/broker.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/channel.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/lease.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/pool.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/session.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/runtime/telemetry.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/backends/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/backends/filesystem.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/backends/google_auth.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/backends/layout.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/backends/objects.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/cas.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/pathdata.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/sendorder.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/store/volume.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/stubs.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/tools.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/__init__.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/agent.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/announce.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/link.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/nat.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/pipeline.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/portmap.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/probe.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/quic.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/rendezvous.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/setup.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/sshopts.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify/transport/strategies.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/.gitignore +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/build.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/.vscodeignore +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/LICENSE +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/README.md +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/media/icon.svg +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/src/cli.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/src/extension.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/src/history.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/src/model.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/src/poll.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/src/render.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/test/model.test.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify-ext/tsconfig.json +0 -0
- {letify-2.6.0 → letify-2.6.2}/letify_providers.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/scripts/sync-main.sh +0 -0
- {letify-2.6.0 → letify-2.6.2}/scripts/version.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/conftest.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/fake_modal_adapter.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_cli.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_cli_output.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_colab_keepalive.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_config.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_core.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_data.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_declare.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_device.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_distributed.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_editor_sessions.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_elice.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_frames.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_install.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_kaggle.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_kaggle_link.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_link_log.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_notebook.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_parallel_placement.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_paths.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_portmap.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_protocol.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_providers.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_quic.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_remoting.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_render.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_ship.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_store.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_strategies.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_stubs.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_transport.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_tunnel_setup.py +0 -0
- {letify-2.6.0 → letify-2.6.2}/tests/test_version.py +0 -0
|
@@ -1261,7 +1261,7 @@ A `stat` cannot take that snapshot first, because it is on a data loader's hot p
|
|
|
1261
1261
|
|
|
1262
1262
|
#### When a blob does not arrive <!-- id: project-data-streaming-failures -->
|
|
1263
1263
|
|
|
1264
|
-
- **A blob that never arrives.** A `data_want`
|
|
1264
|
+
- **A blob that never arrives.** A `data_want` whose blob has not grown for `data_wait_timeout` seconds on the account, 600 by default, fails that open with `RuntimeFailure` naming the relative path, the digest, and how much of the blob had arrived. The call is not killed, because the body may handle it; the failure is infrastructure, so a retry of the call is allowed. The timeout counts time without progress rather than total time, because a blob large enough to need longer than the timeout at the link's rate would otherwise fail however healthy the transfer was: 11.3 GB needs 19.3 MiB/s to land inside 600 s, and a Modal path measured at 45.3 MiB/s for one blob alone did not reach that behind 37 other files. The worker reads the size of the partial it is writing, so progress is the bytes on its own disk and not an estimate.
|
|
1265
1265
|
- **A sender that stops.** The background sender raising leaves every blob still queued unsendable. letify says so once, `letify: data sending stopped after <n> of <n> files, <n> not sent: <error>`, and tells the runtime, which wakes every reader waiting on one of those blobs with `RuntimeFailure` naming the file and that reason, and fails a reader that arrives later at once. Waiting it out instead costs `data_wait_timeout` once for every blob still queued, which is how a hundred queued shards become an hour of silence.
|
|
1266
1266
|
- **A client that disconnects mid stream.** The session ends as any lost channel ends it. Every blocked reader is woken with `RuntimeFailure`, the call ends with it, and no partial file is ever placed or read. A `.partial` file is removed when the worker next starts.
|
|
1267
1267
|
- **A call that finishes before the background transfer.** The transfer is cancelled, both the channel sender and any bucket upload not yet started. A blob already committed to the cache stays and counts as held for the next call, and a partial one is removed. The write-back and the `data_evict` request follow the cancellation, in that order.
|
|
@@ -1498,6 +1498,8 @@ For an ephemeral provider that archives its environment, the default managed Pyt
|
|
|
1498
1498
|
|
|
1499
1499
|
Otherwise the sync step sets `UV_CACHE_DIR`, `UV_PYTHON_INSTALL_DIR` and `UV_TOOL_DIR` under `<workspace root>` for `uv sync` and `uv pip install`, unconditionally: not only when the provider's `persistence` is `persistent`, and not only when it sets no `env_root`. An `Env.vars` entry naming one of the three wins over this rule for that variable.
|
|
1500
1500
|
|
|
1501
|
+
`UV_PYTHON_INSTALL_DIR` is the exception: it follows the project directory whenever the provider sets an `env_root`, because the interpreter belongs with the `.venv` it serves and a workspace root is not always a filesystem one can be unpacked on. Modal's workspace root is a Modal volume, measured as supporting no hard link at all: a link inside it fails with `EPERM` and a link from it to the sandbox disk with `EXDEV`. So `uv sync --python 3.13` on a sandbox whose image carries 3.12 downloaded a managed interpreter into the volume and failed with `error: Operation not permitted (os error 1)`, which named neither the directory nor the reason. The archive case already placed it there, for its own reason, so this makes the two agree.
|
|
1502
|
+
|
|
1501
1503
|
On a runtime with a persistent workspace root the cache outlives a container rebuild along with the projects built from it. On an ephemeral provider the cache is filled once per runtime wherever it lives and discarded with it, but it is still written under the workspace root rather than the home directory, because the workspace root may be the only writable path.
|
|
1502
1504
|
|
|
1503
1505
|
letify never deletes from the cache. The first sync on a runtime fills `<workspace root>/uv-cache` once, and a cache uv already had elsewhere is left in place. `uv cache prune` run with the same `UV_CACHE_DIR` removes entries no lock file needs any more; a file still hard linked from a `.venv` keeps its disk blocks until that `.venv` is removed as well.
|
|
@@ -1269,6 +1269,31 @@ def _op_data_failed(request):
|
|
|
1269
1269
|
return {"ok": True, "value": True}
|
|
1270
1270
|
|
|
1271
1271
|
|
|
1272
|
+
def _data_received(blobs, digest):
|
|
1273
|
+
"""Bytes of this blob that are on the runtime's disk, however they are arriving.
|
|
1274
|
+
|
|
1275
|
+
Spec "When a blob does not arrive": the wait counts time without progress, and this is
|
|
1276
|
+
what progress means. Reads the partial a single put writes and the one a parallel put
|
|
1277
|
+
fills, so neither shape looks stalled while it grows.
|
|
1278
|
+
"""
|
|
1279
|
+
total = 0
|
|
1280
|
+
final = _data_file(blobs, digest)
|
|
1281
|
+
directory = os.path.dirname(final)
|
|
1282
|
+
name = os.path.basename(final)
|
|
1283
|
+
try:
|
|
1284
|
+
entries = os.listdir(directory)
|
|
1285
|
+
except OSError:
|
|
1286
|
+
return 0
|
|
1287
|
+
for entry in entries:
|
|
1288
|
+
if entry != name and not entry.startswith(name + "."):
|
|
1289
|
+
continue
|
|
1290
|
+
try:
|
|
1291
|
+
total = max(total, os.stat(os.path.join(directory, entry)).st_size)
|
|
1292
|
+
except OSError:
|
|
1293
|
+
continue
|
|
1294
|
+
return total
|
|
1295
|
+
|
|
1296
|
+
|
|
1272
1297
|
def _data_want(digests, blocking=False):
|
|
1273
1298
|
"""Ask the local process for these blobs, in this order. Nothing replies to it."""
|
|
1274
1299
|
if not digests:
|
|
@@ -1353,6 +1378,7 @@ def _data_wait(path):
|
|
|
1353
1378
|
state["first"] = os.path.basename(path)
|
|
1354
1379
|
deadline = started + state["wait"]
|
|
1355
1380
|
_data_want([digest], True)
|
|
1381
|
+
received = _data_received(state["blobs"], digest)
|
|
1356
1382
|
try:
|
|
1357
1383
|
with _DATA_READY:
|
|
1358
1384
|
while path in state["pending"] and state["dir"] in _DATA_CALLS:
|
|
@@ -1361,11 +1387,20 @@ def _data_wait(path):
|
|
|
1361
1387
|
"%s was never sent: %s"
|
|
1362
1388
|
% (os.path.basename(path), state["failed"])
|
|
1363
1389
|
)
|
|
1390
|
+
# Spec "When a blob does not arrive": the wait counts time without
|
|
1391
|
+
# progress, because a blob needing longer than the timeout at the link's
|
|
1392
|
+
# rate would otherwise fail however healthy its transfer was.
|
|
1393
|
+
landed = _data_received(state["blobs"], digest)
|
|
1394
|
+
if landed > received:
|
|
1395
|
+
received = landed
|
|
1396
|
+
deadline = time.monotonic() + state["wait"]
|
|
1364
1397
|
left = deadline - time.monotonic()
|
|
1365
1398
|
if left <= 0:
|
|
1366
1399
|
raise _DataFailure(
|
|
1367
|
-
"the file blob %s of %s did not arrive within %.0f s"
|
|
1368
|
-
|
|
1400
|
+
"the file blob %s of %s did not arrive within %.0f s, "
|
|
1401
|
+
"%.1f MiB of %.1f MiB arrived"
|
|
1402
|
+
% (digest, os.path.basename(path), state["wait"],
|
|
1403
|
+
received / 1048576.0, size / 1048576.0)
|
|
1369
1404
|
)
|
|
1370
1405
|
_DATA_READY.wait(min(left, 0.5))
|
|
1371
1406
|
finally:
|
|
@@ -357,7 +357,9 @@ def sync_source(
|
|
|
357
357
|
It raises ``RuntimeError`` with ``uv could not be installed on <name>`` or ``uv sync
|
|
358
358
|
failed on <name>`` so the local side can name the step that failed.
|
|
359
359
|
"""
|
|
360
|
-
|
|
360
|
+
# Derived from the workspace root when it is given, so the two cannot disagree: the
|
|
361
|
+
# managed Python's directory is chosen by whether the project lies inside it.
|
|
362
|
+
root = root or project_dir(workspace or DEFAULT_WORKSPACE_ROOT, env)
|
|
361
363
|
workspace = workspace or DEFAULT_WORKSPACE_ROOT
|
|
362
364
|
encoded = {key: base64.b64encode(value).decode() for key, value in files.items()}
|
|
363
365
|
sync_args = sync_command(env)[1:]
|
|
@@ -400,8 +402,15 @@ def sync_source(
|
|
|
400
402
|
]
|
|
401
403
|
declared = dict(env.variables)
|
|
402
404
|
directories = uv_env(workspace)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
+
inside = root.rstrip("/") == workspace.rstrip("/") or root.startswith(
|
|
406
|
+
workspace.rstrip("/") + "/"
|
|
407
|
+
)
|
|
408
|
+
if archive or not inside:
|
|
409
|
+
# Keep an interpreter uv downloads inside the tree sent to the next VM, and, when
|
|
410
|
+
# the project directory is off the workspace root, beside the .venv it serves: a
|
|
411
|
+
# workspace root is not always a filesystem an interpreter can be unpacked on.
|
|
412
|
+
# Spec "Remote paths letify writes": Modal's is a volume that carries no hard
|
|
413
|
+
# link, so unpacking one there fails with Operation not permitted.
|
|
405
414
|
directories["UV_PYTHON_INSTALL_DIR"] = f"{root.rstrip('/')}/.letify-python"
|
|
406
415
|
for key, value in directories.items():
|
|
407
416
|
if key not in declared:
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.6.
|
|
141
|
+
version = "2.6.2"
|
|
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.2"
|
|
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.2"
|
|
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.2",
|
|
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.2",
|
|
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.2",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import collections
|
|
17
17
|
import os
|
|
18
|
+
import pickle
|
|
18
19
|
import re
|
|
19
20
|
import tempfile
|
|
20
21
|
import threading
|
|
@@ -1684,3 +1685,75 @@ def test_a_sender_that_stops_fails_the_readers_waiting_on_it(
|
|
|
1684
1685
|
assert "was never sent" in str(failure.value), failure.value
|
|
1685
1686
|
err = capsys.readouterr().err
|
|
1686
1687
|
assert "data sending stopped after" in err, err
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
def test_a_blob_still_arriving_does_not_time_out(let, cpu, project, monkeypatch) -> None:
|
|
1691
|
+
"""Spec "When a blob does not arrive": the timeout counts time without progress.
|
|
1692
|
+
|
|
1693
|
+
KICT's 11.3 GB table failed after 600 s on a Modal path measured at 45.3 MiB/s for a
|
|
1694
|
+
blob on its own. 11.3 GB needs 19.3 MiB/s to land inside 600 s, so a healthy transfer
|
|
1695
|
+
of a blob that large always failed, and the line said 0.0 MiB/s because the rate counts
|
|
1696
|
+
only the blobs that completed.
|
|
1697
|
+
"""
|
|
1698
|
+
from letify.store import pathdata
|
|
1699
|
+
|
|
1700
|
+
big = project / "slow.bin"
|
|
1701
|
+
big.write_bytes(b"s" * (6 << 20))
|
|
1702
|
+
monkeypatch.setattr(pathdata, "FIRST_WAVE_MIB", 0.0)
|
|
1703
|
+
monkeypatch.setattr(pathdata, "FIRST_WAVE_FILES", 0)
|
|
1704
|
+
monkeypatch.setattr(pathdata, "CHUNK", 1 << 20)
|
|
1705
|
+
# Each piece lands well inside the wait, and the whole blob takes longer than it.
|
|
1706
|
+
monkeypatch.setattr(pathdata, "WAIT_TIMEOUT_S", 4.0)
|
|
1707
|
+
def piecewise(runtime, pipeline, blobs, digest, local, size, meter):
|
|
1708
|
+
with open(local, "rb") as handle:
|
|
1709
|
+
offset = 0
|
|
1710
|
+
while offset < size:
|
|
1711
|
+
piece = handle.read(1 << 20)
|
|
1712
|
+
if not piece:
|
|
1713
|
+
break
|
|
1714
|
+
pipeline.send(
|
|
1715
|
+
{
|
|
1716
|
+
"op": "data_put",
|
|
1717
|
+
"dir": blobs,
|
|
1718
|
+
"digest": digest,
|
|
1719
|
+
"offset": offset,
|
|
1720
|
+
"chunk": pickle.PickleBuffer(piece),
|
|
1721
|
+
"last": offset + len(piece) >= size,
|
|
1722
|
+
}
|
|
1723
|
+
)
|
|
1724
|
+
offset += len(piece)
|
|
1725
|
+
time.sleep(3.0)
|
|
1726
|
+
|
|
1727
|
+
monkeypatch.setattr(pathdata, "_put", piecewise)
|
|
1728
|
+
|
|
1729
|
+
# retries=0, because a retry finds the blob already there and hides the timeout.
|
|
1730
|
+
@let.function(device=cpu, host=letify.remote, retries=0)
|
|
1731
|
+
def read_it(path) -> int:
|
|
1732
|
+
return len(path.read_bytes())
|
|
1733
|
+
|
|
1734
|
+
assert read_it(path=big) == 6 << 20
|
|
1735
|
+
|
|
1736
|
+
|
|
1737
|
+
def test_a_blob_that_never_starts_still_times_out_and_says_how_far_it_got(
|
|
1738
|
+
let, cpu, project, monkeypatch
|
|
1739
|
+
) -> None:
|
|
1740
|
+
"""Spec "When a blob does not arrive": the timeout still catches a blob that stalls."""
|
|
1741
|
+
from letify.store import pathdata
|
|
1742
|
+
|
|
1743
|
+
big = project / "stalled.bin"
|
|
1744
|
+
big.write_bytes(b"t" * (2 << 20))
|
|
1745
|
+
monkeypatch.setattr(pathdata, "FIRST_WAVE_MIB", 0.0)
|
|
1746
|
+
monkeypatch.setattr(pathdata, "FIRST_WAVE_FILES", 0)
|
|
1747
|
+
monkeypatch.setattr(pathdata, "WAIT_TIMEOUT_S", 4.0)
|
|
1748
|
+
# Nothing is ever sent, so nothing ever grows.
|
|
1749
|
+
monkeypatch.setattr(pathdata.Stream, "_send", lambda self, digest, meter: None)
|
|
1750
|
+
|
|
1751
|
+
@let.function(device=cpu, host=letify.remote)
|
|
1752
|
+
def read_it(path) -> int:
|
|
1753
|
+
return len(path.read_bytes())
|
|
1754
|
+
|
|
1755
|
+
with pytest.raises(RuntimeFailure) as failure:
|
|
1756
|
+
read_it(path=big)
|
|
1757
|
+
message = str(failure.value)
|
|
1758
|
+
assert "did not arrive" in message, message
|
|
1759
|
+
assert "arrived" in message, message
|
|
@@ -2523,3 +2523,44 @@ def test_a_lock_naming_cloudpickle_alone_is_accepted(tmp_path) -> None:
|
|
|
2523
2523
|
(tmp_path / "uv.lock").write_text('version = 1\n\n[[package]]\nname = "cloudpickle"\n')
|
|
2524
2524
|
files = bootstrap.project_files(Env(lock=str(tmp_path / "uv.lock")))
|
|
2525
2525
|
assert b"cloudpickle" in files["uv.lock"]
|
|
2526
|
+
|
|
2527
|
+
|
|
2528
|
+
def test_the_managed_python_goes_with_the_venv_when_the_provider_sets_an_env_root() -> None:
|
|
2529
|
+
"""Spec "Remote paths letify writes": a workspace root is not always unpackable.
|
|
2530
|
+
|
|
2531
|
+
Modal's workspace root is a Modal volume, which supports no hard link at all: a link
|
|
2532
|
+
inside it fails with EPERM and one to the sandbox disk with EXDEV. So uv downloading a
|
|
2533
|
+
managed interpreter into it failed with "Operation not permitted (os error 1)", naming
|
|
2534
|
+
neither the directory nor the reason. Reproduced on a live Modal T4 sandbox: the same
|
|
2535
|
+
sync with that directory on the sandbox disk succeeds.
|
|
2536
|
+
"""
|
|
2537
|
+
from letify.declare.env import Env
|
|
2538
|
+
from letify.runtime import bootstrap
|
|
2539
|
+
|
|
2540
|
+
env = Env()
|
|
2541
|
+
project = "/root/.letify-env/project/abc123"
|
|
2542
|
+
files = {"pyproject.toml": b"[project]\n", "uv.lock": b""}
|
|
2543
|
+
# A persistent provider archives nothing, which is the path that was left on the volume.
|
|
2544
|
+
source = bootstrap.sync_source(
|
|
2545
|
+
env, files, root=project, name="sandbox", workspace="/letify", archive=False
|
|
2546
|
+
)
|
|
2547
|
+
assert f"UV_PYTHON_INSTALL_DIR'] = os.path.expanduser('{project}/.letify-python')" in source
|
|
2548
|
+
# The cache and the tool directory stay on the workspace root, which outlives a sandbox
|
|
2549
|
+
# and was measured to carry them.
|
|
2550
|
+
assert "UV_CACHE_DIR'] = os.path.expanduser('/letify/uv-cache')" in source
|
|
2551
|
+
assert "UV_TOOL_DIR'] = os.path.expanduser('/letify/uv-tool')" in source
|
|
2552
|
+
|
|
2553
|
+
|
|
2554
|
+
def test_the_managed_python_stays_on_the_workspace_root_without_an_env_root() -> None:
|
|
2555
|
+
"""Spec "Remote paths letify writes": the workspace root is the only writable path
|
|
2556
|
+
some accounts have, so nothing moves where the venv is already under it."""
|
|
2557
|
+
from letify.declare.env import Env
|
|
2558
|
+
from letify.runtime import bootstrap
|
|
2559
|
+
|
|
2560
|
+
env = Env()
|
|
2561
|
+
project = "/workspace/project/abc123"
|
|
2562
|
+
files = {"pyproject.toml": b"[project]\n", "uv.lock": b""}
|
|
2563
|
+
source = bootstrap.sync_source(
|
|
2564
|
+
env, files, root=project, name="lab", workspace="/workspace", archive=False
|
|
2565
|
+
)
|
|
2566
|
+
assert "UV_PYTHON_INSTALL_DIR'] = os.path.expanduser('/workspace/uv-python')" in source
|
|
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.0 → letify-2.6.2}/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.0 → letify-2.6.2}/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.0 → letify-2.6.2}/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.0 → letify-2.6.2}/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
|
|
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
|
|
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
|