letify 2.7.0__tar.gz → 2.7.1__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.7.0 → letify-2.7.1}/PKG-INFO +1 -1
- {letify-2.7.0 → letify-2.7.1}/docs/SPEC.md +5 -1
- {letify-2.7.0 → letify-2.7.1}/letify/__init__.py +1 -1
- {letify-2.7.0 → letify-2.7.1}/letify/store/pathdata.py +36 -12
- {letify-2.7.0 → letify-2.7.1}/letify-core/Cargo.lock +3 -3
- {letify-2.7.0 → letify-2.7.1}/letify-core/Cargo.toml +1 -1
- {letify-2.7.0 → letify-2.7.1}/letify-ext/package-lock.json +2 -2
- {letify-2.7.0 → letify-2.7.1}/letify-ext/package.json +1 -1
- {letify-2.7.0 → letify-2.7.1}/pyproject.toml +1 -1
- {letify-2.7.0 → letify-2.7.1}/tests/test_data_streaming.py +114 -0
- {letify-2.7.0 → letify-2.7.1}/.github/workflows/ci.yml +0 -0
- {letify-2.7.0 → letify-2.7.1}/.github/workflows/docs.yml +0 -0
- {letify-2.7.0 → letify-2.7.1}/.github/workflows/publish.yml +0 -0
- {letify-2.7.0 → letify-2.7.1}/.gitignore +0 -0
- {letify-2.7.0 → letify-2.7.1}/.researchtree +0 -0
- {letify-2.7.0 → letify-2.7.1}/CLAUDE.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/LICENSE +0 -0
- {letify-2.7.0 → letify-2.7.1}/PROJECT.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/README.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/COMPONENT.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/INTENT.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/NETWORK.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/01-getting-started.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/02-providers.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/05-concurrency.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/06-cost.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/08-releasing.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/README.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.7.0 → letify-2.7.1}/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.7.0 → letify-2.7.1}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/guide/ko/README.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/locales/README_ko.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/docs/mkdocs.yml +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/README.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/cli.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/config/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/config/inventory.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/config/login.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/config/schema.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/config/secrets.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/config/writer.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/declare/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/declare/cache.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/declare/env.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/declare/function.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/declare/instance.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/declare/reduce.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/declare/replicas.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/errors.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/install.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/launcher.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/paths.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/protocol/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/protocol/codec.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/protocol/driver.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/protocol/handle.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/protocol/wire.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/protocol/worker.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/base.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/colab.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/colab_files.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/elice.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/kaggle.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/local.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/modal.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/modal_adapter.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/naming.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/shell.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/tunnel.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/providers/usage.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/capability.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/client.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/cuda.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/executor.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/frames.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/guard.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/tensor.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/trace.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/device/value.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/loader.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/remoting/probe.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/render.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/bootstrap.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/broker.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/channel.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/lease.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/pool.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/session.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/runtime/telemetry.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/backends/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/backends/filesystem.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/backends/google_auth.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/backends/layout.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/backends/objects.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/cas.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/sendorder.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/store/volume.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/stubs.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/tools.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/__init__.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/agent.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/announce.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/link.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/nat.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/pipeline.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/portmap.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/probe.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/quic.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/rendezvous.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/setup.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/sshopts.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify/transport/strategies.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/.gitignore +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/build.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-agent/Cargo.toml +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-agent/src/driver.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-agent/src/main.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-driver/Cargo.toml +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-driver/src/client.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-driver/src/exports.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-driver/src/image.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-driver/src/lib.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-driver/src/status.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-driver/src/table.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-wire/Cargo.toml +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-wire/src/lib.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/letify-wire/tests/throughput.rs +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/.vscodeignore +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/LICENSE +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/README.md +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/media/icon.svg +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/src/cli.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/src/extension.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/src/history.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/src/model.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/src/poll.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/src/render.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/test/model.test.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify-ext/tsconfig.json +0 -0
- {letify-2.7.0 → letify-2.7.1}/letify_providers.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/scripts/sync-main.sh +0 -0
- {letify-2.7.0 → letify-2.7.1}/scripts/version.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/conftest.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/fake_modal_adapter.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_cli.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_cli_output.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_colab_keepalive.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_config.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_core.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_data.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_declare.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_device.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_distributed.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_editor_sessions.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_elice.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_frames.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_install.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_kaggle.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_kaggle_link.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_link_log.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_notebook.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_parallel_placement.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_paths.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_portmap.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_protocol.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_providers.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_quic.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_remoting.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_render.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_runtime.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_ship.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_store.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_strategies.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_stubs.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_transport.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_tunnel_setup.py +0 -0
- {letify-2.7.0 → letify-2.7.1}/tests/test_version.py +0 -0
|
@@ -1428,7 +1428,7 @@ A call with an output location that returned prints a second line after the writ
|
|
|
1428
1428
|
|
|
1429
1429
|
`letify: data wrote back <files> files <size> in <seconds> s (<rate> MiB/s), <files> files <size> during the call, <files> files <size> already on the client`
|
|
1430
1430
|
|
|
1431
|
-
The first count and size are the total written back, the background write-back of Writing back and the final write-back combined, and the rate covers both. `during the call` is the part of that total the background write-back already delivered while the body was still running, counted as the call ran rather than only once it returned, so a long call's log line still shows separately how much of the output had already made it back before the call ended. `already on the client` is unchanged: files the local digest cache already held, fetched by neither path.
|
|
1431
|
+
The first count and size are the total written back, the background write-back of Writing back and the final write-back combined, and the rate covers both. The rate divides those bytes by the seconds spent carrying them, the background write-back's time inside its transfers plus the final write-back's own, and not by how long the background write-back was alive: it polls for the whole call, so dividing by its lifetime turned 1212.4 MiB carried in 19.5 s, about 62 MiB/s, into a reported 2.4 MiB/s. `<seconds>` is the final write-back's own time, as before. `during the call` is the part of that total the background write-back already delivered while the body was still running, counted as the call ran rather than only once it returned, so a long call's log line still shows separately how much of the output had already made it back before the call ended. `already on the client` is unchanged: files the local digest cache already held, fetched by neither path.
|
|
1432
1432
|
|
|
1433
1433
|
#### Writing back <!-- id: project-data-write-back -->
|
|
1434
1434
|
|
|
@@ -1449,6 +1449,10 @@ A path written more than once during a call, such as a checkpoint overwritten on
|
|
|
1449
1449
|
|
|
1450
1450
|
The final write-back, after the body returns, is unchanged in what it detects: the worker walks each output's runtime path, as it always has, and renames every changed file into the file blob cache as before. What changed is that it is now incremental. Before fetching a listed file, the local process checks it against what the background write-back already sent for that relative path during the call: a file whose listed digest equals the digest the background path already delivered is skipped outright, counted as already sent rather than fetched again, because fetching it a second time would be a duplicate transfer of bytes the local disk already has. Only a file the background path never sent, or sent an older digest for, travels on the final write-back's own `data_stream` requests.
|
|
1451
1451
|
|
|
1452
|
+
The final write-back starts only once the background write-back has stopped, including a file it was in the middle of carrying when the call returned, because the two decide from the same record of what was sent. The background write-back checks for the stop between files, so waiting for it costs at most the rest of one file. Starting early let the final write-back find a 402.2 MiB file already complete on the local disk while the background path had not yet recorded sending it, so the line counted it both as delivered during the call and as already on the client, and the final write-back then waited on the same output lock for the background transfer to finish, which is why that call reported 209.1 s.
|
|
1453
|
+
|
|
1454
|
+
The progress line counts only the bytes this write-back fetches. A file skipped because the background path or the local digest cache already has it is left out of the line's total rather than added to its progress in an instant, which drew rates such as 31830.2 MiB/s.
|
|
1455
|
+
|
|
1452
1456
|
On the runtime, after the body returned and before the call directory is removed, the worker walks each output's runtime path as Which paths are data walks a directory, without following symbolic links. A file is unchanged when it is the hard link or copy the worker placed and its inode, size and modification time in nanoseconds equal those recorded when it was placed; it is not read. Any other regular file, a file Copy on write converted included, is hashed with the file digest, and a file whose digest equals the digest placed at that relative path is unchanged too. Each changed file is renamed into the file blob cache and made read-only, or dropped when the cache already holds its digest with its size. The worker keeps the list of relative path, digest and size per output under the call id until the local process asks for it with one `data_written` request, which removes it. The background poll above and the final `data_written` request keep separate bookkeeping on the worker, so one does not consume what the other needs.
|
|
1453
1457
|
|
|
1454
1458
|
When the runtime path of an output that did not exist is a regular file, the local path receives that file. When it is a directory, the local path becomes a directory holding the changed files. When nothing was created there, nothing is written.
|
|
@@ -1435,6 +1435,9 @@ class WriteBack:
|
|
|
1435
1435
|
self.blobs = blobs
|
|
1436
1436
|
self.files = self.bytes = 0
|
|
1437
1437
|
self.seconds = 0.0
|
|
1438
|
+
#: Seconds spent inside transfers, which the wrote back rate divides by. Spec
|
|
1439
|
+
#: "Data log line": ``seconds`` is how long this ran, which is the whole call.
|
|
1440
|
+
self.carrying_seconds = 0.0
|
|
1438
1441
|
#: The digest this path was last sent with, which keeps the final write-back from
|
|
1439
1442
|
#: sending it again and a repeated poll from sending the same version twice.
|
|
1440
1443
|
self.sent: dict[Path, str] = {}
|
|
@@ -1488,11 +1491,16 @@ class WriteBack:
|
|
|
1488
1491
|
if self.sent.get(target) == digest:
|
|
1489
1492
|
continue
|
|
1490
1493
|
with _output_lock(placed.local):
|
|
1494
|
+
began = time.monotonic()
|
|
1491
1495
|
_get(self.runtime, self.blobs, digest, target, int(size), _Uploaded(1, 0))
|
|
1492
1496
|
self.collector.cache.record(target, digest)
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1497
|
+
# Recorded before the lock is released, so the final write-back, which
|
|
1498
|
+
# decides under the same lock, never sees the file on disk unrecorded.
|
|
1499
|
+
# Spec "Writing back".
|
|
1500
|
+
self.carrying_seconds += time.monotonic() - began
|
|
1501
|
+
self.sent[target] = digest
|
|
1502
|
+
self.files += 1
|
|
1503
|
+
self.bytes += int(size)
|
|
1496
1504
|
|
|
1497
1505
|
def cancel(self) -> None:
|
|
1498
1506
|
"""Stop polling, which the call's end does before the final write-back runs."""
|
|
@@ -1526,22 +1534,38 @@ def write_back(
|
|
|
1526
1534
|
target = placed.local / rel if rel else placed.local
|
|
1527
1535
|
files.append((target, str(digest), int(size)))
|
|
1528
1536
|
plan.append((placed, files))
|
|
1529
|
-
total = sum(size for _placed, files in plan for _t, _d, size in files)
|
|
1530
|
-
meter = _Downloaded(sum(len(files) for _p, files in plan), total)
|
|
1531
1537
|
sent_files = sent_bytes = kept_files = kept_bytes = 0
|
|
1532
|
-
|
|
1538
|
+
|
|
1539
|
+
def delivered(target: Path, digest: str) -> bool:
|
|
1540
|
+
"""Whether the background write-back already brought this very version.
|
|
1541
|
+
|
|
1542
|
+
Read under the output lock, never from a copy taken earlier: the background path
|
|
1543
|
+
may still be finishing a file when the call returns, and it records the file
|
|
1544
|
+
under the same lock. Spec "Writing back".
|
|
1545
|
+
"""
|
|
1546
|
+
return background is not None and background.sent.get(target) == digest
|
|
1547
|
+
|
|
1548
|
+
# Decided before anything is fetched, so the progress line covers only the bytes this
|
|
1549
|
+
# write-back carries. Spec "Writing back": a skip added its size at once.
|
|
1550
|
+
fetch: list[tuple[Placed, list[tuple[Path, str, int]]]] = []
|
|
1533
1551
|
for placed, files in plan:
|
|
1552
|
+
wanted = []
|
|
1534
1553
|
with _output_lock(placed.local):
|
|
1535
1554
|
for target, digest, size in files:
|
|
1536
|
-
if
|
|
1537
|
-
# The background write-back delivered this very version during the
|
|
1538
|
-
# call, so fetching it now would be a duplicate transfer.
|
|
1539
|
-
meter.advance(size)
|
|
1555
|
+
if delivered(target, digest):
|
|
1540
1556
|
continue
|
|
1541
1557
|
if _same(cache, target, digest):
|
|
1542
1558
|
kept_files += 1
|
|
1543
1559
|
kept_bytes += size
|
|
1544
|
-
|
|
1560
|
+
continue
|
|
1561
|
+
wanted.append((target, digest, size))
|
|
1562
|
+
fetch.append((placed, wanted))
|
|
1563
|
+
total = sum(size for _placed, files in fetch for _t, _d, size in files)
|
|
1564
|
+
meter = _Downloaded(sum(len(files) for _p, files in fetch), total)
|
|
1565
|
+
for placed, files in fetch:
|
|
1566
|
+
with _output_lock(placed.local):
|
|
1567
|
+
for target, digest, size in files:
|
|
1568
|
+
if delivered(target, digest):
|
|
1545
1569
|
continue
|
|
1546
1570
|
_get(runtime, blobs, digest, target, size, meter)
|
|
1547
1571
|
cache.record(target, digest)
|
|
@@ -1552,7 +1576,7 @@ def write_back(
|
|
|
1552
1576
|
cache.save()
|
|
1553
1577
|
during_files = background.files if background is not None else 0
|
|
1554
1578
|
during_bytes = background.bytes if background is not None else 0
|
|
1555
|
-
during_seconds = background.
|
|
1579
|
+
during_seconds = background.carrying_seconds if background is not None else 0.0
|
|
1556
1580
|
elapsed = time.monotonic() - started
|
|
1557
1581
|
total_files = sent_files + during_files
|
|
1558
1582
|
total_bytes = sent_bytes + during_bytes
|
|
@@ -138,7 +138,7 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify-agent"
|
|
141
|
-
version = "2.7.
|
|
141
|
+
version = "2.7.1"
|
|
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.7.
|
|
149
|
+
version = "2.7.1"
|
|
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.7.
|
|
167
|
+
version = "2.7.1"
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "libc"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.7.
|
|
9
|
+
"version": "2.7.1",
|
|
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.7.
|
|
5
|
+
"version": "2.7.1",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -21,6 +21,7 @@ import tempfile
|
|
|
21
21
|
import threading
|
|
22
22
|
import time
|
|
23
23
|
from pathlib import Path
|
|
24
|
+
from typing import ClassVar
|
|
24
25
|
|
|
25
26
|
import pytest
|
|
26
27
|
|
|
@@ -1820,3 +1821,116 @@ def test_a_large_blob_beside_the_call_draws_the_progress_line(
|
|
|
1820
1821
|
|
|
1821
1822
|
assert read_it(path=big) == 70 << 20
|
|
1822
1823
|
assert "uploading" in capsys.readouterr().err
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def test_the_wrote_back_rate_counts_the_seconds_spent_carrying(
|
|
1827
|
+
launcher_from, project, capsys
|
|
1828
|
+
) -> None:
|
|
1829
|
+
"""Spec "Data log line": not how long the background write-back was alive.
|
|
1830
|
+
|
|
1831
|
+
It polls for the whole call, so dividing by its lifetime turned 1212.4 MiB carried in
|
|
1832
|
+
19.5 s, about 62 MiB/s, into a reported 2.4 MiB/s on KICT's Modal run.
|
|
1833
|
+
"""
|
|
1834
|
+
let = streaming(launcher_from)
|
|
1835
|
+
root = project / "rate"
|
|
1836
|
+
root.mkdir()
|
|
1837
|
+
|
|
1838
|
+
@let.function(device=let.providers.lab.CPU, host=letify.remote)
|
|
1839
|
+
def train(directory: Path, held: float) -> str:
|
|
1840
|
+
import time as clock
|
|
1841
|
+
|
|
1842
|
+
(directory / "checkpoint.bin").write_bytes(b"\x05" * (8 << 20))
|
|
1843
|
+
clock.sleep(held)
|
|
1844
|
+
return "done"
|
|
1845
|
+
|
|
1846
|
+
assert train(root, HELD_S) == "done"
|
|
1847
|
+
line = wrote_back_line(capsys.readouterr().err)
|
|
1848
|
+
size = re.search(r"wrote back \d+ files ([\d.]+) MiB", line)
|
|
1849
|
+
rate = re.search(r"\(([\d.]+) MiB/s\)", line)
|
|
1850
|
+
assert size and rate, line
|
|
1851
|
+
# 8 MiB over a local channel takes a fraction of a second; the call held for HELD_S.
|
|
1852
|
+
implied = float(size.group(1)) / max(float(rate.group(1)), 1e-9)
|
|
1853
|
+
assert implied < HELD_S / 2, f"the rate implies {implied:.1f} s of carrying: {line}"
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
def test_a_file_the_background_is_still_carrying_is_not_counted_twice(
|
|
1857
|
+
launcher_from, project, capsys, monkeypatch
|
|
1858
|
+
) -> None:
|
|
1859
|
+
"""Spec "Writing back": the final write-back starts only once the background stopped.
|
|
1860
|
+
|
|
1861
|
+
KICT's line read "1 files 402.2 MiB during the call, 1 files 402.2 MiB already on the
|
|
1862
|
+
client": the final write-back began while the background path was still carrying that
|
|
1863
|
+
file, found it complete on the local disk, and counted it a second time. The wait
|
|
1864
|
+
for the background used to give up after 60 s, so the test takes the wait away.
|
|
1865
|
+
"""
|
|
1866
|
+
from letify.store import pathdata
|
|
1867
|
+
|
|
1868
|
+
# What a wait that gave up looks like: the stop is asked for and nothing waits for it.
|
|
1869
|
+
monkeypatch.setattr(pathdata.WriteBack, "cancel", lambda self: self._cancelled.set())
|
|
1870
|
+
real_get = pathdata._get
|
|
1871
|
+
|
|
1872
|
+
def slow_in_the_background(runtime, blobs, digest, target, size, meter):
|
|
1873
|
+
if threading.current_thread().name == "letify-write-back":
|
|
1874
|
+
time.sleep(3.0)
|
|
1875
|
+
return real_get(runtime, blobs, digest, target, size, meter)
|
|
1876
|
+
|
|
1877
|
+
monkeypatch.setattr(pathdata, "_get", slow_in_the_background)
|
|
1878
|
+
let = streaming(launcher_from)
|
|
1879
|
+
root = project / "twice"
|
|
1880
|
+
root.mkdir()
|
|
1881
|
+
|
|
1882
|
+
@let.function(device=let.providers.lab.CPU, host=letify.remote)
|
|
1883
|
+
def train(directory: Path) -> str:
|
|
1884
|
+
import time as clock
|
|
1885
|
+
|
|
1886
|
+
(directory / "checkpoint.bin").write_bytes(b"\x06" * (1 << 20))
|
|
1887
|
+
# Long enough for a poll to find the file complete, short enough that the call
|
|
1888
|
+
# returns while the background is still carrying it.
|
|
1889
|
+
clock.sleep(1.5)
|
|
1890
|
+
return "done"
|
|
1891
|
+
|
|
1892
|
+
assert train(root) == "done"
|
|
1893
|
+
line = wrote_back_line(capsys.readouterr().err)
|
|
1894
|
+
during = re.search(r"(\d+) files [\d.]+ MiB during the call", line)
|
|
1895
|
+
kept = re.search(r"(\d+) files [\d.]+ MiB already on the client", line)
|
|
1896
|
+
assert during and kept, line
|
|
1897
|
+
assert not (int(during.group(1)) == 1 and int(kept.group(1)) == 1), line
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def test_skipped_files_do_not_enter_the_progress_line(project, monkeypatch) -> None:
|
|
1901
|
+
"""Spec "Writing back": a skip added its size at once and drew 31830.2 MiB/s."""
|
|
1902
|
+
from letify.store import pathdata
|
|
1903
|
+
|
|
1904
|
+
totals: list[int] = []
|
|
1905
|
+
real = pathdata._Downloaded
|
|
1906
|
+
|
|
1907
|
+
class Watched(real): # type: ignore[misc, valid-type]
|
|
1908
|
+
def __init__(self, files: int, total: int) -> None:
|
|
1909
|
+
totals.append(total)
|
|
1910
|
+
super().__init__(files, total)
|
|
1911
|
+
|
|
1912
|
+
monkeypatch.setattr(pathdata, "_Downloaded", Watched)
|
|
1913
|
+
kept = project / "kept.bin"
|
|
1914
|
+
kept.write_bytes(b"k" * 1000)
|
|
1915
|
+
|
|
1916
|
+
class Runtime:
|
|
1917
|
+
name = "rt"
|
|
1918
|
+
|
|
1919
|
+
def request(self, payload, timeout=None):
|
|
1920
|
+
raise AssertionError(f"nothing should be fetched: {payload.get('op')}")
|
|
1921
|
+
|
|
1922
|
+
class Placed:
|
|
1923
|
+
local = project
|
|
1924
|
+
runtime = "/rt/out"
|
|
1925
|
+
|
|
1926
|
+
class Collector:
|
|
1927
|
+
call_dir = "/rt/call"
|
|
1928
|
+
outputs: ClassVar[list] = [Placed]
|
|
1929
|
+
cache = pathdata.DigestCache(project / "digests.json")
|
|
1930
|
+
|
|
1931
|
+
digest = Collector.cache.digest(kept)
|
|
1932
|
+
monkeypatch.setattr(
|
|
1933
|
+
pathdata, "_worker", lambda runtime, payload: {"/rt/out": [["kept.bin", digest, 1000]]}
|
|
1934
|
+
)
|
|
1935
|
+
pathdata.write_back(Runtime(), Collector(), "/rt/blobs")
|
|
1936
|
+
assert totals == [0], totals
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.7.0 → letify-2.7.1}/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.7.0 → letify-2.7.1}/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.7.0 → letify-2.7.1}/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.7.0 → letify-2.7.1}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md"
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|