letify 2.8.6__tar.gz → 2.8.7__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.8.7/.github/workflows/publish-crates.yml +89 -0
- letify-2.8.7/.github/workflows/reserve-crates.yml +77 -0
- {letify-2.8.6 → letify-2.8.7}/.gitignore +2 -1
- {letify-2.8.6 → letify-2.8.7}/CLAUDE.md +2 -2
- {letify-2.8.6 → letify-2.8.7}/PKG-INFO +6 -9
- {letify-2.8.6 → letify-2.8.7}/PROJECT.md +4 -1
- {letify-2.8.6 → letify-2.8.7}/README.md +5 -8
- {letify-2.8.6 → letify-2.8.7}/docs/COMPONENT.md +4 -4
- {letify-2.8.6 → letify-2.8.7}/docs/NETWORK.md +1 -21
- {letify-2.8.6 → letify-2.8.7}/docs/SPEC.md +15 -10
- {letify-2.8.6 → letify-2.8.7}/docs/guide/03-execution-modes.md +1 -1
- {letify-2.8.6 → letify-2.8.7}/docs/guide/06-cost.md +1 -1
- {letify-2.8.6 → letify-2.8.7}/docs/guide/07-troubleshooting.md +1 -1
- {letify-2.8.6 → letify-2.8.7}/docs/guide/08-releasing.md +13 -2
- {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md +1 -1
- {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/06-/353/271/204/354/232/251.md +1 -1
- {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md +1 -1
- {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md +13 -2
- {letify-2.8.6 → letify-2.8.7}/docs/locales/README_ko.md +4 -7
- {letify-2.8.6 → letify-2.8.7}/letify/__init__.py +1 -1
- {letify-2.8.6 → letify-2.8.7}/letify/cli.py +0 -37
- letify-2.8.7/letify/remoting/__init__.py +22 -0
- {letify-2.8.6 → letify-2.8.7}/letify-core/Cargo.lock +3 -28
- letify-2.8.7/letify-core/Cargo.toml +14 -0
- letify-2.8.7/letify-core/build.py +75 -0
- letify-2.8.7/letify-core/letify/Cargo.toml +14 -0
- letify-2.8.7/letify-core/letify/README.md +21 -0
- letify-2.8.7/letify-core/letify/src/lib.rs +21 -0
- {letify-2.8.6 → letify-2.8.7}/letify-core/letify-quic/Cargo.toml +1 -0
- letify-2.8.7/letify-ext/LICENSE +201 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/package-lock.json +2 -2
- {letify-2.8.6 → letify-2.8.7}/letify-ext/package.json +1 -1
- {letify-2.8.6 → letify-2.8.7}/pyproject.toml +2 -2
- {letify-2.8.6 → letify-2.8.7}/scripts/version.py +1 -1
- {letify-2.8.6 → letify-2.8.7}/tests/test_cli.py +3 -17
- {letify-2.8.6 → letify-2.8.7}/tests/test_cli_output.py +0 -14
- {letify-2.8.6 → letify-2.8.7}/tests/test_providers.py +0 -1
- letify-2.8.7/tests/test_remoting.py +37 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_version.py +25 -0
- letify-2.8.6/letify/remoting/__init__.py +0 -28
- letify-2.8.6/letify/remoting/capability.py +0 -50
- letify-2.8.6/letify/remoting/loader.py +0 -96
- letify-2.8.6/letify/remoting/probe.py +0 -145
- letify-2.8.6/letify-core/Cargo.toml +0 -18
- letify-2.8.6/letify-core/build.py +0 -109
- letify-2.8.6/letify-core/letify-agent/Cargo.toml +0 -13
- letify-2.8.6/letify-core/letify-agent/src/driver.rs +0 -352
- letify-2.8.6/letify-core/letify-agent/src/main.rs +0 -317
- letify-2.8.6/letify-core/letify-driver/Cargo.toml +0 -18
- letify-2.8.6/letify-core/letify-driver/src/client.rs +0 -271
- letify-2.8.6/letify-core/letify-driver/src/exports.rs +0 -464
- letify-2.8.6/letify-core/letify-driver/src/image.rs +0 -106
- letify-2.8.6/letify-core/letify-driver/src/lib.rs +0 -90
- letify-2.8.6/letify-core/letify-driver/src/status.rs +0 -61
- letify-2.8.6/letify-core/letify-driver/src/table.rs +0 -206
- letify-2.8.6/letify-core/letify-wire/Cargo.toml +0 -9
- letify-2.8.6/letify-core/letify-wire/src/lib.rs +0 -957
- letify-2.8.6/letify-core/letify-wire/tests/throughput.rs +0 -133
- letify-2.8.6/tests/test_remoting.py +0 -340
- {letify-2.8.6 → letify-2.8.7}/.github/workflows/ci.yml +0 -0
- {letify-2.8.6 → letify-2.8.7}/.github/workflows/docs.yml +0 -0
- {letify-2.8.6 → letify-2.8.7}/.github/workflows/publish.yml +0 -0
- {letify-2.8.6 → letify-2.8.7}/.researchtree +0 -0
- {letify-2.8.6 → letify-2.8.7}/LICENSE +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/INTENT.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/01-getting-started.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/02-providers.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/05-concurrency.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/README.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-2.8.6 → letify-2.8.7}/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.8.6 → letify-2.8.7}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/README.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/docs/mkdocs.yml +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/README.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/config/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/config/inventory.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/config/login.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/config/schema.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/config/secrets.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/config/writer.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/declare/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/declare/cache.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/declare/env.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/declare/function.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/declare/instance.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/declare/reduce.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/declare/replicas.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/errors.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/install.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/launcher.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/paths.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/protocol/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/protocol/codec.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/protocol/driver.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/protocol/handle.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/protocol/wire.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/protocol/worker.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/base.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/colab.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/colab_files.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/elice.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/kaggle.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/local.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/modal.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/modal_adapter.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/naming.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/shell.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/tunnel.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/providers/usage.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/client.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/cuda.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/executor.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/frames.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/guard.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/tensor.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/trace.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/value.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/render.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/bootstrap.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/broker.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/channel.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/lease.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/pool.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/session.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/runtime/telemetry.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/backends/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/backends/drive.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/backends/filesystem.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/backends/google_auth.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/backends/layout.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/backends/objects.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/cas.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/handoff.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/pathdata.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/sendorder.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/store/volume.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/stubs.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/tools.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/__init__.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/agent.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/announce.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/link.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/nat.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/pipeline.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/portmap.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/probe.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/quic.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/rendezvous.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/setup.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/sshopts.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify/transport/strategies.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-core/.gitignore +0 -0
- {letify-2.8.6/letify-ext → letify-2.8.7/letify-core/letify}/LICENSE +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/.vscodeignore +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/README.md +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/media/icon.svg +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/src/cli.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/src/extension.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/src/history.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/src/model.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/src/poll.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/src/render.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/test/model.test.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify-ext/tsconfig.json +0 -0
- {letify-2.8.6 → letify-2.8.7}/letify_providers.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/scripts/sync-main.sh +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/conftest.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/fake_modal_adapter.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_colab_keepalive.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_config.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_core.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_data.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_data_streaming.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_declare.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_device.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_distributed.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_drive_handoff.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_drive_store.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_editor_sessions.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_elice.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_frames.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_install.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_kaggle.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_kaggle_link.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_link_log.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_notebook.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_parallel_placement.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_paths.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_portmap.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_protocol.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_quic.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_render.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_runtime.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_ship.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_store.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_strategies.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_stubs.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_transport.py +0 -0
- {letify-2.8.6 → letify-2.8.7}/tests/test_tunnel_setup.py +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Publish the crate letify to crates.io when a version tag is pushed.
|
|
2
|
+
#
|
|
3
|
+
# This runs beside publish.yml on the same tag v<version>. Order inside this file:
|
|
4
|
+
# 1. verify: the tag agrees with every version file, the tagged commit is on develop, the
|
|
5
|
+
# workspace builds and its tests pass, and `cargo publish --dry-run` packages and builds
|
|
6
|
+
# the crate alone.
|
|
7
|
+
# 2. publish-crate: the upload, only after verify succeeds.
|
|
8
|
+
# The upload is last because crates.io never replaces a version. A published number can be
|
|
9
|
+
# yanked but is taken forever, so every check that can fail runs before it.
|
|
10
|
+
#
|
|
11
|
+
# GitHub Release creation stays in publish.yml. Other crates in letify-core set publish = false.
|
|
12
|
+
#
|
|
13
|
+
# One-time setup: add the crates.io API token as the secret CARGO_REGISTRY_TOKEN in the GitHub
|
|
14
|
+
# environment crates-io. See docs/guide/08-releasing.md.
|
|
15
|
+
name: publish-crates
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
tags: ["v*"]
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
concurrency:
|
|
25
|
+
group: publish-crates-${{ github.ref }}
|
|
26
|
+
cancel-in-progress: false
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
verify:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
with:
|
|
34
|
+
fetch-depth: 0
|
|
35
|
+
- uses: astral-sh/setup-uv@v6
|
|
36
|
+
- name: Install Rust
|
|
37
|
+
# rustup reads letify-core/rust-toolchain.toml when cargo runs there.
|
|
38
|
+
run: |
|
|
39
|
+
rustup toolchain install stable --profile minimal
|
|
40
|
+
rustup default stable
|
|
41
|
+
- name: Check the tag and every file carry one version
|
|
42
|
+
run: uv run --no-project python scripts/version.py check --tag "$GITHUB_REF_NAME"
|
|
43
|
+
- name: Check the tagged commit is on develop
|
|
44
|
+
# letify tags develop, not main, so develop is the branch a release must come from.
|
|
45
|
+
run: |
|
|
46
|
+
git fetch origin develop
|
|
47
|
+
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/develop; then
|
|
48
|
+
echo "Tagged commit $GITHUB_SHA is not an ancestor of origin/develop."
|
|
49
|
+
echo "Merge the release commit into develop before tagging."
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
- name: Build the workspace
|
|
53
|
+
# All five crates are pure Rust with no system packages needed on ubuntu-latest.
|
|
54
|
+
working-directory: letify-core
|
|
55
|
+
run: cargo build --workspace --locked
|
|
56
|
+
- name: Test the workspace
|
|
57
|
+
working-directory: letify-core
|
|
58
|
+
run: cargo test --workspace --locked
|
|
59
|
+
- name: Package and build the crate without uploading
|
|
60
|
+
working-directory: letify-core
|
|
61
|
+
run: cargo publish -p letify --dry-run --locked
|
|
62
|
+
|
|
63
|
+
publish-crate:
|
|
64
|
+
needs: verify
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
environment: crates-io
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
- name: Install Rust
|
|
70
|
+
run: |
|
|
71
|
+
rustup toolchain install stable --profile minimal
|
|
72
|
+
rustup default stable
|
|
73
|
+
- name: Publish letify to crates.io
|
|
74
|
+
working-directory: letify-core
|
|
75
|
+
env:
|
|
76
|
+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
77
|
+
run: |
|
|
78
|
+
version="${GITHUB_REF_NAME#v}"
|
|
79
|
+
status=$(curl -s -o /dev/null -w '%{http_code}' -A "letify-release (https://github.com/DarkPyonix/letify)" "https://crates.io/api/v1/crates/letify/${version}")
|
|
80
|
+
if [ "$status" = "200" ]; then
|
|
81
|
+
echo "letify ${version} is already on crates.io, nothing to publish."
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
|
|
85
|
+
echo "CARGO_REGISTRY_TOKEN is empty."
|
|
86
|
+
echo "Add a crates.io API token as the secret CARGO_REGISTRY_TOKEN in the crates-io environment."
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
cargo publish -p letify --locked
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Reserve a letify-* crate name on crates.io before its first real release.
|
|
2
|
+
#
|
|
3
|
+
# Pushing the tag reserve/<crate>, for example reserve/letify-link, publishes version 0.0.0 of
|
|
4
|
+
# that crate: a library with no code whose README says which letify release ships it. The real
|
|
5
|
+
# crate later publishes at letify's own version, as every crate in letify-core does, so 0.0.0
|
|
6
|
+
# stays below every real release.
|
|
7
|
+
#
|
|
8
|
+
# Only names starting with letify- are accepted, and a name crates.io already has is skipped,
|
|
9
|
+
# so pushing the tag again changes nothing. The crate letify itself is not reserved here: it
|
|
10
|
+
# publishes at the release version from publish-crates.yml.
|
|
11
|
+
#
|
|
12
|
+
# The upload runs in the GitHub environment crates-io with the secret CARGO_REGISTRY_TOKEN, as
|
|
13
|
+
# publish-crates.yml does.
|
|
14
|
+
name: reserve-crates
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
push:
|
|
18
|
+
tags: ["reserve/*"]
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: reserve-crates-${{ github.ref }}
|
|
25
|
+
cancel-in-progress: false
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
reserve:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment: crates-io
|
|
31
|
+
steps:
|
|
32
|
+
- name: Install Rust
|
|
33
|
+
run: |
|
|
34
|
+
rustup toolchain install stable --profile minimal
|
|
35
|
+
rustup default stable
|
|
36
|
+
- name: Publish 0.0.0 unless the name is taken
|
|
37
|
+
env:
|
|
38
|
+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
39
|
+
CRATE: ${{ github.ref_name }}
|
|
40
|
+
run: |
|
|
41
|
+
set -euo pipefail
|
|
42
|
+
name="${CRATE#reserve/}"
|
|
43
|
+
if [[ ! "$name" =~ ^letify-[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
|
44
|
+
echo "reserve/$name: only names of the form letify-<word> are reserved here" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' -A 'letify-reserve (github.com/DarkPyonix/letify)' "https://crates.io/api/v1/crates/$name")"
|
|
48
|
+
if [[ "$code" == "200" ]]; then
|
|
49
|
+
echo "$name is already on crates.io; nothing to reserve"
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
if [[ -z "$CARGO_REGISTRY_TOKEN" ]]; then
|
|
53
|
+
echo "CARGO_REGISTRY_TOKEN is empty. Add it to the crates-io environment." >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
dir="$RUNNER_TEMP/$name"
|
|
57
|
+
mkdir -p "$dir/src"
|
|
58
|
+
cat > "$dir/Cargo.toml" <<TOML
|
|
59
|
+
[package]
|
|
60
|
+
name = "$name"
|
|
61
|
+
version = "0.0.0"
|
|
62
|
+
edition = "2024"
|
|
63
|
+
license = "MIT"
|
|
64
|
+
description = "Reserved for the letify project. The first release ships with letify 3.0."
|
|
65
|
+
repository = "https://github.com/DarkPyonix/letify"
|
|
66
|
+
readme = "README.md"
|
|
67
|
+
TOML
|
|
68
|
+
cat > "$dir/README.md" <<MD
|
|
69
|
+
# $name
|
|
70
|
+
|
|
71
|
+
This name is reserved for the letify project, https://github.com/DarkPyonix/letify.
|
|
72
|
+
Version 0.0.0 holds no code. The first release ships with letify 3.0 and carries
|
|
73
|
+
letify's version, as every letify crate does.
|
|
74
|
+
MD
|
|
75
|
+
echo '#![doc = include_str!("../README.md")]' > "$dir/src/lib.rs"
|
|
76
|
+
cd "$dir"
|
|
77
|
+
cargo publish --allow-dirty
|
|
@@ -253,10 +253,11 @@ __marimo__/
|
|
|
253
253
|
.claude
|
|
254
254
|
|
|
255
255
|
# letify
|
|
256
|
+
.letify-home/
|
|
256
257
|
.letify/
|
|
257
258
|
typings/
|
|
258
259
|
|
|
259
|
-
# letify-
|
|
260
|
+
# The letify-quic binary, copied here by letify-core/build.py
|
|
260
261
|
letify/remoting/lib/
|
|
261
262
|
|
|
262
263
|
# Artifacts the examples write where they run
|
|
@@ -114,7 +114,7 @@ This repository uses [ResearchTree](https://darkpyonix.github.io/researchtree/)
|
|
|
114
114
|
|
|
115
115
|
## Code layout
|
|
116
116
|
|
|
117
|
-
The Python package is pure Python and links no Python extension. The package lives in `letify/` at the repository root. letify-core, the Rust workspace in `letify-core/`, is built in CI and
|
|
117
|
+
The Python package is pure Python and links no Python extension. The package lives in `letify/` at the repository root. letify-core, the Rust workspace in `letify-core/`, is built in CI, and its `letify-quic` binary is shipped in `letify/remoting/lib/` inside platform wheels.
|
|
118
118
|
|
|
119
119
|
```
|
|
120
120
|
letify/
|
|
@@ -129,7 +129,7 @@ letify/
|
|
|
129
129
|
runtime/ Channel, session, pool, lease, bootstrap and telemetry
|
|
130
130
|
store/ Content addressed blob store, volumes and backends
|
|
131
131
|
providers/ Provider base class and one module per provider
|
|
132
|
-
remoting/
|
|
132
|
+
remoting/ PyTorch operator forwarding for host="local" (remoting/device/) and the efficiency arithmetic; also holds the letify-quic binary in lib/
|
|
133
133
|
_vendor/ Vendored third party code
|
|
134
134
|
letify-ext/ VS Code extension (TypeScript) showing quota and GPU activity from the CLI's --json output
|
|
135
135
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: letify
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.7
|
|
4
4
|
Summary: Declarations that become infrastructure.
|
|
5
5
|
Project-URL: Homepage, https://github.com/DarkPyonix/letify
|
|
6
6
|
Project-URL: Documentation, https://github.com/DarkPyonix/letify/tree/main/docs
|
|
@@ -171,7 +171,7 @@ uv add letify
|
|
|
171
171
|
|
|
172
172
|
That is the whole install, for every provider. letify installs only cloudpickle and blake3. It needs [uv](https://docs.astral.sh/uv/) on the machine, because provider tools such as the Colab CLI and the Modal client run through uv in their own environments, not in your `.venv`.
|
|
173
173
|
|
|
174
|
-
The wheel carries [letify-core](letify-core/), the Rust
|
|
174
|
+
The wheel carries the `letify-quic` binary built from [letify-core](letify-core/), the Rust workspace. Wheels are built for Linux x86_64 and aarch64, Windows x86_64 and arm64, and macOS arm64 and x86_64. On another platform, pip falls back to the source distribution, which has no `letify-quic`, so the QUIC connection strategy is skipped.
|
|
175
175
|
|
|
176
176
|
---
|
|
177
177
|
|
|
@@ -259,9 +259,9 @@ Two words, and neither of them is a mechanism.
|
|
|
259
259
|
|
|
260
260
|
**`device`** carries the provider, the account and the accelerator in one value, because those are one decision. Core count and memory come with the shape the provider registered, so there is nothing to ask for.
|
|
261
261
|
|
|
262
|
-
**`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only
|
|
262
|
+
**`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only PyTorch operators. `letify.remote` ships the function to the machine that holds the device. The strings `"local"` and `"remote"` are the same values.
|
|
263
263
|
|
|
264
|
-
Leave `host` out and you get `letify.local`. When the device is far away, forwarding
|
|
264
|
+
Leave `host` out and you get `letify.local`. When the device is far away, forwarding operators is slow, so the call warns with the expected efficiency and then runs. For a remote GPU, `host=letify.remote` is usually what you want.
|
|
265
265
|
|
|
266
266
|
How long a session lives is not a declaration argument. A call ends its session. `with let.keep_alive():` keeps sessions for the length of a block, so a run of separate calls does not pay session start each time.
|
|
267
267
|
|
|
@@ -283,7 +283,7 @@ Counter-intuitive consequence: **a faster GPU makes forwarding worse**, because
|
|
|
283
283
|
|
|
284
284
|
Decoding is the case that stays bad. Throughput is bounded near `1000 / (k × RTT)` tokens per second, so the card stops mattering. Ship the whole `generate` call instead.
|
|
285
285
|
|
|
286
|
-
Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `
|
|
286
|
+
Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `ping`. See [docs/NETWORK.md](docs/NETWORK.md).
|
|
287
287
|
|
|
288
288
|
</details>
|
|
289
289
|
|
|
@@ -495,7 +495,6 @@ letify status # what is running right now
|
|
|
495
495
|
letify usage # what is left on each account
|
|
496
496
|
letify utilization # how busy each instance's GPU is
|
|
497
497
|
letify check lab # does this machine answer?
|
|
498
|
-
letify probe lab # is host=letify.local worth using here?
|
|
499
498
|
letify efficiency 0.5 3 150 # the formula, from measured terms
|
|
500
499
|
```
|
|
501
500
|
|
|
@@ -513,7 +512,7 @@ Add `--json` to `usage`, `utilization` or `status` for output a program can read
|
|
|
513
512
|
| 📐 [docs/SPEC.md](docs/SPEC.md) | The design as it stands, decision by decision |
|
|
514
513
|
| 🧩 [docs/COMPONENT.md](docs/COMPONENT.md) | Every class, and the vocabulary |
|
|
515
514
|
| 🌐 [docs/NETWORK.md](docs/NETWORK.md) | Transports, latency measurements, tunnel choices |
|
|
516
|
-
| 🦀 [letify-core/](letify-core/) | The Rust workspace
|
|
515
|
+
| 🦀 [letify-core/](letify-core/) | The Rust workspace: the `letify` crate and the `letify-quic` binary |
|
|
517
516
|
| 🧭 [docs/guide/](docs/guide/) | Task-oriented guides |
|
|
518
517
|
| 🇰🇷 [docs/locales/README_ko.md](docs/locales/README_ko.md) | 한국어 |
|
|
519
518
|
|
|
@@ -527,11 +526,9 @@ Alpha, and honest about it. What works today:
|
|
|
527
526
|
✅ Persistent sessions: a session cache keeps values between calls, large arguments travel once
|
|
528
527
|
✅ Content addressed storage, configuration and secrets
|
|
529
528
|
✅ The `Local` and `Colab` providers
|
|
530
|
-
✅ `letify-core`, verified on a real GPU: the agent opens the driver, the local driver forwards an allocation and a copy in both directions, and the bytes match
|
|
531
529
|
|
|
532
530
|
Not finished yet:
|
|
533
531
|
|
|
534
|
-
🚧 `letify-driver` covers the entry points a PyTorch process needs to start up and run one kernel. Anything else names itself and returns `CUDA_ERROR_NOT_SUPPORTED`, so a real run prints the list of what to build next
|
|
535
532
|
🚧 `Modal` and `Elice` follow each published interface but have not been run against the live services
|
|
536
533
|
🚧 Unified memory cannot be forwarded at all, so a paged optimizer needs `host=letify.remote`
|
|
537
534
|
|
|
@@ -238,7 +238,6 @@ letify status # live sessions and what they are costing
|
|
|
238
238
|
letify usage [alias] # what is left on each account
|
|
239
239
|
letify utilization [alias] # how busy each instance's accelerator is
|
|
240
240
|
letify check <alias> # confirm a machine answers
|
|
241
|
-
letify probe <host> # whether host="local" is worth using
|
|
242
241
|
letify efficiency 0.5 3 150 # expected share of a direct run
|
|
243
242
|
letify stubs # write typings/letify_providers.pyi for editor completion
|
|
244
243
|
```
|
|
@@ -298,6 +297,10 @@ That is the only install. `letify` installs cloudpickle and blake3 and nothing e
|
|
|
298
297
|
|
|
299
298
|
`host="local"` additionally needs PyTorch 2.1 or newer in the project, locally and in the runtime's environment, with the same major.minor version on both sides. letify never installs it: torch is the project's own dependency.
|
|
300
299
|
|
|
300
|
+
## Rust crate
|
|
301
|
+
|
|
302
|
+
The Rust crate `letify` is published on crates.io at the same version as the Python package. It will re-export the components meant for use outside letify, first the connection library `letify-link`.
|
|
303
|
+
|
|
301
304
|
## How this project is built
|
|
302
305
|
|
|
303
306
|
Spec driven and test driven. [docs/SPEC.md](docs/SPEC.md) is the source of truth, a change edits it before the code, and every test traces to a section of it. The rules are in [CLAUDE.md](CLAUDE.md).
|
|
@@ -146,7 +146,7 @@ uv add letify
|
|
|
146
146
|
|
|
147
147
|
That is the whole install, for every provider. letify installs only cloudpickle and blake3. It needs [uv](https://docs.astral.sh/uv/) on the machine, because provider tools such as the Colab CLI and the Modal client run through uv in their own environments, not in your `.venv`.
|
|
148
148
|
|
|
149
|
-
The wheel carries [letify-core](letify-core/), the Rust
|
|
149
|
+
The wheel carries the `letify-quic` binary built from [letify-core](letify-core/), the Rust workspace. Wheels are built for Linux x86_64 and aarch64, Windows x86_64 and arm64, and macOS arm64 and x86_64. On another platform, pip falls back to the source distribution, which has no `letify-quic`, so the QUIC connection strategy is skipped.
|
|
150
150
|
|
|
151
151
|
---
|
|
152
152
|
|
|
@@ -234,9 +234,9 @@ Two words, and neither of them is a mechanism.
|
|
|
234
234
|
|
|
235
235
|
**`device`** carries the provider, the account and the accelerator in one value, because those are one decision. Core count and memory come with the shape the provider registered, so there is nothing to ask for.
|
|
236
236
|
|
|
237
|
-
**`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only
|
|
237
|
+
**`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only PyTorch operators. `letify.remote` ships the function to the machine that holds the device. The strings `"local"` and `"remote"` are the same values.
|
|
238
238
|
|
|
239
|
-
Leave `host` out and you get `letify.local`. When the device is far away, forwarding
|
|
239
|
+
Leave `host` out and you get `letify.local`. When the device is far away, forwarding operators is slow, so the call warns with the expected efficiency and then runs. For a remote GPU, `host=letify.remote` is usually what you want.
|
|
240
240
|
|
|
241
241
|
How long a session lives is not a declaration argument. A call ends its session. `with let.keep_alive():` keeps sessions for the length of a block, so a run of separate calls does not pay session start each time.
|
|
242
242
|
|
|
@@ -258,7 +258,7 @@ Counter-intuitive consequence: **a faster GPU makes forwarding worse**, because
|
|
|
258
258
|
|
|
259
259
|
Decoding is the case that stays bad. Throughput is bounded near `1000 / (k × RTT)` tokens per second, so the card stops mattering. Ship the whole `generate` call instead.
|
|
260
260
|
|
|
261
|
-
Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `
|
|
261
|
+
Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `ping`. See [docs/NETWORK.md](docs/NETWORK.md).
|
|
262
262
|
|
|
263
263
|
</details>
|
|
264
264
|
|
|
@@ -470,7 +470,6 @@ letify status # what is running right now
|
|
|
470
470
|
letify usage # what is left on each account
|
|
471
471
|
letify utilization # how busy each instance's GPU is
|
|
472
472
|
letify check lab # does this machine answer?
|
|
473
|
-
letify probe lab # is host=letify.local worth using here?
|
|
474
473
|
letify efficiency 0.5 3 150 # the formula, from measured terms
|
|
475
474
|
```
|
|
476
475
|
|
|
@@ -488,7 +487,7 @@ Add `--json` to `usage`, `utilization` or `status` for output a program can read
|
|
|
488
487
|
| 📐 [docs/SPEC.md](docs/SPEC.md) | The design as it stands, decision by decision |
|
|
489
488
|
| 🧩 [docs/COMPONENT.md](docs/COMPONENT.md) | Every class, and the vocabulary |
|
|
490
489
|
| 🌐 [docs/NETWORK.md](docs/NETWORK.md) | Transports, latency measurements, tunnel choices |
|
|
491
|
-
| 🦀 [letify-core/](letify-core/) | The Rust workspace
|
|
490
|
+
| 🦀 [letify-core/](letify-core/) | The Rust workspace: the `letify` crate and the `letify-quic` binary |
|
|
492
491
|
| 🧭 [docs/guide/](docs/guide/) | Task-oriented guides |
|
|
493
492
|
| 🇰🇷 [docs/locales/README_ko.md](docs/locales/README_ko.md) | 한국어 |
|
|
494
493
|
|
|
@@ -502,11 +501,9 @@ Alpha, and honest about it. What works today:
|
|
|
502
501
|
✅ Persistent sessions: a session cache keeps values between calls, large arguments travel once
|
|
503
502
|
✅ Content addressed storage, configuration and secrets
|
|
504
503
|
✅ The `Local` and `Colab` providers
|
|
505
|
-
✅ `letify-core`, verified on a real GPU: the agent opens the driver, the local driver forwards an allocation and a copy in both directions, and the bytes match
|
|
506
504
|
|
|
507
505
|
Not finished yet:
|
|
508
506
|
|
|
509
|
-
🚧 `letify-driver` covers the entry points a PyTorch process needs to start up and run one kernel. Anything else names itself and returns `CUDA_ERROR_NOT_SUPPORTED`, so a real run prints the list of what to build next
|
|
510
507
|
🚧 `Modal` and `Elice` follow each published interface but have not been run against the live services
|
|
511
508
|
🚧 Unified memory cannot be forwarded at all, so a paged optimizer needs `host=letify.remote`
|
|
512
509
|
|
|
@@ -24,7 +24,7 @@ Read this table first. Most confusion about letify is one of these words meaning
|
|
|
24
24
|
| **persistence** | Whether a provider's storage outlives a session: `persistent` or `ephemeral`. |
|
|
25
25
|
| **RemoteTensor** | Under `host="local"`, the local stand-in for a tensor on the runtime's GPU: a meta tensor for shape and dtype plus a handle. Reports its device as `cuda:0`. |
|
|
26
26
|
| **device worker** | The process on the runtime that runs PyTorch operators on real tensors keyed by handle, one per session. |
|
|
27
|
-
| **letify-core** |
|
|
27
|
+
| **letify-core** | The Rust workspace in `letify-core/`: the `letify` crate and the `letify-quic` binary. The CUDA driver stand-in it once held is removed. |
|
|
28
28
|
|
|
29
29
|
## The shape of the thing
|
|
30
30
|
|
|
@@ -58,7 +58,7 @@ One directory per concern, so the file you need is the one named after the thing
|
|
|
58
58
|
| `runtime/` | The channel, the session, the pool, the lease, bootstrap source |
|
|
59
59
|
| `providers/` | The base class, name normalization, and one module per provider |
|
|
60
60
|
| `store/` | The content addressed store, volumes, and `backends/` |
|
|
61
|
-
| `remoting/` | PyTorch forwarding in `remoting/device/`, and the
|
|
61
|
+
| `remoting/` | PyTorch forwarding in `remoting/device/`, and the efficiency arithmetic |
|
|
62
62
|
| `launcher.py` | `Launcher` itself, which ties the rest together |
|
|
63
63
|
| `errors.py` | The exception hierarchy, which everything imports |
|
|
64
64
|
| `cli.py` | The command line |
|
|
@@ -180,7 +180,7 @@ Three protocol rules exist for performance rather than tidiness. Large arguments
|
|
|
180
180
|
|
|
181
181
|
## remoting
|
|
182
182
|
|
|
183
|
-
PyTorch forwarding, in `remoting/device/`, and the
|
|
183
|
+
PyTorch forwarding, in `remoting/device/`, and the efficiency arithmetic beside it.
|
|
184
184
|
|
|
185
185
|
| Module | Owns |
|
|
186
186
|
|---|---|
|
|
@@ -197,4 +197,4 @@ It refuses rather than degrading when PyTorch is absent or too old. It does not
|
|
|
197
197
|
|
|
198
198
|
## letify-core
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
The Rust workspace in `letify-core/`. It holds the crate `letify`, published to crates.io, and the binary `letify-quic`, which the wheels carry in `letify/remoting/lib/` and `letify/transport/quic.py` runs. The driver stand-in it once held is removed; the spec's "The driver stand-in is removed" gives the measurement and where the code is.
|
|
@@ -217,26 +217,6 @@ Both sides bound one TCP port, learned the mapping from `stun.nextcloud.com:443`
|
|
|
217
217
|
|
|
218
218
|
`colab console` is not a usable channel: it is a terminal inside tmux, and a single input line of 4000 characters or more arrives truncated or mixed with terminal control sequences. The Colab runtime proxy reaches only port 8080 on the VM; other port prefixes return 404.
|
|
219
219
|
|
|
220
|
-
### letify-core copy throughput
|
|
221
|
-
|
|
222
|
-
A copy to the device through letify-core, with the payload streamed from the caller's buffer into the agent's staging buffer, moves about 2000 MiB/s over loopback TCP. The same payload through an encoded frame, the path before streaming, moves about 520 MiB/s. Both carry one 256 MiB `CopyToDevice` request between a `BufWriter` and a `BufReader` with `TCP_NODELAY`, on an AMD EPYC 7763, and report the median of five runs.
|
|
223
|
-
|
|
224
|
-
| Path | Three invocations |
|
|
225
|
-
|---|---|
|
|
226
|
-
| Encoded frame, before the change | 545, 544, 507 MiB/s |
|
|
227
|
-
| Encoded frame, after the change | 515, 514, 519 MiB/s |
|
|
228
|
-
| Streamed into staging | 2043, 1961, 1873 MiB/s |
|
|
229
|
-
|
|
230
|
-
A copy to the host, with the payload streamed from the agent's staging buffer into the caller's buffer, moves about 3250 MiB/s over loopback TCP. The same payload through an encoded frame and a copy into the caller's buffer, the path before streaming, moves about 520 MiB/s. Both carry one 256 MiB `Payload` reply over the same socket pair and machine and report the median of five runs. The copy on the device is not included.
|
|
231
|
-
|
|
232
|
-
| Path | Three invocations |
|
|
233
|
-
|---|---|
|
|
234
|
-
| Encoded frame, before the change | 522, 514, 519 MiB/s |
|
|
235
|
-
| Encoded frame, after the change | 528, 527, 540 MiB/s |
|
|
236
|
-
| Streamed into the destination | 3234, 3261, 3244 MiB/s |
|
|
237
|
-
|
|
238
|
-
Loopback removes the network, so these numbers bound what the copy path itself costs. On a real link the link rate decides throughput whenever it is below them. To reproduce, run `cargo test --release -p letify-wire --test throughput -- --ignored --nocapture --test-threads=1` in `letify-core/`.
|
|
239
|
-
|
|
240
220
|
## PyTorch forwarding on dept_gpu
|
|
241
221
|
|
|
242
222
|
> Reading the loss once every 50 steps, `host="local"` takes a median 3.45 ms per step against 1.70 ms directly on the server: about twice the direct time, and one round trip per 45 steps. Measured on 2026-09-14.
|
|
@@ -315,7 +295,7 @@ Three checks settle most of what is provider specific.
|
|
|
315
295
|
|
|
316
296
|
```bash
|
|
317
297
|
# Round trip to the machine, which sets the efficiency formula.
|
|
318
|
-
|
|
298
|
+
ping -c 5 gpu.lab.example.edu
|
|
319
299
|
|
|
320
300
|
# Whether a layer 3 tunnel is possible at all.
|
|
321
301
|
ls -l /dev/net/tun
|
|
@@ -278,7 +278,7 @@ Not opening a link is a decision of this project, not a rule of Kaggle's. Two so
|
|
|
278
278
|
- `TPU_V3_8` is a TPU instance, carried by `Instance.tpu` rather than `Instance.gpu`, and requests `TPU_V3_8` as the accelerator in `CommitAndRun`. With `host="remote"`, letify ships the whole function to the runtime. Using the TPU is the user's code's responsibility, with `torch-xla` or JAX in the user's own lock file; letify neither installs nor checks those TPU libraries. letify requests and reports the TPU, without making the user's code TPU-capable. Kaggle refuses `host="local"` for TPU instances as it does for every other Kaggle instance.
|
|
279
279
|
- When no account inventory is configured, the default inventory takes each discovered instance's device count, so the two-card `T4` shape can be reserved. An explicit account inventory keeps its configured counts and indices.
|
|
280
280
|
- **Two credentials, both required.** The cookie starts and ends the interactive session and mints its Jupyter proxy URL. The API token authenticates every official Kaggle CLI invocation through `KAGGLE_API_TOKEN` in the child environment, never process arguments, for notebook deletion and weekly quota. There is no cookie fallback. Login stores the cookie in `~/.letify/accounts/<alias>/cookie` and the verbatim token, including `KGAT_`, in `access_token` beside it, using owner-only files (0600) and an owner-only directory (0700). The CLI accepts no working file-based token form, so keeping the token out of the environment cannot be honoured. Its child process environment is the narrowest available scope and exposes less than the argument list. An account without `access_token` is refused with `ConfigError` naming the login command when it reaches a CLI-only path.
|
|
281
|
-
- **Token input and notebook owner.** `letify login kaggle <alias>` accepts `--key` or hidden input asking for the API token exactly as kaggle.com shows it, including `KGAT_`. It requires that prefix and a nonempty body with no whitespace; it does not strip or reformat the token. Length is not fixed because the measured 32-character body establishes a working token, not a universal length contract. A malformed token is refused with a message naming what to paste, before any cookie verification or file write. The token is not verified online at login. A
|
|
281
|
+
- **Token input and notebook owner.** `letify login kaggle <alias>` accepts `--key` or hidden input asking for the API token exactly as kaggle.com shows it, including `KGAT_`. It requires that prefix and a nonempty body with no whitespace; it does not strip or reformat the token. Length is not fixed because the measured 32-character body establishes a working token, not a universal length contract. A malformed token is refused with a message naming what to paste, before any cookie verification or file write. The token is not verified online at login. A renewal asks for each credential rather than reusing what is on disk, and a blank answer keeps the stored one. The two credentials expire on different clocks: the cookie is a thirty day web session and the token lasts until it is rotated, so renewing one must not demand the other. `letify login kaggle <alias> --replace` therefore prompts for the token and then the cookie, each saying that blank keeps what is stored, and writes only what was answered. A blank answer with nothing stored is refused, naming what to paste. Silently reusing a stored credential would make a renewal a no-op with no way to replace a credential that stopped working. Login retains `--username` or a username prompt and stores it in `username` beside the credentials with mode 0600. This is only the owner for `kernels delete -y <owner>/<slug>`, not an authentication credential. Explicit input avoids another authenticated lookup and does not mistake a cookie display name for an owner. The owner is not read from an authenticated call. Reading an existing `access_token` file strips leading and trailing whitespace, including a trailing newline, before CLI calls or reuse by login when `--key` is omitted. Explicit `--key` and prompted token input still reject whitespace rather than reformatting it, so adding an owner with `--username` does not require pasting the token again. The owner is a public Kaggle username, not a secret, and its file uses the same owner-only permissions as the account directory. Login refuses a missing or blank owner before cookie verification or any file write. Nothing is written until both credentials, the owner and cookie verification pass. Before any session starts or notebook is created, `open_channel` requires a readable, nonempty `username` file. An incomplete account is refused with `ConfigError` naming the login command, so a run cannot reach teardown without a notebook owner.
|
|
282
282
|
- **The cookie carries an ``exp`` claim, but that claim is not proof the cookie still works.** The `CLIENT-TOKEN` cookie is an `alg:none` JWT whose `exp` claim was thirty days after the login that minted it, in every case observed. But a live account has been seen with over nine days left on that claim while every internal call, including the plain `users.UsersService/GetCurrentUser` read, answered as a refused, anonymous session: Kaggle invalidates a cookie server side, for reasons this project has not pinned down, without the claim changing to say so. So `exp` is useful for exactly one direction: a cookie past its `exp` is certainly dead. It says nothing about a cookie before its `exp`, which may be live or may already be refused.
|
|
283
283
|
- **Two checks, for two costs.** An offline check reads only the claim, costs no network call, and is what `letify providers` shows as the account's `exp claim says N days left (not checked live; see letify usage)`, or `EXPIRED` once `exp` has passed. An online check additionally calls `GetCurrentUser`, the same call the login proved the cookie with, and is made once per session start (`open_channel`) and once per `letify usage` read, each result trusted for sixty seconds so starting several runtimes together costs one round trip rather than one per runtime. A run whose cookie fails either check is refused with `ConfigError`: missing or past `exp` says exactly that; refused online says "the Kaggle cookie has not expired, but Kaggle no longer accepts it", which is the one fact that check establishes, never a guess at why. A run is also refused when fewer than one hour is left on the `exp` claim, since starting a session the claim says the cookie cannot outlive risks it dying mid run with no way to tell that apart from any other lost runtime; this offline guard and the online check both have to pass.
|
|
284
284
|
- The online check is not made on every call into a running session, only at the two points above, so it is not the cost of every request a declared function makes.
|
|
@@ -1895,7 +1895,7 @@ A UDP pinhole opens where a TCP simultaneous open is refused, which is why Tailc
|
|
|
1895
1895
|
|
|
1896
1896
|
QUIC is the one UDP payload such a device is unlikely to shape, because HTTP/3 is QUIC and shaping it breaks the web. So letify can carry the session over a real QUIC v1 connection: version 1 on the wire, a TLS 1.3 handshake, a certificate, and the ALPN `letify/1`.
|
|
1897
1897
|
|
|
1898
|
-
`letify-quic` is a binary in letify-core, built for each wheel and installed
|
|
1898
|
+
`letify-quic` is a binary in letify-core, built for each wheel and installed in `letify/remoting/lib/`. It takes the punched endpoints rather than discovering anything, because the rendezvous already exchanges them:
|
|
1899
1899
|
|
|
1900
1900
|
```
|
|
1901
1901
|
letify-quic serve --bind <port> --peer <ip:port> --token <hex> --forward <ssh port>
|
|
@@ -2585,11 +2585,13 @@ A lost worker process or link raises `RuntimeLost`, and the session is discarded
|
|
|
2585
2585
|
|
|
2586
2586
|
`_make_wrapper_subclass`, `TorchFunctionMode` and `torch.utils._pytree` are each present in 2.1. An older PyTorch raises `UnsupportedMode` naming the version found and the version needed.
|
|
2587
2587
|
|
|
2588
|
-
### The driver stand-in is
|
|
2588
|
+
### The driver stand-in is removed <!-- id: letify-core -->
|
|
2589
2589
|
|
|
2590
|
-
> `host="local"` does not load letify-core. Standing in for the CUDA driver was replaced by operator forwarding.
|
|
2590
|
+
> `host="local"` does not load letify-core. Standing in for the CUDA driver was replaced by operator forwarding, and the driver stand-in was removed from the tree.
|
|
2591
2591
|
|
|
2592
|
-
Measured on 2026-09-14 against a Tesla P100 server with torch 2.5.1 cu121: libcudart 12.1 resolves 425 driver symbols by name and the stand-in `libcuda.so.1` exports 20, so CUDA initialization fails with `cudaErrorInsufficientDriver`. The private `cuGetExportTable` blocks adding symbols one by one, and PyTorch kernels arrive through fatbinary registration that `cuModuleLoadData` does not see. Operator forwarding depends on PyTorch's public extension points instead of the driver's private ones.
|
|
2592
|
+
Measured on 2026-09-14 against a Tesla P100 server with torch 2.5.1 cu121: libcudart 12.1 resolves 425 driver symbols by name and the stand-in `libcuda.so.1` exports 20, so CUDA initialization fails with `cudaErrorInsufficientDriver`. The private `cuGetExportTable` blocks adding symbols one by one, and PyTorch kernels arrive through fatbinary registration that `cuModuleLoadData` does not see. Operator forwarding depends on PyTorch's public extension points instead of the driver's private ones.
|
|
2593
|
+
|
|
2594
|
+
The crates `letify-wire`, `letify-driver` and `letify-agent` and the module `letify.remoting.probe` are not in the tree. The code is at the git tag `archive/cuda-driver-forwarding`. A stand-in at the CUDA runtime API layer, which libcudart does not need driver symbols for, is proposed in [pull request 74](https://github.com/DarkPyonix/letify/pull/74). `letify-core/` holds the crates `letify` and `letify-quic`.
|
|
2593
2595
|
|
|
2594
2596
|
## Command line
|
|
2595
2597
|
|
|
@@ -2615,7 +2617,7 @@ A heading, an alias at the top of a block and a table's column names are bold. S
|
|
|
2615
2617
|
|
|
2616
2618
|
A `LetifyError` that reaches the command line prints `✗ <message>` on standard error and exits with 1. An argument error is argparse's own and exits with 2.
|
|
2617
2619
|
|
|
2618
|
-
`--json` prints the records with no styling on `providers`, `devices`, `status`, `usage`, `utilization
|
|
2620
|
+
`--json` prints the records with no styling on `providers`, `devices`, `status`, `usage`, `utilization` and `efficiency`.
|
|
2619
2621
|
|
|
2620
2622
|
Connection decision lines stay on standard error as `letify: <message>`, with the content set by Transport. When standard error is a terminal with colour, the `letify:` prefix is dim and nothing else changes.
|
|
2621
2623
|
|
|
@@ -2627,7 +2629,6 @@ Connection decision lines stay on standard error as `letify: <message>`, with th
|
|
|
2627
2629
|
| `devices` | A table `PROVIDER ACCELERATORS`, the accelerators joined by `, `. `--json` is the mapping from alias to accelerator names |
|
|
2628
2630
|
| `status` | The header `<name> <live> live, <busy> busy`, one block per live runtime, then a table `PROVIDER ACCELERATOR RESERVED INDICES` with reserved as `<reserved>/<count>`. With no runtime the blocks are replaced by `no live session in this process`. `--json` is `Launcher.status()` |
|
|
2629
2631
|
| `usage`, `utilization` | As Remaining usage and GPU utilization describe |
|
|
2630
|
-
| `probe` | A mark and `forwarding usable`, `forwarding usable but costly` or `forwarding not usable`, then aligned `platform`, `core`, `agent` and `round trip` fields and the reason, dim. `--json` is the capability record |
|
|
2631
2632
|
| `efficiency` | `<p>% of a direct run`. `--json` is `{"efficiency": <fraction>}` |
|
|
2632
2633
|
| `check` | `✓ <alias> answers`, then the machine's output indented by 2 spaces |
|
|
2633
2634
|
| `login` | `✓ <alias> declared in <home>`, or `! <alias> was already declared in <home>, so nothing was asked for`, then `✓ <alias> referenced in <project>, which is safe to commit` |
|
|
@@ -2754,7 +2755,7 @@ Provider tools run out of process and never in the user's `.venv`. Colab runs th
|
|
|
2754
2755
|
|
|
2755
2756
|
uv must be installed. letify finds it from the `UV` environment variable, then from `PATH`. If neither has it, letify raises an error that says uv is required.
|
|
2756
2757
|
|
|
2757
|
-
The Python code is pure and links no Python extension. letify-core
|
|
2758
|
+
The Python code is pure and links no Python extension. The `letify-quic` binary of letify-core is built in CI and placed in `letify/remoting/lib/`, so each release has one wheel per desktop platform, tagged `py3-none-<platform>`:
|
|
2758
2759
|
|
|
2759
2760
|
| Platform | Wheel platform tag |
|
|
2760
2761
|
|---|---|
|
|
@@ -2765,7 +2766,7 @@ The Python code is pure and links no Python extension. letify-core binaries are
|
|
|
2765
2766
|
| macOS arm64 | `macosx_11_0_arm64` |
|
|
2766
2767
|
| macOS x86_64 | `macosx_10_12_x86_64` |
|
|
2767
2768
|
|
|
2768
|
-
Linux wheels are built inside the `manylinux_2_28` containers, so the binaries need glibc 2.28 or newer. That covers RHEL 8, Debian 10 and Ubuntu 18.10 onward. The sdist carries no binaries, and an install from it has no letify-
|
|
2769
|
+
Linux wheels are built inside the `manylinux_2_28` containers, so the binaries need glibc 2.28 or newer. That covers RHEL 8, Debian 10 and Ubuntu 18.10 onward. The sdist carries no binaries, and an install from it has no `letify-quic`, so the QUIC strategy is skipped.
|
|
2769
2770
|
|
|
2770
2771
|
### Versioning and releases <!-- id: versioning -->
|
|
2771
2772
|
|
|
@@ -2776,7 +2777,7 @@ Linux wheels are built inside the `manylinux_2_28` containers, so the binaries n
|
|
|
2776
2777
|
| File | Field |
|
|
2777
2778
|
|---|---|
|
|
2778
2779
|
| `letify/__init__.py` | `__version__` |
|
|
2779
|
-
| `letify-core/Cargo.toml` | `[workspace.package].version`, and the `letify
|
|
2780
|
+
| `letify-core/Cargo.toml` | `[workspace.package].version`, and the `letify` and `letify-quic` entries in `Cargo.lock` |
|
|
2780
2781
|
| `letify-ext/package.json` | `version`, and `version` and `packages[""].version` in `package-lock.json`, when `letify-ext/` exists |
|
|
2781
2782
|
| Git tag | `v<version>` |
|
|
2782
2783
|
|
|
@@ -2784,6 +2785,10 @@ Linux wheels are built inside the `manylinux_2_28` containers, so the binaries n
|
|
|
2784
2785
|
|
|
2785
2786
|
Pushing a tag `v*` runs `.github/workflows/publish.yml`. It builds the sdist, the six platform wheels above and, when `letify-ext/` exists, the extension's `letify-ext-<version>.vsix` after its unit tests pass. It then creates the GitHub Release for the tag with every one of those files attached, and uploads the sdist and wheels to PyPI. The extension is not published to the VS Code Marketplace.
|
|
2786
2787
|
|
|
2788
|
+
The same tag also runs `.github/workflows/publish-crates.yml`, which publishes the crate `letify` to crates.io at the same version. `letify` is the Rust face of the project: a library crate in `letify-core/letify` that re-exports the letify components meant for use outside letify, the first being the connection library `letify-link` once it exists. Every other crate in `letify-core/` sets `publish = false`, because each is a binary shipped inside the wheels and tied to the Python package of the same version. The workflow checks before it uploads anything: the tag agrees with every version file, the tagged commit is on `develop`, the workspace builds and its tests pass, and `cargo publish -p letify --dry-run` packages and builds the crate on its own. The upload comes last, because crates.io never replaces a version: a published number can be yanked but is taken forever. It runs in the GitHub environment `crates-io` with the repository secret `CARGO_REGISTRY_TOKEN`, a crates.io API token scoped to publishing `letify` and the future `letify-*` crates. A tag pushed before that secret exists fails at the upload and publishes nothing.
|
|
2789
|
+
|
|
2790
|
+
A `letify-*` crate name is reserved before its first release by pushing the tag `reserve/<crate>`, which runs `.github/workflows/reserve-crates.yml` and publishes version 0.0.0 of that name with no code and a README naming the letify release that ships it. A name crates.io already has is skipped, and a name not of the form `letify-<word>` is refused. The real crate then publishes at letify's version like every other, so 0.0.0 stays below every real release. `letify-link` and `letify-proto` are reserved this way, for letify 3.0.
|
|
2791
|
+
|
|
2787
2792
|
## Known gaps
|
|
2788
2793
|
|
|
2789
2794
|
> Implemented and unimplemented, stated plainly so nobody builds on a promise.
|
|
@@ -86,7 +86,7 @@ torch.cuda.set_sync_debug_mode("warn")
|
|
|
86
86
|
This does not depend on where the GPU is, so it can be measured on any CUDA device, including a Colab runtime. Under `host="local"`, `letify.remoting.device.current_client().stats` counts it for you: `round_trips` is `k` and `ops` is `n`. With `k` measured and the round trip measured, the formula gives you a real answer instead of an estimate.
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
|
|
89
|
+
ping -c 5 gpu.lab.example.edu # round trip
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
## Tuning `k` down
|
|
@@ -148,7 +148,7 @@ Colab figures assume the 600 credit pack at 49.99 USD, which is about 0.083 USD
|
|
|
148
148
|
## Checking before you spend
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
|
|
151
|
+
ping -c 5 gpu.lab.example.edu # round trip, which sets what forwarding costs
|
|
152
152
|
letify check lab_a100 # does the machine answer at all?
|
|
153
153
|
letify login shell lab_a100 # declare the machine before spending on it
|
|
154
154
|
letify usage # what is left on each account
|
|
@@ -242,7 +242,7 @@ Pulling 20 GB from a bucket next to the runtime is 40 to 60 seconds against 27 m
|
|
|
242
242
|
If `host="local"`, you are forwarding PyTorch operators and paying a round trip per host synchronization. Measure both terms:
|
|
243
243
|
|
|
244
244
|
```bash
|
|
245
|
-
|
|
245
|
+
ping -c 5 gpu.lab.example.edu
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
```python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Releasing
|
|
2
2
|
|
|
3
|
-
A release is one version number pushed as the tag `v<version>`. GitHub Actions builds everything
|
|
3
|
+
A release is one version number pushed as the tag `v<version>`. GitHub Actions builds everything, attaches it to a GitHub Release and publishes the crate `letify` to crates.io.
|
|
4
4
|
|
|
5
5
|
## The version rule
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ The `ci` workflow runs `check` on every push and pull request.
|
|
|
24
24
|
git commit -am "Chore: Set the version to 1.2.0"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
2.
|
|
27
|
+
2. Tag that commit on `develop` and push the tag. The crates.io workflow refuses a tag whose commit is not in `develop`'s history:
|
|
28
28
|
|
|
29
29
|
```
|
|
30
30
|
git tag v1.2.0
|
|
@@ -38,6 +38,17 @@ The `ci` workflow runs `check` on every push and pull request.
|
|
|
38
38
|
|
|
39
39
|
4. It creates the GitHub Release `v1.2.0` with all of those files attached, and uploads the sdist and wheels to PyPI through trusted publishing.
|
|
40
40
|
|
|
41
|
+
5. The `publish-crates` workflow runs on the same tag. Its `verify` job checks the version files, that the tagged commit is on `develop`, the workspace build and tests, and `cargo publish -p letify --dry-run`. Its `publish-crate` job then uploads the crate `letify` 1.2.0 to crates.io. If that version is already there, the job says so and succeeds without uploading. crates.io never replaces a version, so a wrong upload can only be yanked.
|
|
42
|
+
|
|
43
|
+
## One-time setup for crates.io
|
|
44
|
+
|
|
45
|
+
1. On crates.io, create an API token with the scopes `publish-new` and `publish-update`, limited to the crate patterns `letify` and `letify-*`.
|
|
46
|
+
2. In the GitHub repository settings, create an environment named `crates-io`.
|
|
47
|
+
3. Add the token to that environment as the secret `CARGO_REGISTRY_TOKEN`.
|
|
48
|
+
4. Optionally add a required reviewer to the environment, so each crates.io upload waits for an approval.
|
|
49
|
+
|
|
50
|
+
A tag pushed before step 3 passes `verify` and fails at the upload with a message naming the missing secret. Nothing is published.
|
|
51
|
+
|
|
41
52
|
A failed run publishes nothing to the Release. Fix the cause, delete the tag with `git push origin :v1.2.0`, and tag again.
|
|
42
53
|
|
|
43
54
|
## Installing the extension from a release
|
{letify-2.8.6 → letify-2.8.7}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md
RENAMED
|
@@ -86,7 +86,7 @@ torch.cuda.set_sync_debug_mode("warn")
|
|
|
86
86
|
GPU가 어디 있는지와 무관하므로 Colab 런타임을 포함해 어떤 CUDA 장치에서도 잴 수 있습니다. `host="local"`에서는 `letify.remoting.device.current_client().stats`가 대신 셉니다. `round_trips`가 `k`이고 `ops`가 `n`입니다. `k`와 왕복을 둘 다 재면 계산식이 추정이 아니라 실제 답을 줍니다.
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
|
|
89
|
+
ping -c 5 gpu.lab.example.edu # 왕복
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
## `k`를 줄이기
|
|
@@ -142,7 +142,7 @@ Colab 수치는 49.99달러 600 크레딧 팩 기준으로 크레딧당 약 0.08
|
|
|
142
142
|
## 쓰기 전에 확인하기
|
|
143
143
|
|
|
144
144
|
```bash
|
|
145
|
-
|
|
145
|
+
ping -c 5 gpu.lab.example.edu # 왕복, 중계 비용을 정합니다
|
|
146
146
|
letify login shell lab_a100 # 돈을 쓰기 전에 머신을 먼저 등록
|
|
147
147
|
letify usage # 계정마다 남은 사용량
|
|
148
148
|
letify utilization # 선언된 GPU가 지금 얼마나 바쁜가
|