letify 2.4.4__tar.gz → 2.4.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {letify-2.4.4 → letify-2.4.5}/PKG-INFO +1 -1
- {letify-2.4.4 → letify-2.4.5}/letify/__init__.py +1 -1
- {letify-2.4.4 → letify-2.4.5}/letify/declare/function.py +69 -11
- {letify-2.4.4 → letify-2.4.5}/letify/protocol/worker.py +20 -0
- {letify-2.4.4 → letify-2.4.5}/letify/runtime/bootstrap.py +7 -2
- {letify-2.4.4 → letify-2.4.5}/letify/runtime/session.py +21 -4
- {letify-2.4.4 → letify-2.4.5}/letify-core/Cargo.lock +3 -3
- {letify-2.4.4 → letify-2.4.5}/letify-core/Cargo.toml +1 -1
- {letify-2.4.4 → letify-2.4.5}/letify-ext/package-lock.json +2 -2
- {letify-2.4.4 → letify-2.4.5}/letify-ext/package.json +1 -1
- {letify-2.4.4 → letify-2.4.5}/pyproject.toml +1 -1
- {letify-2.4.4 → letify-2.4.5}/tests/test_distributed.py +62 -12
- {letify-2.4.4 → letify-2.4.5}/tests/test_kaggle.py +45 -0
- {letify-2.4.4 → letify-2.4.5}/.github/workflows/ci.yml +0 -0
- {letify-2.4.4 → letify-2.4.5}/.github/workflows/docs.yml +0 -0
- {letify-2.4.4 → letify-2.4.5}/.github/workflows/publish.yml +0 -0
- {letify-2.4.4 → letify-2.4.5}/.gitignore +0 -0
- {letify-2.4.4 → letify-2.4.5}/.researchtree +0 -0
- {letify-2.4.4 → letify-2.4.5}/LICENSE +0 -0
- {letify-2.4.4 → letify-2.4.5}/README.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/01-getting-started.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/02-providers.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/05-concurrency.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/06-cost.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/08-releasing.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/README.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/guide/ko/README.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/locales/README_ko.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/docs/mkdocs.yml +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/README.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/cli.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/config/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/config/inventory.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/config/login.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/config/schema.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/config/secrets.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/config/writer.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/declare/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/declare/cache.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/declare/env.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/declare/instance.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/declare/replicas.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/errors.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/install.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/launcher.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/paths.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/protocol/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/protocol/codec.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/protocol/driver.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/protocol/handle.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/protocol/wire.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/base.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/colab.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/colab_files.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/elice.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/kaggle.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/local.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/modal.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/modal_adapter.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/naming.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/shell.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/tunnel.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/providers/usage.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/capability.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/client.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/cuda.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/executor.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/frames.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/guard.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/tensor.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/trace.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/device/value.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/loader.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/remoting/probe.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/render.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/runtime/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/runtime/channel.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/runtime/lease.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/runtime/pool.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/runtime/telemetry.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/backends/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/backends/filesystem.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/backends/google_auth.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/backends/layout.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/backends/objects.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/cas.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/pathdata.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/sendorder.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/store/volume.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/stubs.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/tools.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/__init__.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/agent.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/announce.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/link.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/nat.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/pipeline.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/portmap.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/probe.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/quic.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/rendezvous.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/setup.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/sshopts.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify/transport/strategies.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/.gitignore +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/build.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/.vscodeignore +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/LICENSE +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/README.md +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/media/icon.svg +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/src/cli.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/src/extension.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/src/history.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/src/model.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/src/poll.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/src/render.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/test/model.test.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify-ext/tsconfig.json +0 -0
- {letify-2.4.4 → letify-2.4.5}/letify_providers.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/scripts/sync-main.sh +0 -0
- {letify-2.4.4 → letify-2.4.5}/scripts/version.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/conftest.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/fake_modal_adapter.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_cli.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_cli_output.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_colab_keepalive.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_config.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_core.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_data.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_data_streaming.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_declare.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_device.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_editor_sessions.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_elice.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_frames.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_install.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_kaggle_link.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_link_log.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_notebook.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_parallel_placement.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_paths.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_portmap.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_protocol.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_providers.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_quic.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_remoting.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_render.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_runtime.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_ship.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_store.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_strategies.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_stubs.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_transport.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_tunnel_setup.py +0 -0
- {letify-2.4.4 → letify-2.4.5}/tests/test_version.py +0 -0
|
@@ -65,8 +65,11 @@ class Function(Generic[R]):
|
|
|
65
65
|
self.data_order = data_order
|
|
66
66
|
self.data_first_wave = data_first_wave
|
|
67
67
|
self.is_async = inspect.iscoroutinefunction(fn)
|
|
68
|
-
#: Spec "Replicas": one Instance per replica, in the order the list named them
|
|
69
|
-
|
|
68
|
+
#: Spec "Replicas": one Instance per replica, in the order the list named them,
|
|
69
|
+
#: each carrying the declared placement so a provider is not told it is local.
|
|
70
|
+
self.replicas = tuple(
|
|
71
|
+
instance._placed(self.host) for instance in _replicas(device, self.host)
|
|
72
|
+
)
|
|
70
73
|
self.device = self._place(self.replicas[0] if self.replicas else device)
|
|
71
74
|
update_wrapper(self, fn)
|
|
72
75
|
|
|
@@ -139,18 +142,21 @@ class Function(Generic[R]):
|
|
|
139
142
|
def _run(self, args: tuple, kwargs: dict) -> Any:
|
|
140
143
|
launcher = self.launcher
|
|
141
144
|
if self.host is Host.distributed:
|
|
142
|
-
|
|
143
|
-
# yet. Running the first replica alone would bill one runtime for a
|
|
144
|
-
# declaration that asked for several, without saying so.
|
|
145
|
-
raise UnsupportedMode(
|
|
146
|
-
f"host=letify.distributed declares {len(self.replicas)} replicas and "
|
|
147
|
-
f"letify cannot run them yet, so the call is refused rather than run on "
|
|
148
|
-
f"one of them. Use host=letify.remote with a single device until it is "
|
|
149
|
-
f"finished."
|
|
150
|
-
)
|
|
145
|
+
return self._run_replicas(args, kwargs)
|
|
151
146
|
instance = launcher.resolve(self.device)
|
|
152
147
|
if instance.placement is Host.local:
|
|
153
148
|
return self._run_here(instance, args, kwargs)
|
|
149
|
+
return self._run_one(instance, args, kwargs)
|
|
150
|
+
|
|
151
|
+
def _run_one(
|
|
152
|
+
self, instance: Any, args: tuple, kwargs: dict, replica: dict | None = None
|
|
153
|
+
) -> Any:
|
|
154
|
+
"""Run the body on one runtime, retrying on an infrastructure failure.
|
|
155
|
+
|
|
156
|
+
One replica of a distributed call runs through here too, so a replica gets the
|
|
157
|
+
same retries and the same reporting as a call that named a single device.
|
|
158
|
+
"""
|
|
159
|
+
launcher = self.launcher
|
|
154
160
|
last: Exception | None = None
|
|
155
161
|
|
|
156
162
|
for attempt in range(self.retries + 1):
|
|
@@ -166,6 +172,7 @@ class Function(Generic[R]):
|
|
|
166
172
|
timeout=self.timeout,
|
|
167
173
|
data_order=self.data_order,
|
|
168
174
|
data_first_wave=self.data_first_wave,
|
|
175
|
+
replica=_with_rate(replica, runtime),
|
|
169
176
|
)
|
|
170
177
|
except (RuntimeFailure, ProtocolError) as failure:
|
|
171
178
|
# The session misbehaved rather than the user's code, so this runtime
|
|
@@ -200,6 +207,40 @@ class Function(Generic[R]):
|
|
|
200
207
|
|
|
201
208
|
raise RuntimeLost(str(last)) # pragma: no cover
|
|
202
209
|
|
|
210
|
+
def _run_replicas(self, args: tuple, kwargs: dict) -> list:
|
|
211
|
+
"""Run the body once per replica, and answer with their values in rank order.
|
|
212
|
+
|
|
213
|
+
Spec "Replicas": each replica is told which one it is, the call waits for all of
|
|
214
|
+
them, and a replica that fails fails the call, as a single remote call's failure
|
|
215
|
+
does. The replicas run at the same time, because running them in turn would make
|
|
216
|
+
a reduction between them impossible.
|
|
217
|
+
"""
|
|
218
|
+
import threading
|
|
219
|
+
|
|
220
|
+
world = len(self.replicas)
|
|
221
|
+
values: list = [None] * world
|
|
222
|
+
failures: list[BaseException] = []
|
|
223
|
+
|
|
224
|
+
def run(rank: int, instance: Any) -> None:
|
|
225
|
+
try:
|
|
226
|
+
values[rank] = self._run_one(
|
|
227
|
+
instance, args, kwargs, {"rank": rank, "world_size": world}
|
|
228
|
+
)
|
|
229
|
+
except BaseException as exc: # reported after every replica has stopped
|
|
230
|
+
failures.append(exc)
|
|
231
|
+
|
|
232
|
+
threads = [
|
|
233
|
+
threading.Thread(target=run, args=(rank, instance), daemon=True)
|
|
234
|
+
for rank, instance in enumerate(self.replicas)
|
|
235
|
+
]
|
|
236
|
+
for thread in threads:
|
|
237
|
+
thread.start()
|
|
238
|
+
for thread in threads:
|
|
239
|
+
thread.join()
|
|
240
|
+
if failures:
|
|
241
|
+
raise failures[0]
|
|
242
|
+
return values
|
|
243
|
+
|
|
203
244
|
def _run_here(self, instance: Any, args: tuple, kwargs: dict) -> Any:
|
|
204
245
|
"""Run the body in this process with its PyTorch operators on the runtime's device.
|
|
205
246
|
|
|
@@ -261,6 +302,23 @@ def _report_deferred(delta: Any) -> None:
|
|
|
261
302
|
)
|
|
262
303
|
|
|
263
304
|
|
|
305
|
+
def _with_rate(replica: dict | None, runtime: Any) -> dict | None:
|
|
306
|
+
"""The replica's numbers plus the rate its own link measured, for ``local_steps``.
|
|
307
|
+
|
|
308
|
+
Spec "How often to synchronize": the rate is the quantity only letify has, and it is
|
|
309
|
+
this replica's link rather than the job's, so it travels with the call.
|
|
310
|
+
"""
|
|
311
|
+
if replica is None:
|
|
312
|
+
return None
|
|
313
|
+
rate = None
|
|
314
|
+
try:
|
|
315
|
+
link = runtime.provider.link(runtime)
|
|
316
|
+
rate = getattr(link, "upload_bps", None)
|
|
317
|
+
except Exception:
|
|
318
|
+
rate = None
|
|
319
|
+
return {**replica, "rate_bps": rate}
|
|
320
|
+
|
|
321
|
+
|
|
264
322
|
def _replicas(device: Any, host: Host) -> tuple[Instance, ...]:
|
|
265
323
|
"""The replicas a declaration names, or empty when it names a single device.
|
|
266
324
|
|
|
@@ -316,6 +316,7 @@ def _reply(stream, outcome):
|
|
|
316
316
|
|
|
317
317
|
def _op_call(request):
|
|
318
318
|
data = request.pop("data", None)
|
|
319
|
+
_letify_replica(request.pop("replica", None))
|
|
319
320
|
modules_dir = request.pop("modules_dir", None)
|
|
320
321
|
if modules_dir:
|
|
321
322
|
modules_dir = os.path.expanduser(modules_dir)
|
|
@@ -349,6 +350,25 @@ def _op_call(request):
|
|
|
349
350
|
_DATA_WRITTEN = {}
|
|
350
351
|
|
|
351
352
|
|
|
353
|
+
def _letify_replica(replica):
|
|
354
|
+
"""Tell the letify in this runtime which replica this call is, or that it is none.
|
|
355
|
+
|
|
356
|
+
Spec "Replicas": the body reads its rank through letify, which is installed here by
|
|
357
|
+
the environment, so the numbers are set on the module rather than passed through the
|
|
358
|
+
user's signature.
|
|
359
|
+
"""
|
|
360
|
+
try:
|
|
361
|
+
from letify.declare import replicas as _replicas
|
|
362
|
+
except Exception:
|
|
363
|
+
return
|
|
364
|
+
if replica is None:
|
|
365
|
+
_replicas._WHO.rank = _replicas._WHO.world_size = _replicas._WHO.rate_bps = None
|
|
366
|
+
return
|
|
367
|
+
_replicas._WHO.rank = replica.get("rank")
|
|
368
|
+
_replicas._WHO.world_size = replica.get("world_size")
|
|
369
|
+
_replicas._WHO.rate_bps = replica.get("rate_bps")
|
|
370
|
+
|
|
371
|
+
|
|
352
372
|
def _data_hash_file(path):
|
|
353
373
|
"""The file digest of a file on the runtime: blake3 when present, else blake2b."""
|
|
354
374
|
hasher = _data_hasher()
|
|
@@ -133,7 +133,7 @@ def project_dir(workspace_root: str, env: Env) -> str:
|
|
|
133
133
|
#: Reads the cards a runtime has, without nvidia-smi, which a Kaggle image lacks on PATH.
|
|
134
134
|
#: Spec "Kaggle", the accelerators.
|
|
135
135
|
CARD_SOURCE = (
|
|
136
|
-
"import glob\n"
|
|
136
|
+
"import glob, os\n"
|
|
137
137
|
"__letify_cards = []\n"
|
|
138
138
|
"for _letify_path in sorted(glob.glob('/proc/driver/nvidia/gpus/*/information')):\n"
|
|
139
139
|
" try:\n"
|
|
@@ -144,7 +144,12 @@ CARD_SOURCE = (
|
|
|
144
144
|
" break\n"
|
|
145
145
|
" except OSError:\n"
|
|
146
146
|
" pass\n"
|
|
147
|
-
|
|
147
|
+
# /proc can list the right cards on a session whose driver cannot be used, and the
|
|
148
|
+
# first .to('cuda') then raises minutes into the call. Spec "Kaggle".
|
|
149
|
+
"__letify_value__ = {\n"
|
|
150
|
+
" 'cards': __letify_cards,\n"
|
|
151
|
+
" 'driver': os.path.exists('/dev/nvidiactl'),\n"
|
|
152
|
+
"}\n"
|
|
148
153
|
)
|
|
149
154
|
|
|
150
155
|
|
|
@@ -269,6 +269,7 @@ class Runtime:
|
|
|
269
269
|
timeout: float | None = None,
|
|
270
270
|
data_order: Any = None,
|
|
271
271
|
data_first_wave: int | None = None,
|
|
272
|
+
replica: dict[str, Any] | None = None,
|
|
272
273
|
) -> tuple[Any, str]:
|
|
273
274
|
"""Run one declared function inside this runtime.
|
|
274
275
|
|
|
@@ -283,7 +284,9 @@ class Runtime:
|
|
|
283
284
|
fn, args, kwargs, timeout=timeout, modules=self.env.ship_modules
|
|
284
285
|
)
|
|
285
286
|
args, kwargs = self._externalize(args, kwargs)
|
|
286
|
-
return self._call_with_data(
|
|
287
|
+
return self._call_with_data(
|
|
288
|
+
fn, args, kwargs, timeout, data_order, data_first_wave, replica
|
|
289
|
+
)
|
|
287
290
|
|
|
288
291
|
def _call_with_data(
|
|
289
292
|
self,
|
|
@@ -293,6 +296,7 @@ class Runtime:
|
|
|
293
296
|
timeout: float | None,
|
|
294
297
|
data_order: Any = None,
|
|
295
298
|
data_first_wave: int | None = None,
|
|
299
|
+
replica: dict[str, Any] | None = None,
|
|
296
300
|
) -> tuple[Any, str]:
|
|
297
301
|
"""Pickle a call, place the first wave of its data, and send the rest beside it.
|
|
298
302
|
|
|
@@ -317,6 +321,10 @@ class Runtime:
|
|
|
317
321
|
ship_digests.add(digest)
|
|
318
322
|
head, buffers = protocol.dumps_call_parts(fn, args, kwargs, data=collector)
|
|
319
323
|
request: dict[str, Any] = {"op": "call", "payload": head, "buffers": buffers}
|
|
324
|
+
if replica is not None:
|
|
325
|
+
# Spec "Replicas": the body reads its rank on the runtime, not here, so the
|
|
326
|
+
# numbers travel with the call rather than being set on a client thread.
|
|
327
|
+
request["replica"] = replica
|
|
320
328
|
blobs = f"{root}/data/blobs"
|
|
321
329
|
stream: pathdata.Stream | None = None
|
|
322
330
|
if self.env.ship_modules:
|
|
@@ -708,16 +716,25 @@ class Runtime:
|
|
|
708
716
|
return
|
|
709
717
|
fragment, count = expected
|
|
710
718
|
try:
|
|
711
|
-
|
|
719
|
+
found = self.eval(bootstrap.CARD_SOURCE, timeout=120)
|
|
712
720
|
except Exception:
|
|
713
721
|
# Reading /proc is not worth failing a session over when it cannot be read.
|
|
714
722
|
return
|
|
715
|
-
|
|
723
|
+
found = found if isinstance(found, dict) else {"cards": found, "driver": True}
|
|
724
|
+
cards = [str(card) for card in found.get("cards") or ()]
|
|
725
|
+
driver = bool(found.get("driver"))
|
|
716
726
|
matched = [card for card in cards if fragment.casefold() in card.casefold()]
|
|
717
|
-
if len(matched) == count and len(cards) == count:
|
|
727
|
+
if len(matched) == count and len(cards) == count and driver:
|
|
718
728
|
return
|
|
719
729
|
asked = getattr(instance, "accelerator", None) or fragment
|
|
720
730
|
arrived = ", ".join(cards) if cards else "no GPU at all"
|
|
731
|
+
if cards and not driver:
|
|
732
|
+
# Spec "Kaggle": the cards are right and the driver cannot be used, which the
|
|
733
|
+
# first .to("cuda") would have reported minutes into the call instead.
|
|
734
|
+
arrived = (
|
|
735
|
+
f"{arrived}, and no usable driver: /dev/nvidiactl is not there, so CUDA "
|
|
736
|
+
f"cannot start"
|
|
737
|
+
)
|
|
721
738
|
message = (
|
|
722
739
|
f"{self.name}: {self.provider.alias} was asked for {count} x {asked} and the "
|
|
723
740
|
f"session has {arrived}. A declaration says where a function runs, so the "
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.4.
|
|
141
|
+
version = "2.4.5"
|
|
142
142
|
dependencies = [
|
|
143
143
|
"letify-wire",
|
|
144
144
|
"libloading",
|
|
@@ -146,7 +146,7 @@ dependencies = [
|
|
|
146
146
|
|
|
147
147
|
[[package]]
|
|
148
148
|
name = "letify-driver"
|
|
149
|
-
version = "2.4.
|
|
149
|
+
version = "2.4.5"
|
|
150
150
|
dependencies = [
|
|
151
151
|
"letify-wire",
|
|
152
152
|
]
|
|
@@ -164,7 +164,7 @@ dependencies = [
|
|
|
164
164
|
|
|
165
165
|
[[package]]
|
|
166
166
|
name = "letify-wire"
|
|
167
|
-
version = "2.4.
|
|
167
|
+
version = "2.4.5"
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "libc"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.4.
|
|
9
|
+
"version": "2.4.5",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@types/node": "^18.19.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "letify-status",
|
|
3
3
|
"displayName": "letify Status",
|
|
4
4
|
"description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
|
|
5
|
-
"version": "2.4.
|
|
5
|
+
"version": "2.4.5",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -127,23 +127,73 @@ def test_local_steps_refuses_a_link_that_was_never_measured() -> None:
|
|
|
127
127
|
assert "probe" in str(failure.value)
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
def
|
|
131
|
-
"""Spec "Replicas": the
|
|
130
|
+
def test_every_replica_runs_the_body_and_knows_which_one_it_is(launcher_from) -> None:
|
|
131
|
+
"""Spec "Replicas": the same body once per device, and the values come back in rank order."""
|
|
132
|
+
let = launcher_from('[a]\nkind = "local"\n\n[b]\nkind = "local"\n')
|
|
133
|
+
|
|
134
|
+
@let.function(
|
|
135
|
+
device=[let.providers.a.CPU, let.providers.b.CPU], host=letify.distributed
|
|
136
|
+
)
|
|
137
|
+
def who() -> tuple[int, int]:
|
|
138
|
+
import letify as inner
|
|
139
|
+
|
|
140
|
+
return inner.rank(), inner.world_size()
|
|
141
|
+
|
|
142
|
+
assert who() == [(0, 2), (1, 2)]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_one_replica_still_comes_back_as_a_list(launcher_from) -> None:
|
|
146
|
+
"""Spec "Replicas": a single instance is one replica, so the shape does not change."""
|
|
147
|
+
let = launcher_from('[a]\nkind = "local"\n')
|
|
148
|
+
|
|
149
|
+
@let.function(device=let.providers.a.CPU, host=letify.distributed)
|
|
150
|
+
def who() -> tuple[int, int]:
|
|
151
|
+
import letify as inner
|
|
152
|
+
|
|
153
|
+
return inner.rank(), inner.world_size()
|
|
154
|
+
|
|
155
|
+
assert who() == [(0, 1)]
|
|
156
|
+
|
|
132
157
|
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
def test_a_replica_that_fails_fails_the_call(launcher_from) -> None:
|
|
159
|
+
"""Spec "Replicas": a replica that cannot run fails the call, as one remote call does."""
|
|
160
|
+
let = launcher_from('[a]\nkind = "local"\n\n[b]\nkind = "local"\n')
|
|
161
|
+
|
|
162
|
+
@let.function(
|
|
163
|
+
device=[let.providers.a.CPU, let.providers.b.CPU], host=letify.distributed
|
|
164
|
+
)
|
|
165
|
+
def breaks() -> int:
|
|
166
|
+
import letify as inner
|
|
167
|
+
|
|
168
|
+
if inner.rank() == 1:
|
|
169
|
+
raise ValueError("the second replica refused")
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
with pytest.raises(letify.RemoteError) as failure:
|
|
173
|
+
breaks()
|
|
174
|
+
assert "the second replica refused" in str(failure.value)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_a_replica_does_not_need_torch_on_this_machine(launcher_from, no_module) -> None:
|
|
178
|
+
"""Spec "Replicas": the placement is not local, so nothing asks for PyTorch here.
|
|
179
|
+
|
|
180
|
+
The replicas were handed to the provider without the declared placement folded in,
|
|
181
|
+
so their placement read as the default, local, and `check_mode` asked for PyTorch on
|
|
182
|
+
the client. It passed on a machine that has torch and failed on CI, which does not.
|
|
135
183
|
"""
|
|
136
184
|
let = launcher_from('[a]\nkind = "local"\n\n[b]\nkind = "local"\n')
|
|
137
185
|
|
|
138
186
|
@let.function(
|
|
139
187
|
device=[let.providers.a.CPU, let.providers.b.CPU], host=letify.distributed
|
|
140
188
|
)
|
|
141
|
-
def
|
|
142
|
-
|
|
189
|
+
def who() -> int:
|
|
190
|
+
import letify as inner
|
|
143
191
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
assert
|
|
148
|
-
assert
|
|
149
|
-
|
|
192
|
+
return inner.rank()
|
|
193
|
+
|
|
194
|
+
no_module("torch")
|
|
195
|
+
assert who() == [0, 1]
|
|
196
|
+
assert [instance.placement for instance in who.replicas] == [
|
|
197
|
+
letify.distributed,
|
|
198
|
+
letify.distributed,
|
|
199
|
+
]
|
|
@@ -1451,3 +1451,48 @@ def test_a_refused_run_says_how_long_ago_the_last_session_was_made(
|
|
|
1451
1451
|
assert "too soon" in detail
|
|
1452
1452
|
path.write_text("", encoding="utf-8")
|
|
1453
1453
|
assert refusal_detail("kaggle_a") == ""
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def test_a_session_with_the_cards_and_no_driver_is_refused() -> None:
|
|
1457
|
+
"""Spec "Kaggle": the cards can be listed on a session whose driver cannot be used.
|
|
1458
|
+
|
|
1459
|
+
A live T4 session passed the card check and then raised "Found no NVIDIA driver on
|
|
1460
|
+
your system" at the first .to("cuda"), minutes into the call.
|
|
1461
|
+
"""
|
|
1462
|
+
from letify.errors import InsufficientDevices
|
|
1463
|
+
from letify.runtime.session import Runtime
|
|
1464
|
+
|
|
1465
|
+
provider = kaggle_provider()
|
|
1466
|
+
session = object.__new__(Runtime)
|
|
1467
|
+
session.name = "letify-t4-fc1bf0"
|
|
1468
|
+
session.provider = provider
|
|
1469
|
+
session.instance = type("I", (), {"gpu": "T4", "accelerator": "T4"})()
|
|
1470
|
+
answer = {"cards": ["Tesla T4", "Tesla T4"], "driver": False}
|
|
1471
|
+
monkey = lambda self, source, timeout=None: answer # noqa: E731
|
|
1472
|
+
Runtime.eval, original = monkey, Runtime.eval
|
|
1473
|
+
try:
|
|
1474
|
+
with pytest.raises(InsufficientDevices) as failure:
|
|
1475
|
+
session.check_devices()
|
|
1476
|
+
finally:
|
|
1477
|
+
Runtime.eval = original
|
|
1478
|
+
message = str(failure.value)
|
|
1479
|
+
assert "driver" in message
|
|
1480
|
+
assert "Tesla T4" in message
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
def test_a_session_with_the_cards_and_a_driver_passes() -> None:
|
|
1484
|
+
"""Spec: both halves have to hold, and then the session is used."""
|
|
1485
|
+
from letify.runtime.session import Runtime
|
|
1486
|
+
|
|
1487
|
+
provider = kaggle_provider()
|
|
1488
|
+
session = object.__new__(Runtime)
|
|
1489
|
+
session.name = "letify-t4-ok"
|
|
1490
|
+
session.provider = provider
|
|
1491
|
+
session.instance = type("I", (), {"gpu": "T4", "accelerator": "T4"})()
|
|
1492
|
+
answer = {"cards": ["Tesla T4", "Tesla T4"], "driver": True}
|
|
1493
|
+
monkey = lambda self, source, timeout=None: answer # noqa: E731
|
|
1494
|
+
Runtime.eval, original = monkey, Runtime.eval
|
|
1495
|
+
try:
|
|
1496
|
+
session.check_devices()
|
|
1497
|
+
finally:
|
|
1498
|
+
Runtime.eval = original
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.4.4 → letify-2.4.5}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-2.4.4 → letify-2.4.5}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-2.4.4 → letify-2.4.5}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md"
RENAMED
|
File without changes
|
|
File without changes
|
{letify-2.4.4 → letify-2.4.5}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md"
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|