letify 1.2.4__tar.gz → 2.0.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-1.2.4 → letify-2.0.0}/PKG-INFO +1 -1
- {letify-1.2.4 → letify-2.0.0}/letify/__init__.py +1 -1
- {letify-1.2.4 → letify-2.0.0}/letify/install.py +58 -0
- {letify-1.2.4 → letify-2.0.0}/letify/protocol/worker.py +76 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/base.py +36 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/colab.py +5 -18
- {letify-1.2.4 → letify-2.0.0}/letify/providers/kaggle.py +174 -1
- {letify-1.2.4 → letify-2.0.0}/letify/providers/local.py +14 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/shell.py +66 -2
- {letify-1.2.4 → letify-2.0.0}/letify/store/pathdata.py +156 -1
- {letify-1.2.4 → letify-2.0.0}/letify/stubs.py +3 -2
- {letify-1.2.4 → letify-2.0.0}/letify/transport/nat.py +206 -14
- {letify-1.2.4 → letify-2.0.0}/letify/transport/pipeline.py +32 -0
- letify-2.0.0/letify/transport/portmap.py +323 -0
- letify-2.0.0/letify/transport/quic.py +60 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/rendezvous.py +39 -9
- {letify-1.2.4 → letify-2.0.0}/letify/transport/strategies.py +166 -5
- letify-2.0.0/letify-core/Cargo.lock +749 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/Cargo.toml +2 -2
- {letify-1.2.4 → letify-2.0.0}/letify-core/build.py +13 -0
- letify-2.0.0/letify-core/letify-quic/Cargo.toml +20 -0
- letify-2.0.0/letify-core/letify-quic/src/main.rs +316 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/package-lock.json +2 -2
- {letify-1.2.4 → letify-2.0.0}/letify-ext/package.json +1 -1
- {letify-1.2.4 → letify-2.0.0}/pyproject.toml +9 -1
- letify-2.0.0/tests/test_kaggle_link.py +232 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_link_log.py +32 -0
- letify-2.0.0/tests/test_parallel_placement.py +351 -0
- letify-2.0.0/tests/test_portmap.py +225 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_providers.py +32 -8
- letify-2.0.0/tests/test_quic.py +187 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_strategies.py +208 -5
- {letify-1.2.4 → letify-2.0.0}/tests/test_stubs.py +25 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_transport.py +175 -0
- letify-1.2.4/letify-core/Cargo.lock +0 -44
- {letify-1.2.4 → letify-2.0.0}/.github/workflows/ci.yml +0 -0
- {letify-1.2.4 → letify-2.0.0}/.github/workflows/docs.yml +0 -0
- {letify-1.2.4 → letify-2.0.0}/.github/workflows/publish.yml +0 -0
- {letify-1.2.4 → letify-2.0.0}/.gitignore +0 -0
- {letify-1.2.4 → letify-2.0.0}/.researchtree +0 -0
- {letify-1.2.4 → letify-2.0.0}/LICENSE +0 -0
- {letify-1.2.4 → letify-2.0.0}/README.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/01-getting-started.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/02-providers.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/03-execution-modes.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/04-environments-and-data.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/05-concurrency.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/06-cost.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/07-troubleshooting.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/08-releasing.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/README.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-1.2.4 → letify-2.0.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-1.2.4 → letify-2.0.0}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/guide/ko/README.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/locales/README_ko.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/docs/mkdocs.yml +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/README.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/README.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/common.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/cli.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/config/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/config/inventory.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/config/login.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/config/schema.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/config/secrets.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/config/writer.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/declare/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/declare/cache.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/declare/env.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/declare/function.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/declare/instance.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/errors.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/launcher.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/paths.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/protocol/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/protocol/codec.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/protocol/driver.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/protocol/handle.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/protocol/wire.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/colab_files.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/colab_keepalive.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/elice.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/kaggle_adapter.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/modal.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/modal_adapter.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/naming.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/tunnel.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/providers/usage.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/capability.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/client.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/cuda.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/executor.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/frames.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/guard.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/tensor.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/trace.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/device/value.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/loader.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/remoting/probe.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/render.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/runtime/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/runtime/bootstrap.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/runtime/channel.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/runtime/lease.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/runtime/pool.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/runtime/session.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/runtime/telemetry.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/backends/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/backends/filesystem.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/backends/google_auth.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/backends/layout.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/backends/objects.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/cas.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/sendorder.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/store/volume.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/tools.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/__init__.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/agent.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/announce.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/link.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/probe.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/setup.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify/transport/sshopts.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/.gitignore +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-core/rust-toolchain.toml +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/.vscodeignore +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/LICENSE +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/README.md +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/media/icon.svg +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/src/cli.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/src/extension.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/src/history.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/src/model.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/src/poll.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/src/render.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/test/cli.test.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/test/manifest.test.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/test/model.test.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/test/regressions.test.ts +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify-ext/tsconfig.json +0 -0
- {letify-1.2.4 → letify-2.0.0}/letify_providers.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/scripts/sync-main.sh +0 -0
- {letify-1.2.4 → letify-2.0.0}/scripts/version.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/conftest.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/fake_kaggle_adapter.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/fake_modal_adapter.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_cli.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_cli_output.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_colab_keepalive.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_config.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_core.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_data.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_data_streaming.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_declare.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_device.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_editor_sessions.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_elice.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_frames.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_install.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_kaggle.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_kaggle_cookie.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_notebook.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_paths.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_protocol.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_remoting.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_render.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_runtime.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_ship.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_store.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_tunnel_setup.py +0 -0
- {letify-1.2.4 → letify-2.0.0}/tests/test_version.py +0 -0
|
@@ -626,6 +626,64 @@ def remote_tailcat_source() -> str:
|
|
|
626
626
|
)
|
|
627
627
|
|
|
628
628
|
|
|
629
|
+
def remote_quic_source(version: str) -> str:
|
|
630
|
+
"""Prepare ``letify-quic`` on the remote from the letify wheel of this version.
|
|
631
|
+
|
|
632
|
+
Spec "QUIC over a punched UDP pair": the binary changes with every letify release, so
|
|
633
|
+
a hash pinned in this source cannot describe the build that is about to be made. The
|
|
634
|
+
remote therefore fetches the wheel for its own platform at exactly this version, which
|
|
635
|
+
PyPI verifies, and takes the binary out of it.
|
|
636
|
+
"""
|
|
637
|
+
import inspect
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
inspect.getsource(_remote_quic)
|
|
641
|
+
+ f"\nbinary = _remote_quic({version!r})\n"
|
|
642
|
+
+ "import json\nprint('LETIFY-QUIC ' + json.dumps(binary))\n"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _remote_quic(version):
|
|
647
|
+
"""Take letify-quic out of the letify wheel for this machine, standard library only."""
|
|
648
|
+
import json
|
|
649
|
+
import os
|
|
650
|
+
import subprocess
|
|
651
|
+
import sys
|
|
652
|
+
import tempfile
|
|
653
|
+
import zipfile
|
|
654
|
+
from pathlib import Path
|
|
655
|
+
|
|
656
|
+
inside = "letify/remoting/lib/letify-quic"
|
|
657
|
+
target = Path(tempfile.gettempdir()) / f"letify-quic-{version}"
|
|
658
|
+
if target.is_file() and os.access(target, os.X_OK):
|
|
659
|
+
return str(target)
|
|
660
|
+
directory = tempfile.mkdtemp(prefix="letify-quic-")
|
|
661
|
+
done = subprocess.run(
|
|
662
|
+
[
|
|
663
|
+
sys.executable, "-m", "pip", "download", f"letify=={version}",
|
|
664
|
+
"--no-deps", "--only-binary", ":all:", "-d", directory,
|
|
665
|
+
],
|
|
666
|
+
capture_output=True,
|
|
667
|
+
text=True,
|
|
668
|
+
timeout=600,
|
|
669
|
+
)
|
|
670
|
+
if done.returncode != 0:
|
|
671
|
+
raise RuntimeError(
|
|
672
|
+
"could not download the letify wheel for letify-quic: "
|
|
673
|
+
+ (done.stderr or done.stdout).strip()[-400:]
|
|
674
|
+
)
|
|
675
|
+
wheels = sorted(Path(directory).glob("letify-*.whl"))
|
|
676
|
+
if not wheels:
|
|
677
|
+
raise RuntimeError("pip downloaded no letify wheel")
|
|
678
|
+
with zipfile.ZipFile(wheels[0]) as archive:
|
|
679
|
+
if inside not in archive.namelist():
|
|
680
|
+
raise RuntimeError(f"{wheels[0].name} carries no {inside}")
|
|
681
|
+
target.write_bytes(archive.read(inside))
|
|
682
|
+
target.chmod(0o755)
|
|
683
|
+
json.dumps(str(target))
|
|
684
|
+
return str(target)
|
|
685
|
+
|
|
686
|
+
|
|
629
687
|
def _remote_tailcat(version, release, pins):
|
|
630
688
|
"""Install a verified Linux tailcat using only the runtime's standard library."""
|
|
631
689
|
import hashlib
|
|
@@ -694,6 +694,79 @@ def _op_data_put(request):
|
|
|
694
694
|
return {"ok": True, "value": None}
|
|
695
695
|
|
|
696
696
|
|
|
697
|
+
def _parallel_partial(root, digest):
|
|
698
|
+
"""Where parallel senders write. One name per digest, not per process or thread."""
|
|
699
|
+
return "%s%s.parallel" % (_data_file(root, digest), _DATA_PARTIAL)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _op_data_begin(request):
|
|
703
|
+
"""Make the file parallel senders will fill, of the size it will end up.
|
|
704
|
+
|
|
705
|
+
Spec "Several connections at once". A placement starts here rather than at the write
|
|
706
|
+
of offset zero, because with several connections offset zero is just another range and
|
|
707
|
+
may arrive last: truncating on it would throw away what the others had written.
|
|
708
|
+
"""
|
|
709
|
+
partial = _parallel_partial(request["dir"], request["digest"])
|
|
710
|
+
os.makedirs(os.path.dirname(partial), exist_ok=True)
|
|
711
|
+
with open(partial, "wb") as handle:
|
|
712
|
+
handle.truncate(int(request["size"]))
|
|
713
|
+
return {"ok": True, "value": None}
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _op_data_put_at(request):
|
|
717
|
+
"""Write one piece of a file blob at its offset, for several senders at once.
|
|
718
|
+
|
|
719
|
+
No hashing: a hash over a stream cannot be computed out of order, so ``data_seal``
|
|
720
|
+
does it by reading the finished file.
|
|
721
|
+
"""
|
|
722
|
+
partial = _parallel_partial(request["dir"], request["digest"])
|
|
723
|
+
chunk = request.pop("chunk")
|
|
724
|
+
handle = open(partial, "r+b")
|
|
725
|
+
try:
|
|
726
|
+
handle.seek(int(request["offset"]))
|
|
727
|
+
handle.write(chunk)
|
|
728
|
+
finally:
|
|
729
|
+
handle.close()
|
|
730
|
+
chunk = None
|
|
731
|
+
return {"ok": True, "value": None}
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _op_data_seal(request):
|
|
735
|
+
"""Hash a file assembled by parallel senders, then commit it as ``data_put`` does."""
|
|
736
|
+
digest = request["digest"]
|
|
737
|
+
root = request["dir"]
|
|
738
|
+
partial = _parallel_partial(root, digest)
|
|
739
|
+
final = _data_file(root, digest)
|
|
740
|
+
size = request.get("size")
|
|
741
|
+
try:
|
|
742
|
+
found = os.path.getsize(partial)
|
|
743
|
+
except OSError as exc:
|
|
744
|
+
raise ValueError("no pieces of file blob %s arrived: %s" % (digest, exc))
|
|
745
|
+
if size is not None and found != int(size):
|
|
746
|
+
raise ValueError(
|
|
747
|
+
"file blob %s is short: %d of %d bytes arrived" % (digest, found, int(size))
|
|
748
|
+
)
|
|
749
|
+
hasher = _data_hasher()
|
|
750
|
+
if hasher is not None:
|
|
751
|
+
with open(partial, "rb") as handle:
|
|
752
|
+
while True:
|
|
753
|
+
piece = handle.read(1 << 22)
|
|
754
|
+
if not piece:
|
|
755
|
+
break
|
|
756
|
+
hasher.update(piece)
|
|
757
|
+
got = hasher.hexdigest(length=16)
|
|
758
|
+
if got != digest:
|
|
759
|
+
os.remove(partial)
|
|
760
|
+
raise ValueError("file blob %s arrived with digest %s" % (digest, got))
|
|
761
|
+
try:
|
|
762
|
+
os.chmod(partial, 0o444)
|
|
763
|
+
except OSError:
|
|
764
|
+
pass
|
|
765
|
+
os.replace(partial, final)
|
|
766
|
+
_data_arrived(root, digest)
|
|
767
|
+
return {"ok": True, "value": None}
|
|
768
|
+
|
|
769
|
+
|
|
697
770
|
def _op_data_pull(request):
|
|
698
771
|
"""Download file blobs from the bucket, eight at a time, then forget the headers."""
|
|
699
772
|
import urllib.request
|
|
@@ -1875,6 +1948,9 @@ _OPS = {
|
|
|
1875
1948
|
"have": _op_have,
|
|
1876
1949
|
"data_have": _op_data_have,
|
|
1877
1950
|
"data_put": _op_data_put,
|
|
1951
|
+
"data_begin": _op_data_begin,
|
|
1952
|
+
"data_put_at": _op_data_put_at,
|
|
1953
|
+
"data_seal": _op_data_seal,
|
|
1878
1954
|
"data_pull": _op_data_pull,
|
|
1879
1955
|
"data_evict": _op_data_evict,
|
|
1880
1956
|
"data_cache": _op_data_cache,
|
|
@@ -260,6 +260,42 @@ class Provider(abc.ABC):
|
|
|
260
260
|
return "ephemeral"
|
|
261
261
|
return self.default_persistence
|
|
262
262
|
|
|
263
|
+
@property
|
|
264
|
+
def transfer_streams(self) -> int:
|
|
265
|
+
"""Connections a large blob is split across, from ``transfer_streams``.
|
|
266
|
+
|
|
267
|
+
Spec "Several connections at once": one TCP connection over a long round trip
|
|
268
|
+
carries a fraction of what the path will carry, and independent connections add
|
|
269
|
+
up. One keeps the single stream.
|
|
270
|
+
"""
|
|
271
|
+
from ..store.pathdata import STREAMS
|
|
272
|
+
|
|
273
|
+
value = self.config.option("transfer_streams")
|
|
274
|
+
return max(1, int(value)) if isinstance(value, (int, float)) else STREAMS
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def transfer_parallel_rtt_ms(self) -> int:
|
|
278
|
+
"""The round trip above which a blob is split, from ``transfer_parallel_rtt_ms``.
|
|
279
|
+
|
|
280
|
+
Below it one stream already carries everything the link will carry, so the extra
|
|
281
|
+
connections are pure cost. Spec "Several connections at once".
|
|
282
|
+
"""
|
|
283
|
+
from ..store.pathdata import PARALLEL_RTT_MS
|
|
284
|
+
|
|
285
|
+
value = self.config.option("transfer_parallel_rtt_ms")
|
|
286
|
+
return max(0, int(value)) if isinstance(value, (int, float)) else PARALLEL_RTT_MS
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def transfer_parallel_mib(self) -> int:
|
|
290
|
+
"""The size above which a blob is split, from ``transfer_parallel_mib``.
|
|
291
|
+
|
|
292
|
+
Below it, opening more connections costs more than it saves.
|
|
293
|
+
"""
|
|
294
|
+
from ..store.pathdata import PARALLEL_MIB
|
|
295
|
+
|
|
296
|
+
value = self.config.option("transfer_parallel_mib")
|
|
297
|
+
return max(0, int(value)) if isinstance(value, (int, float)) else PARALLEL_MIB
|
|
298
|
+
|
|
263
299
|
@property
|
|
264
300
|
def persistent(self) -> bool:
|
|
265
301
|
return self.persistence == "persistent"
|
|
@@ -323,30 +323,17 @@ class Colab(Shell):
|
|
|
323
323
|
def rendezvous(self, runtime: Runtime | None = None) -> Rendezvous | None:
|
|
324
324
|
if runtime is None:
|
|
325
325
|
return None
|
|
326
|
-
from ..transport.rendezvous import ColabRendezvous
|
|
326
|
+
from ..transport.rendezvous import ColabRendezvous, prepare_tailcat
|
|
327
327
|
|
|
328
328
|
name = runtime.name
|
|
329
329
|
|
|
330
|
-
def
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if not (
|
|
334
|
-
install.find("tailcat", link_cache=False) or self.config.option("tailcat_binary")
|
|
335
|
-
):
|
|
336
|
-
return None
|
|
337
|
-
output = self._exec(name, install.remote_tailcat_source(), 180)
|
|
338
|
-
for line in output.splitlines():
|
|
339
|
-
if line.startswith("LETIFY-TAILCAT "):
|
|
340
|
-
return json.loads(line[len("LETIFY-TAILCAT ") :])
|
|
341
|
-
raise RuntimeFailure(
|
|
342
|
-
"tailcat installation gave no verified binary path",
|
|
343
|
-
stderr=output.strip(),
|
|
344
|
-
)
|
|
330
|
+
def run(source: str, timeout: float) -> str:
|
|
331
|
+
return self._exec(name, source, timeout)
|
|
345
332
|
|
|
346
333
|
return ColabRendezvous(
|
|
347
|
-
|
|
334
|
+
run,
|
|
348
335
|
self._public_key(),
|
|
349
|
-
prepare_tailcat=prepare_tailcat,
|
|
336
|
+
prepare_tailcat=prepare_tailcat(run, self.config.option("tailcat_binary")),
|
|
350
337
|
)
|
|
351
338
|
|
|
352
339
|
def fallback(self, runtime: Runtime | None = None) -> Callable[[], Link] | None:
|
|
@@ -25,6 +25,7 @@ import binascii
|
|
|
25
25
|
import json
|
|
26
26
|
import math
|
|
27
27
|
import re
|
|
28
|
+
import textwrap
|
|
28
29
|
import time
|
|
29
30
|
import urllib.error
|
|
30
31
|
import urllib.parse
|
|
@@ -929,6 +930,48 @@ class KaggleChannel(FramedChannel):
|
|
|
929
930
|
process.kill()
|
|
930
931
|
|
|
931
932
|
|
|
933
|
+
def _capture_stdout(source: str) -> str:
|
|
934
|
+
"""``source`` with its standard output captured into ``__letify_value__``.
|
|
935
|
+
|
|
936
|
+
A ``from __future__`` import has to be the first statement of the module, so those
|
|
937
|
+
lines are hoisted above the wrapper rather than indented under it. Without that the
|
|
938
|
+
rendezvous program, which begins with one, fails to compile and the punch reports that
|
|
939
|
+
the remote half gave no answer.
|
|
940
|
+
"""
|
|
941
|
+
lines = source.splitlines(keepends=True)
|
|
942
|
+
future = [line for line in lines if line.lstrip().startswith("from __future__")]
|
|
943
|
+
rest = [line for line in lines if not line.lstrip().startswith("from __future__")]
|
|
944
|
+
return (
|
|
945
|
+
"".join(future)
|
|
946
|
+
+ "import contextlib as _c, io as _io\n"
|
|
947
|
+
+ "__letify_buffer__ = _io.StringIO()\n"
|
|
948
|
+
+ "with _c.redirect_stdout(__letify_buffer__):\n"
|
|
949
|
+
+ textwrap.indent("".join(rest), " ")
|
|
950
|
+
+ "\n__letify_value__ = __letify_buffer__.getvalue()\n"
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _key_path():
|
|
955
|
+
"""Where letify keeps the key pair a Kaggle rendezvous authorizes."""
|
|
956
|
+
from pathlib import Path as _Path
|
|
957
|
+
|
|
958
|
+
return _Path.home() / ".ssh" / "id_letify"
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _public_key() -> str | None:
|
|
962
|
+
"""letify's own public key, so the rendezvous can authorize it on the machine."""
|
|
963
|
+
try:
|
|
964
|
+
return _key_path().with_suffix(".pub").read_text(encoding="utf-8").strip() or None
|
|
965
|
+
except OSError:
|
|
966
|
+
return None
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _private_key() -> str | None:
|
|
970
|
+
"""The private half, which the SSH command over a punched link logs in with."""
|
|
971
|
+
path = _key_path()
|
|
972
|
+
return str(path) if path.exists() else None
|
|
973
|
+
|
|
974
|
+
|
|
932
975
|
class Kaggle(Provider):
|
|
933
976
|
"""One Kaggle account."""
|
|
934
977
|
|
|
@@ -1005,6 +1048,8 @@ class Kaggle(Provider):
|
|
|
1005
1048
|
#: runs its programs in. The notebook is this runtime's own, created in
|
|
1006
1049
|
#: ``open_channel`` and deleted in ``stop``.
|
|
1007
1050
|
self._kernels: dict[str, tuple[Session, str, KaggleChannel, int, int, str | None]] = {}
|
|
1051
|
+
#: The link chosen over the bridge, per runtime, closed with the session.
|
|
1052
|
+
self._links: dict[str, Any] = {}
|
|
1008
1053
|
#: Monotonic time of the last confirmed-live cookie check, or None before the first.
|
|
1009
1054
|
self._cookie_checked_at: float | None = None
|
|
1010
1055
|
|
|
@@ -1039,6 +1084,126 @@ class Kaggle(Provider):
|
|
|
1039
1084
|
self._cookie_checked_at = now
|
|
1040
1085
|
return cookie
|
|
1041
1086
|
|
|
1087
|
+
# -- an SSH link over the kernel ----------------------------------------------
|
|
1088
|
+
|
|
1089
|
+
def rendezvous_over(self, channel: Any) -> Any:
|
|
1090
|
+
"""The bridge as a rendezvous: it runs one program on the machine and reports.
|
|
1091
|
+
|
|
1092
|
+
Spec "Kaggle runtimes", An SSH link over the kernel. The kernel is how letify
|
|
1093
|
+
reaches the machine; it does not have to be how the session is carried.
|
|
1094
|
+
"""
|
|
1095
|
+
from ..transport.rendezvous import CommandRendezvous, prepare_tailcat
|
|
1096
|
+
|
|
1097
|
+
bridge = channel
|
|
1098
|
+
|
|
1099
|
+
class KaggleRendezvous(CommandRendezvous):
|
|
1100
|
+
"""``eval`` on the open bridge, which is a standard library program on the VM."""
|
|
1101
|
+
|
|
1102
|
+
def extras(self) -> dict[str, Any]:
|
|
1103
|
+
# The VM has no key of letify's yet, and no SSH server running.
|
|
1104
|
+
extra: dict[str, Any] = {"start_sshd": True}
|
|
1105
|
+
public = _public_key()
|
|
1106
|
+
if public:
|
|
1107
|
+
extra["authorized_key"] = public
|
|
1108
|
+
return extra
|
|
1109
|
+
|
|
1110
|
+
def run_python(self, source: str, timeout: float | None) -> str:
|
|
1111
|
+
# The rendezvous program prints its answer, and ``eval`` returns what the
|
|
1112
|
+
# source left in __letify_value__, not what it printed: the print goes out
|
|
1113
|
+
# as a STDOUT frame and never reaches the caller. So the source runs with
|
|
1114
|
+
# its standard output captured into that name.
|
|
1115
|
+
captured = _capture_stdout(source)
|
|
1116
|
+
value, _ = bridge.request(
|
|
1117
|
+
{"op": "eval", "source": captured}, timeout=timeout
|
|
1118
|
+
)
|
|
1119
|
+
return value if isinstance(value, str) else str(value or "")
|
|
1120
|
+
|
|
1121
|
+
rendezvous = KaggleRendezvous()
|
|
1122
|
+
# A Kaggle image ships no tailcat, so the request has to carry an installed path.
|
|
1123
|
+
rendezvous._prepare_tailcat = prepare_tailcat(
|
|
1124
|
+
rendezvous.run_python, self.config.option("tailcat_binary")
|
|
1125
|
+
)
|
|
1126
|
+
return rendezvous
|
|
1127
|
+
|
|
1128
|
+
def target_over(self, channel: Any) -> Any:
|
|
1129
|
+
"""What the strategies need, with no address because there is none to dial."""
|
|
1130
|
+
from ..transport import nat
|
|
1131
|
+
from ..transport.strategies import Target
|
|
1132
|
+
|
|
1133
|
+
user = self.config.option("user")
|
|
1134
|
+
return Target(
|
|
1135
|
+
alias=self.alias,
|
|
1136
|
+
rendezvous=self.rendezvous_over(channel),
|
|
1137
|
+
remote_python=self.remote_python,
|
|
1138
|
+
stun=nat.DEFAULT_STUN,
|
|
1139
|
+
workspace=self.workspace_root,
|
|
1140
|
+
# The rendezvous authorized this key for root on the VM, so the link has to
|
|
1141
|
+
# offer it and name that user. Without them the VM closes the banner exchange.
|
|
1142
|
+
key=_private_key(),
|
|
1143
|
+
user=user if isinstance(user, str) and user else "root",
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
def strategies(self) -> list[Any]:
|
|
1147
|
+
"""The strategies a session with no address can use, in rank order."""
|
|
1148
|
+
from ..transport.strategies import ProviderFallback, QuicUDP, TailcatUDP, TCPPunch
|
|
1149
|
+
|
|
1150
|
+
return [TCPPunch(), TailcatUDP(), QuicUDP(), ProviderFallback(rank=6)]
|
|
1151
|
+
|
|
1152
|
+
def link_over(self, channel: Any) -> Any:
|
|
1153
|
+
"""Race the strategies over the bridge and return the link that wins."""
|
|
1154
|
+
from ..transport.announce import printer
|
|
1155
|
+
from ..transport.pipeline import LinkCache, Pipeline, network_fingerprint
|
|
1156
|
+
|
|
1157
|
+
target = self.target_over(channel)
|
|
1158
|
+
return Pipeline(
|
|
1159
|
+
self.strategies(),
|
|
1160
|
+
target=target,
|
|
1161
|
+
alias=self.alias,
|
|
1162
|
+
cache=LinkCache(self.alias),
|
|
1163
|
+
fingerprint=lambda: network_fingerprint(target.stun),
|
|
1164
|
+
say=printer(self.announce),
|
|
1165
|
+
floor=self.link_floor,
|
|
1166
|
+
).connect()
|
|
1167
|
+
|
|
1168
|
+
@property
|
|
1169
|
+
def link_floor(self) -> Any:
|
|
1170
|
+
"""The slowest probe this account accepts, as a shell account's does."""
|
|
1171
|
+
from ..transport.pipeline import MIB, LinkFloor
|
|
1172
|
+
|
|
1173
|
+
default = LinkFloor.default()
|
|
1174
|
+
min_mib = self.config.option("min_mib_per_s")
|
|
1175
|
+
max_rtt = self.config.option("max_rtt_ms")
|
|
1176
|
+
return LinkFloor(
|
|
1177
|
+
min_bps=float(min_mib) * MIB if isinstance(min_mib, (int, float)) else default.min_bps,
|
|
1178
|
+
max_rtt_ms=float(max_rtt) if isinstance(max_rtt, (int, float)) else default.max_rtt_ms,
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
@property
|
|
1182
|
+
def remote_python(self) -> str:
|
|
1183
|
+
value = self.config.option("remote_python")
|
|
1184
|
+
return str(value) if value else "python3"
|
|
1185
|
+
|
|
1186
|
+
def channel_over(self, bridge: Any, *, name: str) -> Channel:
|
|
1187
|
+
"""A worker over the chosen link, or the bridge when nothing was chosen.
|
|
1188
|
+
|
|
1189
|
+
Spec "Kaggle runtimes", An SSH link over the kernel: a network that cannot punch
|
|
1190
|
+
keeps the bridge, which is what every Kaggle session used until now.
|
|
1191
|
+
"""
|
|
1192
|
+
import shlex as _shlex
|
|
1193
|
+
|
|
1194
|
+
from ..protocol.worker import BOOTSTRAP
|
|
1195
|
+
from ..runtime.channel import PersistentChannel
|
|
1196
|
+
|
|
1197
|
+
try:
|
|
1198
|
+
link = self.link_over(bridge)
|
|
1199
|
+
except Exception:
|
|
1200
|
+
return bridge
|
|
1201
|
+
self._links[name] = link
|
|
1202
|
+
return PersistentChannel(
|
|
1203
|
+
link.ssh_command(f"{self.remote_python} -u -c {_shlex.quote(BOOTSTRAP)}"),
|
|
1204
|
+
name=name,
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1042
1207
|
def open_channel(self, runtime: Runtime) -> Channel:
|
|
1043
1208
|
"""Start a session from the cookie and open one worker in one cell of it.
|
|
1044
1209
|
|
|
@@ -1065,7 +1230,9 @@ class Kaggle(Provider):
|
|
|
1065
1230
|
session=session,
|
|
1066
1231
|
)
|
|
1067
1232
|
self._kernels[runtime.name] = (session, kernel, channel, run, notebook, slug)
|
|
1068
|
-
|
|
1233
|
+
# Spec "Kaggle runtimes", An SSH link over the kernel: the bridge is the
|
|
1234
|
+
# rendezvous, and the session rides a link when one is chosen over it.
|
|
1235
|
+
return self.channel_over(channel, name=runtime.name)
|
|
1069
1236
|
|
|
1070
1237
|
def stop(self, runtime: Runtime) -> None:
|
|
1071
1238
|
"""Close the bridge, delete the kernel, cancel the session run, delete the notebook.
|
|
@@ -1081,6 +1248,12 @@ class Kaggle(Provider):
|
|
|
1081
1248
|
if held is None:
|
|
1082
1249
|
return
|
|
1083
1250
|
session, kernel, channel, run, notebook, slug = held
|
|
1251
|
+
link = self._links.pop(runtime.name, None)
|
|
1252
|
+
if link is not None:
|
|
1253
|
+
try:
|
|
1254
|
+
link.close()
|
|
1255
|
+
except Exception:
|
|
1256
|
+
pass
|
|
1084
1257
|
try:
|
|
1085
1258
|
channel.close()
|
|
1086
1259
|
except OSError:
|
|
@@ -143,6 +143,20 @@ class Local(Provider):
|
|
|
143
143
|
|
|
144
144
|
return PersistentChannel([str(interpreter), "-u", "-c", BOOTSTRAP], name=runtime.name)
|
|
145
145
|
|
|
146
|
+
def transfer_channel(self, runtime: Runtime | None) -> Channel:
|
|
147
|
+
"""Another worker subprocess for one transfer stream.
|
|
148
|
+
|
|
149
|
+
Spec "Several connections at once" is about kernel connections, and here there is
|
|
150
|
+
no network to make one over. A subprocess is the local equivalent, and having it
|
|
151
|
+
lets the parallel placement be exercised over the real framed path.
|
|
152
|
+
"""
|
|
153
|
+
from ..protocol.worker import BOOTSTRAP
|
|
154
|
+
from ..runtime.channel import PersistentChannel
|
|
155
|
+
|
|
156
|
+
return PersistentChannel(
|
|
157
|
+
[str(sys.executable), "-u", "-c", BOOTSTRAP], name=f"{self.alias}-transfer"
|
|
158
|
+
)
|
|
159
|
+
|
|
146
160
|
def device_channel(self, runtime: Runtime) -> Channel:
|
|
147
161
|
"""The session's call worker subprocess, which hosts the PyTorch device executor."""
|
|
148
162
|
assert runtime.channel is not None
|
|
@@ -140,6 +140,28 @@ class Shell(Provider):
|
|
|
140
140
|
value = self.config.option("reverse_ssh")
|
|
141
141
|
return dict(value) if isinstance(value, Mapping) else None
|
|
142
142
|
|
|
143
|
+
@property
|
|
144
|
+
def connect_back(self) -> dict[str, Any] | None:
|
|
145
|
+
"""Where the remote can reach this machine, from ``connect_back``.
|
|
146
|
+
|
|
147
|
+
Spec "Connect back". ``port`` is required; ``address`` is optional and the STUN
|
|
148
|
+
answer stands in for it when the port is forwarded here but the name is not known.
|
|
149
|
+
"""
|
|
150
|
+
value = self.config.option("connect_back")
|
|
151
|
+
if not isinstance(value, Mapping) or not value.get("port"):
|
|
152
|
+
return None
|
|
153
|
+
return dict(value)
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def port_mapping(self) -> bool:
|
|
157
|
+
"""Whether to ask the NAT for a forwarding, from ``port_mapping``.
|
|
158
|
+
|
|
159
|
+
Spec "Asking the NAT for a mapping": on by default, because a device that does not
|
|
160
|
+
speak one of the three protocols answers nothing and the attempt costs a second.
|
|
161
|
+
"""
|
|
162
|
+
value = self.config.option("port_mapping")
|
|
163
|
+
return True if value is None else bool(value)
|
|
164
|
+
|
|
143
165
|
@property
|
|
144
166
|
def link_floor(self) -> LinkFloor:
|
|
145
167
|
"""The slowest probe this account accepts, from ``min_mib_per_s`` and ``max_rtt_ms``.
|
|
@@ -231,17 +253,22 @@ class Shell(Provider):
|
|
|
231
253
|
def strategies(self) -> list[Strategy]:
|
|
232
254
|
"""The ranked strategy list. Reverse SSH joins only when the account sets it."""
|
|
233
255
|
from ..transport.strategies import (
|
|
256
|
+
ConnectBack,
|
|
234
257
|
DirectSSH,
|
|
235
258
|
ProviderFallback,
|
|
259
|
+
QuicUDP,
|
|
236
260
|
ReverseSSH,
|
|
237
261
|
TailcatUDP,
|
|
238
262
|
TCPPunch,
|
|
239
263
|
)
|
|
240
264
|
|
|
241
|
-
chosen: list[Strategy] = [DirectSSH()
|
|
265
|
+
chosen: list[Strategy] = [DirectSSH()]
|
|
266
|
+
if self.connect_back or self.port_mapping:
|
|
267
|
+
chosen.append(ConnectBack(mapping=self.port_mapping))
|
|
268
|
+
chosen.extend((TCPPunch(), TailcatUDP(), QuicUDP()))
|
|
242
269
|
if self.reverse_ssh:
|
|
243
270
|
chosen.append(ReverseSSH())
|
|
244
|
-
chosen.append(ProviderFallback(rank=
|
|
271
|
+
chosen.append(ProviderFallback(rank=7 if self.reverse_ssh else 6))
|
|
245
272
|
return chosen
|
|
246
273
|
|
|
247
274
|
def fallback(self, runtime: Runtime | None = None) -> Callable[[], Link] | None:
|
|
@@ -269,6 +296,7 @@ class Shell(Provider):
|
|
|
269
296
|
remote_python=self.remote_python,
|
|
270
297
|
rendezvous=self.rendezvous(runtime),
|
|
271
298
|
reverse_ssh=self.reverse_ssh,
|
|
299
|
+
connect_back=self.connect_back,
|
|
272
300
|
fallback=self.fallback(runtime),
|
|
273
301
|
stun=stun,
|
|
274
302
|
tailcat=self.tailcat_binary,
|
|
@@ -463,6 +491,42 @@ class Shell(Provider):
|
|
|
463
491
|
name=runtime.name,
|
|
464
492
|
)
|
|
465
493
|
|
|
494
|
+
def transfer_command(self, runtime: Runtime | None) -> list[str]:
|
|
495
|
+
"""An SSH command for one transfer connection, outside the multiplexed one.
|
|
496
|
+
|
|
497
|
+
Spec "Several connections at once": every SSH command letify builds carries
|
|
498
|
+
``ControlMaster=auto``, which puts further sessions inside the first connection.
|
|
499
|
+
A transfer stream has to be its own kernel connection, so it turns that off. A
|
|
500
|
+
punched link redials for each, which is already one connection per session.
|
|
501
|
+
"""
|
|
502
|
+
import shlex as _shlex
|
|
503
|
+
|
|
504
|
+
from ..protocol.worker import BOOTSTRAP
|
|
505
|
+
|
|
506
|
+
link = self.link(runtime)
|
|
507
|
+
command = link.ssh_command(
|
|
508
|
+
self.remote_command(f"{self.remote_python} -u -c {_shlex.quote(BOOTSTRAP)}")
|
|
509
|
+
)
|
|
510
|
+
kept: list[str] = []
|
|
511
|
+
index = 0
|
|
512
|
+
while index < len(command):
|
|
513
|
+
part = command[index]
|
|
514
|
+
if part == "-o" and index + 1 < len(command):
|
|
515
|
+
option = command[index + 1]
|
|
516
|
+
if option.startswith(("ControlMaster=", "ControlPath=", "ControlPersist=")):
|
|
517
|
+
index += 2
|
|
518
|
+
continue
|
|
519
|
+
kept.append(part)
|
|
520
|
+
index += 1
|
|
521
|
+
# After the program name, so they are options rather than the remote command.
|
|
522
|
+
return [kept[0], "-o", "ControlMaster=no", "-o", "ControlPath=none", *kept[1:]]
|
|
523
|
+
|
|
524
|
+
def transfer_channel(self, runtime: Runtime | None) -> Channel:
|
|
525
|
+
"""One channel for one transfer stream, on a connection of its own."""
|
|
526
|
+
from ..runtime.channel import PersistentChannel
|
|
527
|
+
|
|
528
|
+
return PersistentChannel(self.transfer_command(runtime), name=f"{self.alias}-transfer")
|
|
529
|
+
|
|
466
530
|
def device_channel(self, runtime: Runtime) -> Channel:
|
|
467
531
|
"""The session's call channel, which hosts the PyTorch device executor."""
|
|
468
532
|
from ..errors import UnsupportedMode
|