letify 2.0.0__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {letify-2.0.0 → letify-2.1.0}/PKG-INFO +1 -1
- {letify-2.0.0 → letify-2.1.0}/letify/__init__.py +1 -1
- {letify-2.0.0 → letify-2.1.0}/letify/protocol/worker.py +12 -4
- {letify-2.0.0 → letify-2.1.0}/letify/providers/kaggle.py +7 -4
- {letify-2.0.0 → letify-2.1.0}/letify/transport/probe.py +35 -9
- {letify-2.0.0 → letify-2.1.0}/letify-core/Cargo.lock +3 -3
- {letify-2.0.0 → letify-2.1.0}/letify-core/Cargo.toml +1 -1
- {letify-2.0.0 → letify-2.1.0}/letify-ext/package-lock.json +2 -2
- {letify-2.0.0 → letify-2.1.0}/letify-ext/package.json +1 -1
- {letify-2.0.0 → letify-2.1.0}/pyproject.toml +1 -1
- {letify-2.0.0 → letify-2.1.0}/tests/test_data_streaming.py +20 -1
- {letify-2.0.0 → letify-2.1.0}/tests/test_kaggle_link.py +14 -1
- {letify-2.0.0 → letify-2.1.0}/tests/test_transport.py +40 -0
- {letify-2.0.0 → letify-2.1.0}/.github/workflows/ci.yml +0 -0
- {letify-2.0.0 → letify-2.1.0}/.github/workflows/docs.yml +0 -0
- {letify-2.0.0 → letify-2.1.0}/.github/workflows/publish.yml +0 -0
- {letify-2.0.0 → letify-2.1.0}/.gitignore +0 -0
- {letify-2.0.0 → letify-2.1.0}/.researchtree +0 -0
- {letify-2.0.0 → letify-2.1.0}/LICENSE +0 -0
- {letify-2.0.0 → letify-2.1.0}/README.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/01-getting-started.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/02-providers.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/05-concurrency.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/06-cost.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/08-releasing.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/README.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/guide/ko/README.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/locales/README_ko.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/docs/mkdocs.yml +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/README.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/cli.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/config/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/config/inventory.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/config/login.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/config/schema.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/config/secrets.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/config/writer.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/declare/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/declare/cache.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/declare/env.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/declare/function.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/declare/instance.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/errors.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/install.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/launcher.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/paths.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/protocol/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/protocol/codec.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/protocol/driver.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/protocol/handle.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/protocol/wire.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/base.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/colab.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/colab_files.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/elice.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/local.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/modal.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/modal_adapter.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/naming.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/shell.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/tunnel.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/providers/usage.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/capability.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/client.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/cuda.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/executor.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/frames.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/guard.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/tensor.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/trace.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/device/value.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/loader.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/remoting/probe.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/render.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/runtime/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/runtime/bootstrap.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/runtime/channel.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/runtime/lease.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/runtime/pool.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/runtime/session.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/runtime/telemetry.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/backends/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/backends/filesystem.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/backends/google_auth.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/backends/layout.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/backends/objects.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/cas.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/pathdata.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/sendorder.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/store/volume.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/stubs.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/tools.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/__init__.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/agent.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/announce.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/link.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/nat.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/pipeline.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/portmap.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/quic.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/rendezvous.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/setup.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/sshopts.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify/transport/strategies.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/.gitignore +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/build.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/.vscodeignore +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/LICENSE +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/README.md +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/media/icon.svg +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/src/cli.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/src/extension.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/src/history.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/src/model.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/src/poll.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/src/render.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/test/model.test.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify-ext/tsconfig.json +0 -0
- {letify-2.0.0 → letify-2.1.0}/letify_providers.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/scripts/sync-main.sh +0 -0
- {letify-2.0.0 → letify-2.1.0}/scripts/version.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/conftest.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/fake_modal_adapter.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_cli.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_cli_output.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_colab_keepalive.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_config.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_core.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_data.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_declare.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_device.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_editor_sessions.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_elice.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_frames.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_install.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_kaggle.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_link_log.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_notebook.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_parallel_placement.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_paths.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_portmap.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_protocol.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_providers.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_quic.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_remoting.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_render.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_runtime.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_ship.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_store.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_strategies.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_stubs.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_tunnel_setup.py +0 -0
- {letify-2.0.0 → letify-2.1.0}/tests/test_version.py +0 -0
|
@@ -949,13 +949,21 @@ def _data_place_now(state, path, entry):
|
|
|
949
949
|
except OSError:
|
|
950
950
|
pass
|
|
951
951
|
if not linked:
|
|
952
|
+
# Linked at a name of its own and renamed over the path, never unlinked first.
|
|
953
|
+
# Unlinking leaves the final name absent for as long as the link takes, and when
|
|
954
|
+
# the link then fails the file that was there is lost: a body that listed the
|
|
955
|
+
# directory is left with a name it cannot stat. Spec "What the body sees before a
|
|
956
|
+
# file arrives". _DATA_PARTIAL names are filtered from every patched listing.
|
|
957
|
+
staged = "%s%slink%d" % (path, _DATA_PARTIAL, os.getpid())
|
|
952
958
|
try:
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
os.link(source, path)
|
|
959
|
+
os.link(source, staged)
|
|
960
|
+
os.replace(staged, path)
|
|
956
961
|
linked = True
|
|
957
962
|
except OSError:
|
|
958
|
-
|
|
963
|
+
try:
|
|
964
|
+
os.remove(staged)
|
|
965
|
+
except OSError:
|
|
966
|
+
pass
|
|
959
967
|
if not linked:
|
|
960
968
|
# Copied to a name of its own and renamed into place, so a reader never stats a
|
|
961
969
|
# file that is half written. Spec "What the body sees before a file arrives".
|
|
@@ -1125,7 +1125,7 @@ class Kaggle(Provider):
|
|
|
1125
1125
|
)
|
|
1126
1126
|
return rendezvous
|
|
1127
1127
|
|
|
1128
|
-
def target_over(self, channel: Any) -> Any:
|
|
1128
|
+
def target_over(self, channel: Any, *, name: str | None = None) -> Any:
|
|
1129
1129
|
"""What the strategies need, with no address because there is none to dial."""
|
|
1130
1130
|
from ..transport import nat
|
|
1131
1131
|
from ..transport.strategies import Target
|
|
@@ -1133,6 +1133,9 @@ class Kaggle(Provider):
|
|
|
1133
1133
|
user = self.config.option("user")
|
|
1134
1134
|
return Target(
|
|
1135
1135
|
alias=self.alias,
|
|
1136
|
+
# Every run is a new machine whose ssh-keygen -A makes new host keys, so one
|
|
1137
|
+
# alias for the account would fail every session after the first.
|
|
1138
|
+
host_key_alias=f"letify-{self.alias}-{name}" if name else None,
|
|
1136
1139
|
rendezvous=self.rendezvous_over(channel),
|
|
1137
1140
|
remote_python=self.remote_python,
|
|
1138
1141
|
stun=nat.DEFAULT_STUN,
|
|
@@ -1149,12 +1152,12 @@ class Kaggle(Provider):
|
|
|
1149
1152
|
|
|
1150
1153
|
return [TCPPunch(), TailcatUDP(), QuicUDP(), ProviderFallback(rank=6)]
|
|
1151
1154
|
|
|
1152
|
-
def link_over(self, channel: Any) -> Any:
|
|
1155
|
+
def link_over(self, channel: Any, *, name: str | None = None) -> Any:
|
|
1153
1156
|
"""Race the strategies over the bridge and return the link that wins."""
|
|
1154
1157
|
from ..transport.announce import printer
|
|
1155
1158
|
from ..transport.pipeline import LinkCache, Pipeline, network_fingerprint
|
|
1156
1159
|
|
|
1157
|
-
target = self.target_over(channel)
|
|
1160
|
+
target = self.target_over(channel, name=name)
|
|
1158
1161
|
return Pipeline(
|
|
1159
1162
|
self.strategies(),
|
|
1160
1163
|
target=target,
|
|
@@ -1195,7 +1198,7 @@ class Kaggle(Provider):
|
|
|
1195
1198
|
from ..runtime.channel import PersistentChannel
|
|
1196
1199
|
|
|
1197
1200
|
try:
|
|
1198
|
-
link = self.link_over(bridge)
|
|
1201
|
+
link = self.link_over(bridge, name=name)
|
|
1199
1202
|
except Exception:
|
|
1200
1203
|
return bridge
|
|
1201
1204
|
self._links[name] = link
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Probe, a short measurement of a Link.
|
|
2
2
|
|
|
3
|
-
Owns the client side of the probe: 30 round trips, then a
|
|
4
|
-
direction against the responder in ``nat.serve_probe``. It does not own
|
|
5
|
-
decide; the pipeline does.
|
|
3
|
+
Owns the client side of the probe: 30 round trips, then a discarded warm-up and a timed
|
|
4
|
+
transfer in each direction against the responder in ``nat.serve_probe``. It does not own
|
|
5
|
+
what the numbers decide; the pipeline does.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -38,8 +38,19 @@ class ProbeResult:
|
|
|
38
38
|
return cls(float(data["rtt_ms"]), float(data["upload_bps"]), float(data["download_bps"]))
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
#: Round trips of warm-up before the measured transfer, and the bounds on it in seconds.
|
|
42
|
+
WARMUP_ROUND_TRIPS = 8
|
|
43
|
+
WARMUP_MIN_S = 0.5
|
|
44
|
+
WARMUP_MAX_S = 2.0
|
|
45
|
+
|
|
46
|
+
|
|
41
47
|
class Probe:
|
|
42
|
-
"""30 round trips, then ``seconds`` of transfer each way.
|
|
48
|
+
"""30 round trips, a discarded warm-up, then ``seconds`` of transfer each way.
|
|
49
|
+
|
|
50
|
+
The warm-up exists because TCP's ramp is fixed in round trips while the measurement
|
|
51
|
+
window is fixed in seconds, so without it a far link is charged for its own slow
|
|
52
|
+
start and reports the ramp average. Spec "Choosing a link". Durations are injectable.
|
|
53
|
+
"""
|
|
43
54
|
|
|
44
55
|
def __init__(
|
|
45
56
|
self,
|
|
@@ -52,6 +63,11 @@ class Probe:
|
|
|
52
63
|
self.seconds = seconds
|
|
53
64
|
self.clock = clock
|
|
54
65
|
|
|
66
|
+
def warmup_for(self, rtt_ms: float) -> float:
|
|
67
|
+
"""Seconds of transfer to discard before measuring, from the round trip."""
|
|
68
|
+
wanted = WARMUP_ROUND_TRIPS * rtt_ms / 1000.0
|
|
69
|
+
return min(WARMUP_MAX_S, max(WARMUP_MIN_S, wanted))
|
|
70
|
+
|
|
55
71
|
def measure(self, stream: Stream) -> ProbeResult:
|
|
56
72
|
rtts = []
|
|
57
73
|
for index in range(self.round_trips):
|
|
@@ -61,26 +77,36 @@ class Probe:
|
|
|
61
77
|
if nat.recv_exact(stream, 9) != b"P" + payload:
|
|
62
78
|
raise OSError("the probe responder answered out of order")
|
|
63
79
|
rtts.append((self.clock() - began) * 1000.0)
|
|
80
|
+
rtt_ms = statistics.median(rtts) if rtts else 0.0
|
|
81
|
+
|
|
82
|
+
# The warm-up runs on the same connection, so the window it opens carries over.
|
|
83
|
+
warmup = self.warmup_for(rtt_ms)
|
|
84
|
+
self._upload(stream, warmup)
|
|
85
|
+
upload = self._upload(stream, self.seconds)
|
|
86
|
+
self._download(stream, warmup)
|
|
87
|
+
download = self._download(stream, self.seconds)
|
|
88
|
+
return ProbeResult(rtt_ms, upload, download)
|
|
64
89
|
|
|
90
|
+
def _upload(self, stream: Stream, seconds: float) -> float:
|
|
65
91
|
block = b"\0" * nat.CHUNK
|
|
66
92
|
stream.sendall(b"U")
|
|
67
93
|
began = self.clock()
|
|
68
|
-
while self.clock() - began <
|
|
94
|
+
while self.clock() - began < seconds:
|
|
69
95
|
stream.sendall(struct.pack("!I", len(block)) + block)
|
|
70
96
|
stream.sendall(struct.pack("!I", 0))
|
|
71
97
|
received = struct.unpack("!Q", nat.recv_exact(stream, 8))[0]
|
|
72
|
-
|
|
98
|
+
return received / max(self.clock() - began, 1e-9)
|
|
73
99
|
|
|
100
|
+
def _download(self, stream: Stream, seconds: float) -> float:
|
|
74
101
|
began = self.clock()
|
|
75
|
-
stream.sendall(b"D" + struct.pack("!d",
|
|
102
|
+
stream.sendall(b"D" + struct.pack("!d", seconds))
|
|
76
103
|
total = 0
|
|
77
104
|
while True:
|
|
78
105
|
size = struct.unpack("!I", nat.recv_exact(stream, 4))[0]
|
|
79
106
|
if size == 0:
|
|
80
107
|
break
|
|
81
108
|
total += len(nat.recv_exact(stream, size))
|
|
82
|
-
|
|
83
|
-
return ProbeResult(statistics.median(rtts) if rtts else 0.0, upload, download)
|
|
109
|
+
return total / max(self.clock() - began, 1e-9)
|
|
84
110
|
|
|
85
111
|
|
|
86
112
|
__all__ = ["Probe", "ProbeResult", "Stream"]
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.
|
|
141
|
+
version = "2.1.0"
|
|
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.
|
|
149
|
+
version = "2.1.0"
|
|
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.
|
|
167
|
+
version = "2.1.0"
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "libc"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.1.0",
|
|
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.
|
|
5
|
+
"version": "2.1.0",
|
|
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 re
|
|
18
|
+
import threading
|
|
18
19
|
import time
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
|
|
@@ -752,10 +753,28 @@ def test_a_blob_that_never_arrives_fails_the_open(
|
|
|
752
753
|
|
|
753
754
|
|
|
754
755
|
def test_a_call_that_returns_early_cancels_the_transfer(
|
|
755
|
-
launcher_from, project, capsys
|
|
756
|
+
launcher_from, project, capsys, monkeypatch
|
|
756
757
|
) -> None:
|
|
758
|
+
# Spec "A call that finishes before the background transfer". Each piece sent after
|
|
759
|
+
# the call request waits 50 ms, so the cancellation is certain to arrive while the
|
|
760
|
+
# transfer is still running. Without that the test asserts a timing accident: on a
|
|
761
|
+
# local channel all 23 MiB can go out in 0.1 s, before the body has even returned,
|
|
762
|
+
# and nothing is left to cancel. It failed that way in 2 of 4 runs pinned to two
|
|
763
|
+
# cores, which is what CI runs on.
|
|
757
764
|
let = streaming(launcher_from, data_first_wave_files=1)
|
|
758
765
|
root = dataset(project, 24, size=1 << 20)
|
|
766
|
+
calling = threading.Event()
|
|
767
|
+
original_message = wire.Sender.message
|
|
768
|
+
|
|
769
|
+
def message(self_, kind, stream, obj):
|
|
770
|
+
if kind == wire.REQUEST and isinstance(obj, dict):
|
|
771
|
+
if obj.get("op") == "call":
|
|
772
|
+
calling.set()
|
|
773
|
+
elif obj.get("op") == "data_put" and calling.is_set():
|
|
774
|
+
time.sleep(0.05)
|
|
775
|
+
return original_message(self_, kind, stream, obj)
|
|
776
|
+
|
|
777
|
+
monkeypatch.setattr(wire.Sender, "message", message)
|
|
759
778
|
|
|
760
779
|
@let.function(device=let.providers.lab.CPU, host=letify.remote)
|
|
761
780
|
def peek(directory: Path) -> str:
|
|
@@ -131,7 +131,7 @@ def test_a_chosen_link_becomes_the_channel(monkeypatch) -> None:
|
|
|
131
131
|
built.append(command)
|
|
132
132
|
self.name = name
|
|
133
133
|
|
|
134
|
-
monkeypatch.setattr(provider, "link_over", lambda channel: Link())
|
|
134
|
+
monkeypatch.setattr(provider, "link_over", lambda channel, name=None: Link())
|
|
135
135
|
monkeypatch.setattr("letify.runtime.channel.PersistentChannel", Fake)
|
|
136
136
|
channel = provider.channel_over(Channel(), name="k-1")
|
|
137
137
|
assert isinstance(channel, Fake)
|
|
@@ -230,3 +230,16 @@ def test_the_kaggle_punch_target_logs_in_as_root_with_the_authorized_key(tmp_pat
|
|
|
230
230
|
command = target.forwarded_ssh(40000, "python3 -c pass")
|
|
231
231
|
assert "root@127.0.0.1" in command
|
|
232
232
|
assert command[command.index("-i") + 1] == str(key)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_a_kaggle_runtime_pins_its_host_key_under_its_own_alias() -> None:
|
|
236
|
+
"""Spec "Kaggle runtimes", An SSH link over the kernel: each run is a new machine.
|
|
237
|
+
|
|
238
|
+
One alias for the account fails every session after the first with REMOTE HOST
|
|
239
|
+
IDENTIFICATION HAS CHANGED, because ssh-keygen -A makes new host keys on each VM.
|
|
240
|
+
"""
|
|
241
|
+
provider = kaggle()
|
|
242
|
+
target = provider.target_over(object(), name="letify-cpu-abc123")
|
|
243
|
+
assert target.host_key_alias == "letify-kaggle_a-letify-cpu-abc123"
|
|
244
|
+
command = target.forwarded_ssh(40000)
|
|
245
|
+
assert command[command.index("HostKeyAlias=letify-kaggle_a-letify-cpu-abc123")]
|
|
@@ -224,6 +224,46 @@ def test_the_probe_defaults_are_thirty_round_trips_and_two_seconds_each_way() ->
|
|
|
224
224
|
assert (probe.round_trips, probe.seconds) == (30, 2.0)
|
|
225
225
|
|
|
226
226
|
|
|
227
|
+
def test_the_warm_up_is_eight_round_trips_bounded_to_half_a_second_and_two() -> None:
|
|
228
|
+
# Spec "Choosing a link": long enough for TCP to leave slow start, and no longer.
|
|
229
|
+
probe = Probe()
|
|
230
|
+
assert probe.warmup_for(0.0) == 0.5
|
|
231
|
+
assert probe.warmup_for(10.0) == 0.5
|
|
232
|
+
assert probe.warmup_for(190.0) == pytest.approx(1.52)
|
|
233
|
+
assert probe.warmup_for(1000.0) == 2.0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_the_probe_discards_a_warm_up_transfer_in_each_direction() -> None:
|
|
237
|
+
"""Spec "Choosing a link": the measured transfer is not charged for the ramp.
|
|
238
|
+
|
|
239
|
+
The responder is asked to transfer four times, a warm-up and a measurement each way.
|
|
240
|
+
Only the measured ones decide the numbers, so a stream that is slow until it is warm
|
|
241
|
+
reports its warm rate.
|
|
242
|
+
"""
|
|
243
|
+
user, remote = socket.socketpair()
|
|
244
|
+
threading.Thread(target=nat.serve_probe, args=(remote,), daemon=True).start()
|
|
245
|
+
probe = Probe(round_trips=2, seconds=0.02)
|
|
246
|
+
probe.warmup_for = lambda _rtt_ms: 0.02
|
|
247
|
+
seen: list[bytes] = []
|
|
248
|
+
|
|
249
|
+
class Watched:
|
|
250
|
+
"""The stream, recording which transfer ops the probe asked for."""
|
|
251
|
+
|
|
252
|
+
def recv(self, size: int) -> bytes:
|
|
253
|
+
return user.recv(size)
|
|
254
|
+
|
|
255
|
+
def sendall(self, data: bytes) -> None:
|
|
256
|
+
if data[:1] in (b"U", b"D"):
|
|
257
|
+
seen.append(data[:1])
|
|
258
|
+
user.sendall(data)
|
|
259
|
+
|
|
260
|
+
measured = probe.measure(Watched())
|
|
261
|
+
user.close()
|
|
262
|
+
assert seen == [b"U", b"U", b"D", b"D"]
|
|
263
|
+
assert measured.upload_bps > 0
|
|
264
|
+
assert measured.download_bps > 0
|
|
265
|
+
|
|
266
|
+
|
|
227
267
|
def test_a_responder_asked_to_bridge_stops_answering_the_probe() -> None:
|
|
228
268
|
user, remote = socket.socketpair()
|
|
229
269
|
user.sendall(b"B")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.0.0 → letify-2.1.0}/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.0.0 → letify-2.1.0}/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.0.0 → letify-2.1.0}/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.0.0 → letify-2.1.0}/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
|