goaly-looper 0.1.0b1__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.
- goaly_looper-0.1.0b1/.gitignore +16 -0
- goaly_looper-0.1.0b1/PKG-INFO +197 -0
- goaly_looper-0.1.0b1/README.md +188 -0
- goaly_looper-0.1.0b1/looper_sdk/__init__.py +132 -0
- goaly_looper-0.1.0b1/looper_sdk/_version.py +3 -0
- goaly_looper-0.1.0b1/looper_sdk/checkpoints.py +64 -0
- goaly_looper-0.1.0b1/looper_sdk/errors.py +380 -0
- goaly_looper-0.1.0b1/looper_sdk/losses.py +159 -0
- goaly_looper-0.1.0b1/looper_sdk/operations.py +109 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/README.md +20 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/__init__.py +2 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/__init__.py +0 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/checkpoints.proto +45 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/checkpoints_pb2.py +56 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/checkpoints_pb2.pyi +108 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/errors.proto +70 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/errors_pb2.py +42 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/errors_pb2.pyi +101 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/losses.proto +38 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/losses_pb2.py +40 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/losses_pb2.pyi +30 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/operations.proto +160 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/operations_pb2.py +70 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/operations_pb2.pyi +195 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/runs.proto +112 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/runs_pb2.py +66 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/runs_pb2.pyi +146 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/sampling.proto +111 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/sampling_pb2.py +77 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/sampling_pb2.pyi +209 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/sessions.proto +54 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/sessions_pb2.py +51 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/sessions_pb2.pyi +70 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/values.proto +89 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/values_pb2.py +52 -0
- goaly_looper-0.1.0b1/looper_sdk/proto/v1/values_pb2.pyi +88 -0
- goaly_looper-0.1.0b1/looper_sdk/results.py +72 -0
- goaly_looper-0.1.0b1/looper_sdk/sampling.py +247 -0
- goaly_looper-0.1.0b1/looper_sdk/sampling_client.py +196 -0
- goaly_looper-0.1.0b1/looper_sdk/service.py +316 -0
- goaly_looper-0.1.0b1/looper_sdk/training.py +270 -0
- goaly_looper-0.1.0b1/looper_sdk/transport.py +259 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/__init__.py +0 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_credentials.py +105 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_lifecycle.py +260 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_losses.py +117 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_package.py +26 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_sampling.py +154 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_sampling_client.py +280 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_shutdown.py +525 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_transport.py +234 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_values.py +186 -0
- goaly_looper-0.1.0b1/looper_sdk/unit_tests/test_wire.py +406 -0
- goaly_looper-0.1.0b1/looper_sdk/values.py +294 -0
- goaly_looper-0.1.0b1/looper_sdk/wire.py +479 -0
- goaly_looper-0.1.0b1/publish.sh +98 -0
- goaly_looper-0.1.0b1/pyproject.toml +30 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
|
|
6
|
+
# Environments
|
|
7
|
+
.venv/
|
|
8
|
+
|
|
9
|
+
# Job directories kept in the checkout (goaly_rl/run.sh RECIPE --set dump_dir=runs/...)
|
|
10
|
+
runs/
|
|
11
|
+
|
|
12
|
+
# Claude Code session state (settings, worktrees)
|
|
13
|
+
.claude/
|
|
14
|
+
|
|
15
|
+
# Prepared benchmark bundles and images manifest
|
|
16
|
+
.cache/
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: goaly-looper
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Looper's SDK: the client and the typed values of Looper's API, looper.v1.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
7
|
+
Requires-Dist: protobuf<7,>=6.33.5
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# goaly-looper
|
|
11
|
+
|
|
12
|
+
Looper's SDK: the client and the typed values of Looper's API, `looper.v1`.
|
|
13
|
+
Looper is Goaly's hosted post-training service: a user's own program holds the
|
|
14
|
+
data, the rewards and the loop, and calls a few primitives that Looper runs on
|
|
15
|
+
GPUs it manages ([the design](../docs/README.md), [the API and the
|
|
16
|
+
SDK](../docs/sdk.md)).
|
|
17
|
+
|
|
18
|
+
Its import package is `looper_sdk`. It imports nothing of ours, so it installs
|
|
19
|
+
and runs on its own. It is a project of its own, with this `pyproject.toml`,
|
|
20
|
+
to be published as `goaly-looper` and open-sourced
|
|
21
|
+
([monorepo.md](../docs/monorepo.md) §4, §9).
|
|
22
|
+
|
|
23
|
+
Status: `looper.v1`'s messages for P1's calls; the values, losses, results
|
|
24
|
+
and errors, with the module that maps them onto the wire; and the clients
|
|
25
|
+
over httpx: `ServiceClient`, `TrainingClient` and `Operation` (sdk.md §3,
|
|
26
|
+
§6). Publishing, sampling and suspension join with their verbs
|
|
27
|
+
(server.md §14).
|
|
28
|
+
|
|
29
|
+
| Path | What lives there |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `pyproject.toml` | The project: its version, read from `looper_sdk/_version.py`, and its dependency ranges |
|
|
32
|
+
| `looper_sdk/` | The import package |
|
|
33
|
+
| `looper_sdk/values.py`, `losses.py`, `results.py` | What a client sends and what an operation computed: frozen values that validate themselves |
|
|
34
|
+
| `looper_sdk/checkpoints.py` | `StateCheckpoint`: a named registry value with expiry, identity and source-version provenance, and its `CheckpointState`: `uploading`, `ready_to_use` or `failed` ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)) |
|
|
35
|
+
| `looper_sdk/errors.py` | A class for each of §4.9's codes; retry semantics come with each error, as data |
|
|
36
|
+
| `looper_sdk/wire.py` | The values, results and errors as `looper.v1`'s messages, and back |
|
|
37
|
+
| `looper_sdk/proto/` | `looper.v1`'s protos and their generated modules |
|
|
38
|
+
| `looper_sdk/service.py`, `training.py`, `operations.py` | The clients: a program's session, a run's writer, and an accepted operation |
|
|
39
|
+
| `looper_sdk/transport.py` | httpx, the retries every call takes, and the session's heartbeats and closure |
|
|
40
|
+
| `looper_sdk/unit_tests/` | The SDK's unit tests, which import only what the SDK declares |
|
|
41
|
+
|
|
42
|
+
## Client lifetime
|
|
43
|
+
|
|
44
|
+
Use an async context manager when the program's work has a bounded scope:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from looper_sdk import ServiceClient
|
|
48
|
+
|
|
49
|
+
config = ServiceClient.Config(project="research")
|
|
50
|
+
async with await config.build() as service:
|
|
51
|
+
training = await service.create_training_client(training_config)
|
|
52
|
+
status = await training.status()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The default API root is `https://api.looper.goaly.ai`, exported as
|
|
56
|
+
`looper_sdk.DEFAULT_BASE_URL`. Pass `base_url="http://127.0.0.1:8080"` for a
|
|
57
|
+
local service, or another root for an alternate deployment. Omit `/v1` from
|
|
58
|
+
the root: the SDK adds it to requests.
|
|
59
|
+
|
|
60
|
+
Exit closes the session and HTTP client and stops heartbeats, including when
|
|
61
|
+
the body raises or is cancelled. It invalidates local clients and records
|
|
62
|
+
termination of session-owned runs; the coordinator retires workers
|
|
63
|
+
asynchronously. Checkpoints and historical results retain their storage policy.
|
|
64
|
+
|
|
65
|
+
For an application-managed lifetime, keep the client and explicitly call
|
|
66
|
+
`await service.close()` in the application's shutdown path.
|
|
67
|
+
|
|
68
|
+
## API key
|
|
69
|
+
|
|
70
|
+
Every request sends `Authorization: Bearer <key>`: the config's `api_key`,
|
|
71
|
+
or `$LOOPER_API_KEY` when that is None. With neither, the client sends no
|
|
72
|
+
credential, which only local mode accepts (looper/docs/sdk.md §3). The key
|
|
73
|
+
never appears in the config's repr.
|
|
74
|
+
|
|
75
|
+
## Rules
|
|
76
|
+
|
|
77
|
+
- **Ranges, never pins.** Each dependency runs from the oldest version we
|
|
78
|
+
test to the next major. An exact pin in a published wheel would conflict
|
|
79
|
+
with the rest of a user's environment. protobuf's floor is the version of
|
|
80
|
+
the generator that wrote `looper_sdk/proto/` (monorepo.md §4).
|
|
81
|
+
- **It imports nothing of ours.** `looper_server`'s import-rules test fails
|
|
82
|
+
on any import of `goaly_rl`, `looper_server` or `looper_cookbook`, or
|
|
83
|
+
through `looper.`.
|
|
84
|
+
- **Its tests import only what it declares,** so they pass outside this
|
|
85
|
+
repository.
|
|
86
|
+
|
|
87
|
+
## Alone
|
|
88
|
+
|
|
89
|
+
CI's `looper-isolated` job builds the SDK's sdist, and a wheel from it. It
|
|
90
|
+
installs the wheel in a fresh environment, with the declared dependencies at
|
|
91
|
+
the lowest versions their ranges allow. It then runs the unit tests copied out
|
|
92
|
+
of the checkout, so `import looper_sdk` finds only the wheel (monorepo.md §7).
|
|
93
|
+
From the repository's root:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv build looper/sdk --out-dir /tmp/looper-sdk
|
|
97
|
+
uv venv /tmp/looper-sdk/env --python 3.12
|
|
98
|
+
uv pip install --python /tmp/looper-sdk/env --resolution lowest-direct -r looper/sdk/pyproject.toml /tmp/looper-sdk/goaly_looper-*.whl pytest==9.1.1
|
|
99
|
+
cp -r looper/sdk/looper_sdk/unit_tests /tmp/looper-sdk/unit_tests
|
|
100
|
+
cd /tmp/looper-sdk && env/bin/python -m pytest -q unit_tests
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Publish a beta release to PyPI
|
|
104
|
+
|
|
105
|
+
[`publish.sh`](publish.sh) builds the SDK's sdist and a wheel from it, tests the
|
|
106
|
+
installed wheel in isolation at its dependency floors, then publishes both
|
|
107
|
+
artifacts to public PyPI as `goaly-looper`. It publishes only this standalone
|
|
108
|
+
SDK, never the server or the root project. PyPI distributions are publicly
|
|
109
|
+
accessible and discoverable; hosted API access still requires a Looper API key.
|
|
110
|
+
|
|
111
|
+
The version comes from `looper_sdk/_version.py`. Use beta versions such as
|
|
112
|
+
`0.1.0b1`, then `0.1.0b2`; never replace a release with different contents.
|
|
113
|
+
|
|
114
|
+
One-time setup: create a [PyPI account](https://pypi.org/account/register/), enable
|
|
115
|
+
2FA, and create an [API token](https://pypi.org/manage/account/token/). The first
|
|
116
|
+
upload creates the project, so its token initially needs account-wide scope.
|
|
117
|
+
Once the project exists, replace it with a token scoped to `goaly-looper`.
|
|
118
|
+
The name must be available, or the account must already have publishing access.
|
|
119
|
+
|
|
120
|
+
With `uv` and Python 3 installed, run from the repository root:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Read the token without echoing it or storing its literal value in shell history.
|
|
124
|
+
read -r -s -p "PyPI API token: " UV_PUBLISH_TOKEN
|
|
125
|
+
printf '\n'
|
|
126
|
+
export UV_PUBLISH_TOKEN
|
|
127
|
+
bash looper/sdk/publish.sh
|
|
128
|
+
unset UV_PUBLISH_TOKEN
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The hidden-prompt example uses Bash; in zsh use
|
|
132
|
+
`read -r -s 'UV_PUBLISH_TOKEN?PyPI API token: '` instead of its `read` line.
|
|
133
|
+
Never put the token in a notebook or commit it. `uv` uses a temporary Python 3.12
|
|
134
|
+
virtual environment for the SDK tests. No GCS credentials or bucket settings
|
|
135
|
+
are needed. The script works from any directory when called by its path.
|
|
136
|
+
|
|
137
|
+
Uploads use PyPI's check URL so retrying an unchanged release can complete a
|
|
138
|
+
partial upload without replacing existing files. An upload failure exits
|
|
139
|
+
nonzero; completed uploads may remain on PyPI. After uploading, the script
|
|
140
|
+
checks PyPI's release metadata, downloads both public artifacts and verifies
|
|
141
|
+
their SHA-256 against the local build. Only then does it print `SUCCESS`, the
|
|
142
|
+
project URL and an exact-version Colab/Jupyter install command. Verification
|
|
143
|
+
failure exits nonzero and leaves the release in place for investigation.
|
|
144
|
+
Temporary local artifacts are removed on exit. These checks prove SDK unit
|
|
145
|
+
behavior and artifact availability, not compatibility with a deployed service.
|
|
146
|
+
|
|
147
|
+
Users install the beta in Colab or local Jupyter with:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
%pip install "goaly-looper==0.1.0b1"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
An exact prerelease pin does not require `--pre`. Future automation can use
|
|
154
|
+
PyPI Trusted Publishing; the local release script needs no CI configuration.
|
|
155
|
+
|
|
156
|
+
## Readiness and explicit closure
|
|
157
|
+
|
|
158
|
+
`create_training_client(config, key=..., timeout_s=...)` returns after the
|
|
159
|
+
training node is ready. The default wait has no overall deadline.
|
|
160
|
+
`CreationTimeout` carries the originating session ID, creation key and run ID
|
|
161
|
+
when known; accepted work remains intact. Service failures raise `CreationError`
|
|
162
|
+
with the original typed error in `.cause` and `__cause__`, preserving its
|
|
163
|
+
traceback and retry semantics.
|
|
164
|
+
An existing handle can `await training.wait_until_ready(timeout_s=...)`.
|
|
165
|
+
|
|
166
|
+
Explicit service closure, including context exit, rejects new calls with
|
|
167
|
+
`SessionClosed`. Pending calls have up to 60 seconds to finish while heartbeats
|
|
168
|
+
continue. Cleanup then stops heartbeats, cancels remaining calls and closes the
|
|
169
|
+
session and HTTP client. A call interrupted by shutdown raises `SessionClosed`;
|
|
170
|
+
caller cancellation remains `CancelledError`. The default close wait is 70
|
|
171
|
+
seconds; cancellation or a caller deadline leaves shared cleanup running while
|
|
172
|
+
the event loop remains alive. A later `close()` can wait again. Backend runs
|
|
173
|
+
end on session closure or expiry; workers retire asynchronously. Continuation
|
|
174
|
+
uses a new service and run, optionally from a saved checkpoint. Session identity
|
|
175
|
+
is immutable and expiry never renews it. Sampling clients, once added, must
|
|
176
|
+
borrow the same session lifetime.
|
|
177
|
+
|
|
178
|
+
`await training.save_state(name)` accepts a save; `await operation.result()`
|
|
179
|
+
answers once the step is on the worker group's local storage, with the
|
|
180
|
+
checkpoint `uploading`, while the run trains on. `await
|
|
181
|
+
service.wait_checkpoint(checkpoint.id)` waits until it is `ready_to_use`:
|
|
182
|
+
uploaded, named and loadable; it raises `CheckpointFailed` if the upload
|
|
183
|
+
never finished ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)).
|
|
184
|
+
Registry lookup, list and delete methods use `ServiceClient`.
|
|
185
|
+
|
|
186
|
+
## Checkpoint model metadata
|
|
187
|
+
|
|
188
|
+
Every `StateCheckpoint` includes `model_spec: ModelSpec`, exported from
|
|
189
|
+
`looper_sdk`: the source run's `model` name and exact `Full()` or `Lora(...)`
|
|
190
|
+
configuration, including rank and target switches. It excludes workload sizing
|
|
191
|
+
and does not resolve a Hugging Face revision. Save results, registry lookups and
|
|
192
|
+
lists return this metadata; historical save results are completed by the server
|
|
193
|
+
from the immutable source run.
|
|
194
|
+
|
|
195
|
+
The updated SDK requires an updated server: missing or malformed model metadata
|
|
196
|
+
raises `InvalidArgument`. Older SDKs can ignore the additive binary protobuf
|
|
197
|
+
field. Clients using strict JSON schemas must allow the new response field.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# goaly-looper
|
|
2
|
+
|
|
3
|
+
Looper's SDK: the client and the typed values of Looper's API, `looper.v1`.
|
|
4
|
+
Looper is Goaly's hosted post-training service: a user's own program holds the
|
|
5
|
+
data, the rewards and the loop, and calls a few primitives that Looper runs on
|
|
6
|
+
GPUs it manages ([the design](../docs/README.md), [the API and the
|
|
7
|
+
SDK](../docs/sdk.md)).
|
|
8
|
+
|
|
9
|
+
Its import package is `looper_sdk`. It imports nothing of ours, so it installs
|
|
10
|
+
and runs on its own. It is a project of its own, with this `pyproject.toml`,
|
|
11
|
+
to be published as `goaly-looper` and open-sourced
|
|
12
|
+
([monorepo.md](../docs/monorepo.md) §4, §9).
|
|
13
|
+
|
|
14
|
+
Status: `looper.v1`'s messages for P1's calls; the values, losses, results
|
|
15
|
+
and errors, with the module that maps them onto the wire; and the clients
|
|
16
|
+
over httpx: `ServiceClient`, `TrainingClient` and `Operation` (sdk.md §3,
|
|
17
|
+
§6). Publishing, sampling and suspension join with their verbs
|
|
18
|
+
(server.md §14).
|
|
19
|
+
|
|
20
|
+
| Path | What lives there |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `pyproject.toml` | The project: its version, read from `looper_sdk/_version.py`, and its dependency ranges |
|
|
23
|
+
| `looper_sdk/` | The import package |
|
|
24
|
+
| `looper_sdk/values.py`, `losses.py`, `results.py` | What a client sends and what an operation computed: frozen values that validate themselves |
|
|
25
|
+
| `looper_sdk/checkpoints.py` | `StateCheckpoint`: a named registry value with expiry, identity and source-version provenance, and its `CheckpointState`: `uploading`, `ready_to_use` or `failed` ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)) |
|
|
26
|
+
| `looper_sdk/errors.py` | A class for each of §4.9's codes; retry semantics come with each error, as data |
|
|
27
|
+
| `looper_sdk/wire.py` | The values, results and errors as `looper.v1`'s messages, and back |
|
|
28
|
+
| `looper_sdk/proto/` | `looper.v1`'s protos and their generated modules |
|
|
29
|
+
| `looper_sdk/service.py`, `training.py`, `operations.py` | The clients: a program's session, a run's writer, and an accepted operation |
|
|
30
|
+
| `looper_sdk/transport.py` | httpx, the retries every call takes, and the session's heartbeats and closure |
|
|
31
|
+
| `looper_sdk/unit_tests/` | The SDK's unit tests, which import only what the SDK declares |
|
|
32
|
+
|
|
33
|
+
## Client lifetime
|
|
34
|
+
|
|
35
|
+
Use an async context manager when the program's work has a bounded scope:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from looper_sdk import ServiceClient
|
|
39
|
+
|
|
40
|
+
config = ServiceClient.Config(project="research")
|
|
41
|
+
async with await config.build() as service:
|
|
42
|
+
training = await service.create_training_client(training_config)
|
|
43
|
+
status = await training.status()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The default API root is `https://api.looper.goaly.ai`, exported as
|
|
47
|
+
`looper_sdk.DEFAULT_BASE_URL`. Pass `base_url="http://127.0.0.1:8080"` for a
|
|
48
|
+
local service, or another root for an alternate deployment. Omit `/v1` from
|
|
49
|
+
the root: the SDK adds it to requests.
|
|
50
|
+
|
|
51
|
+
Exit closes the session and HTTP client and stops heartbeats, including when
|
|
52
|
+
the body raises or is cancelled. It invalidates local clients and records
|
|
53
|
+
termination of session-owned runs; the coordinator retires workers
|
|
54
|
+
asynchronously. Checkpoints and historical results retain their storage policy.
|
|
55
|
+
|
|
56
|
+
For an application-managed lifetime, keep the client and explicitly call
|
|
57
|
+
`await service.close()` in the application's shutdown path.
|
|
58
|
+
|
|
59
|
+
## API key
|
|
60
|
+
|
|
61
|
+
Every request sends `Authorization: Bearer <key>`: the config's `api_key`,
|
|
62
|
+
or `$LOOPER_API_KEY` when that is None. With neither, the client sends no
|
|
63
|
+
credential, which only local mode accepts (looper/docs/sdk.md §3). The key
|
|
64
|
+
never appears in the config's repr.
|
|
65
|
+
|
|
66
|
+
## Rules
|
|
67
|
+
|
|
68
|
+
- **Ranges, never pins.** Each dependency runs from the oldest version we
|
|
69
|
+
test to the next major. An exact pin in a published wheel would conflict
|
|
70
|
+
with the rest of a user's environment. protobuf's floor is the version of
|
|
71
|
+
the generator that wrote `looper_sdk/proto/` (monorepo.md §4).
|
|
72
|
+
- **It imports nothing of ours.** `looper_server`'s import-rules test fails
|
|
73
|
+
on any import of `goaly_rl`, `looper_server` or `looper_cookbook`, or
|
|
74
|
+
through `looper.`.
|
|
75
|
+
- **Its tests import only what it declares,** so they pass outside this
|
|
76
|
+
repository.
|
|
77
|
+
|
|
78
|
+
## Alone
|
|
79
|
+
|
|
80
|
+
CI's `looper-isolated` job builds the SDK's sdist, and a wheel from it. It
|
|
81
|
+
installs the wheel in a fresh environment, with the declared dependencies at
|
|
82
|
+
the lowest versions their ranges allow. It then runs the unit tests copied out
|
|
83
|
+
of the checkout, so `import looper_sdk` finds only the wheel (monorepo.md §7).
|
|
84
|
+
From the repository's root:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv build looper/sdk --out-dir /tmp/looper-sdk
|
|
88
|
+
uv venv /tmp/looper-sdk/env --python 3.12
|
|
89
|
+
uv pip install --python /tmp/looper-sdk/env --resolution lowest-direct -r looper/sdk/pyproject.toml /tmp/looper-sdk/goaly_looper-*.whl pytest==9.1.1
|
|
90
|
+
cp -r looper/sdk/looper_sdk/unit_tests /tmp/looper-sdk/unit_tests
|
|
91
|
+
cd /tmp/looper-sdk && env/bin/python -m pytest -q unit_tests
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Publish a beta release to PyPI
|
|
95
|
+
|
|
96
|
+
[`publish.sh`](publish.sh) builds the SDK's sdist and a wheel from it, tests the
|
|
97
|
+
installed wheel in isolation at its dependency floors, then publishes both
|
|
98
|
+
artifacts to public PyPI as `goaly-looper`. It publishes only this standalone
|
|
99
|
+
SDK, never the server or the root project. PyPI distributions are publicly
|
|
100
|
+
accessible and discoverable; hosted API access still requires a Looper API key.
|
|
101
|
+
|
|
102
|
+
The version comes from `looper_sdk/_version.py`. Use beta versions such as
|
|
103
|
+
`0.1.0b1`, then `0.1.0b2`; never replace a release with different contents.
|
|
104
|
+
|
|
105
|
+
One-time setup: create a [PyPI account](https://pypi.org/account/register/), enable
|
|
106
|
+
2FA, and create an [API token](https://pypi.org/manage/account/token/). The first
|
|
107
|
+
upload creates the project, so its token initially needs account-wide scope.
|
|
108
|
+
Once the project exists, replace it with a token scoped to `goaly-looper`.
|
|
109
|
+
The name must be available, or the account must already have publishing access.
|
|
110
|
+
|
|
111
|
+
With `uv` and Python 3 installed, run from the repository root:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Read the token without echoing it or storing its literal value in shell history.
|
|
115
|
+
read -r -s -p "PyPI API token: " UV_PUBLISH_TOKEN
|
|
116
|
+
printf '\n'
|
|
117
|
+
export UV_PUBLISH_TOKEN
|
|
118
|
+
bash looper/sdk/publish.sh
|
|
119
|
+
unset UV_PUBLISH_TOKEN
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The hidden-prompt example uses Bash; in zsh use
|
|
123
|
+
`read -r -s 'UV_PUBLISH_TOKEN?PyPI API token: '` instead of its `read` line.
|
|
124
|
+
Never put the token in a notebook or commit it. `uv` uses a temporary Python 3.12
|
|
125
|
+
virtual environment for the SDK tests. No GCS credentials or bucket settings
|
|
126
|
+
are needed. The script works from any directory when called by its path.
|
|
127
|
+
|
|
128
|
+
Uploads use PyPI's check URL so retrying an unchanged release can complete a
|
|
129
|
+
partial upload without replacing existing files. An upload failure exits
|
|
130
|
+
nonzero; completed uploads may remain on PyPI. After uploading, the script
|
|
131
|
+
checks PyPI's release metadata, downloads both public artifacts and verifies
|
|
132
|
+
their SHA-256 against the local build. Only then does it print `SUCCESS`, the
|
|
133
|
+
project URL and an exact-version Colab/Jupyter install command. Verification
|
|
134
|
+
failure exits nonzero and leaves the release in place for investigation.
|
|
135
|
+
Temporary local artifacts are removed on exit. These checks prove SDK unit
|
|
136
|
+
behavior and artifact availability, not compatibility with a deployed service.
|
|
137
|
+
|
|
138
|
+
Users install the beta in Colab or local Jupyter with:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
%pip install "goaly-looper==0.1.0b1"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
An exact prerelease pin does not require `--pre`. Future automation can use
|
|
145
|
+
PyPI Trusted Publishing; the local release script needs no CI configuration.
|
|
146
|
+
|
|
147
|
+
## Readiness and explicit closure
|
|
148
|
+
|
|
149
|
+
`create_training_client(config, key=..., timeout_s=...)` returns after the
|
|
150
|
+
training node is ready. The default wait has no overall deadline.
|
|
151
|
+
`CreationTimeout` carries the originating session ID, creation key and run ID
|
|
152
|
+
when known; accepted work remains intact. Service failures raise `CreationError`
|
|
153
|
+
with the original typed error in `.cause` and `__cause__`, preserving its
|
|
154
|
+
traceback and retry semantics.
|
|
155
|
+
An existing handle can `await training.wait_until_ready(timeout_s=...)`.
|
|
156
|
+
|
|
157
|
+
Explicit service closure, including context exit, rejects new calls with
|
|
158
|
+
`SessionClosed`. Pending calls have up to 60 seconds to finish while heartbeats
|
|
159
|
+
continue. Cleanup then stops heartbeats, cancels remaining calls and closes the
|
|
160
|
+
session and HTTP client. A call interrupted by shutdown raises `SessionClosed`;
|
|
161
|
+
caller cancellation remains `CancelledError`. The default close wait is 70
|
|
162
|
+
seconds; cancellation or a caller deadline leaves shared cleanup running while
|
|
163
|
+
the event loop remains alive. A later `close()` can wait again. Backend runs
|
|
164
|
+
end on session closure or expiry; workers retire asynchronously. Continuation
|
|
165
|
+
uses a new service and run, optionally from a saved checkpoint. Session identity
|
|
166
|
+
is immutable and expiry never renews it. Sampling clients, once added, must
|
|
167
|
+
borrow the same session lifetime.
|
|
168
|
+
|
|
169
|
+
`await training.save_state(name)` accepts a save; `await operation.result()`
|
|
170
|
+
answers once the step is on the worker group's local storage, with the
|
|
171
|
+
checkpoint `uploading`, while the run trains on. `await
|
|
172
|
+
service.wait_checkpoint(checkpoint.id)` waits until it is `ready_to_use`:
|
|
173
|
+
uploaded, named and loadable; it raises `CheckpointFailed` if the upload
|
|
174
|
+
never finished ([sdk.md §4.6](../docs/sdk.md#46-checkpoints-and-exports)).
|
|
175
|
+
Registry lookup, list and delete methods use `ServiceClient`.
|
|
176
|
+
|
|
177
|
+
## Checkpoint model metadata
|
|
178
|
+
|
|
179
|
+
Every `StateCheckpoint` includes `model_spec: ModelSpec`, exported from
|
|
180
|
+
`looper_sdk`: the source run's `model` name and exact `Full()` or `Lora(...)`
|
|
181
|
+
configuration, including rank and target switches. It excludes workload sizing
|
|
182
|
+
and does not resolve a Hugging Face revision. Save results, registry lookups and
|
|
183
|
+
lists return this metadata; historical save results are completed by the server
|
|
184
|
+
from the immutable source run.
|
|
185
|
+
|
|
186
|
+
The updated SDK requires an updated server: missing or malformed model metadata
|
|
187
|
+
raises `InvalidArgument`. Older SDKs can ignore the additive binary protobuf
|
|
188
|
+
field. Clients using strict JSON schemas must allow the new response field.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Looper's SDK: the client and the typed values of Looper's API, ``looper.v1``
|
|
2
|
+
(looper/docs/sdk.md).
|
|
3
|
+
|
|
4
|
+
It imports nothing of ours, so it installs and runs on its own
|
|
5
|
+
(looper/docs/README.md, "Who imports what"), with httpx and protobuf.
|
|
6
|
+
``ServiceClient`` opens the program's session; each run it creates has
|
|
7
|
+
a ``TrainingClient``, and each operation it submits an
|
|
8
|
+
``Operation`` (sdk.md §3, §6).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from looper_sdk._version import __version__
|
|
12
|
+
from looper_sdk.checkpoints import CheckpointState, StateCheckpoint
|
|
13
|
+
from looper_sdk.errors import (
|
|
14
|
+
Cancelled,
|
|
15
|
+
CapacityExhausted,
|
|
16
|
+
Category,
|
|
17
|
+
CheckpointFailed,
|
|
18
|
+
CheckpointNotReady,
|
|
19
|
+
CreationError,
|
|
20
|
+
CreationTimeout,
|
|
21
|
+
DependencyFailed,
|
|
22
|
+
EmptyStep,
|
|
23
|
+
ErrorCode,
|
|
24
|
+
GradientsPending,
|
|
25
|
+
IdempotencyConflict,
|
|
26
|
+
IncompleteAccumulation,
|
|
27
|
+
Internal,
|
|
28
|
+
InvalidArgument,
|
|
29
|
+
LooperError,
|
|
30
|
+
NotFound,
|
|
31
|
+
PayloadTooLarge,
|
|
32
|
+
PermissionDenied,
|
|
33
|
+
QueueTimeout,
|
|
34
|
+
QuotaExceeded,
|
|
35
|
+
RunNotReady,
|
|
36
|
+
RunRolledBack,
|
|
37
|
+
RunStateLost,
|
|
38
|
+
RunSuspended,
|
|
39
|
+
SamplerFailed,
|
|
40
|
+
SessionClosed,
|
|
41
|
+
SessionCloseTimeout,
|
|
42
|
+
SessionExpired,
|
|
43
|
+
SpendRefused,
|
|
44
|
+
Unauthenticated,
|
|
45
|
+
Unavailable,
|
|
46
|
+
VersionSuperseded,
|
|
47
|
+
WriterSuperseded,
|
|
48
|
+
)
|
|
49
|
+
from looper_sdk.losses import CrossEntropy, Dapo, Grpo, Loss
|
|
50
|
+
from looper_sdk.operations import Operation, OperationState
|
|
51
|
+
from looper_sdk.results import DatumResult, ForwardBackwardResult, StepResult
|
|
52
|
+
from looper_sdk.sampling import (
|
|
53
|
+
FinishReason,
|
|
54
|
+
SamplerConfig,
|
|
55
|
+
SampleRequest,
|
|
56
|
+
SampleResult,
|
|
57
|
+
SamplerState,
|
|
58
|
+
SamplerStatus,
|
|
59
|
+
SampleSequence,
|
|
60
|
+
SamplingParams,
|
|
61
|
+
)
|
|
62
|
+
from looper_sdk.service import DEFAULT_BASE_URL, ServiceClient
|
|
63
|
+
from looper_sdk.training import RunState, RunStatus, TrainingClient
|
|
64
|
+
from looper_sdk.values import Datum, Full, Lora, ModelSpec, TrainingConfig, Version
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"DEFAULT_BASE_URL",
|
|
68
|
+
"StateCheckpoint",
|
|
69
|
+
"CheckpointState",
|
|
70
|
+
"Cancelled",
|
|
71
|
+
"CapacityExhausted",
|
|
72
|
+
"Category",
|
|
73
|
+
"CheckpointFailed",
|
|
74
|
+
"CheckpointNotReady",
|
|
75
|
+
"CreationError",
|
|
76
|
+
"CreationTimeout",
|
|
77
|
+
"SessionClosed",
|
|
78
|
+
"SessionCloseTimeout",
|
|
79
|
+
"CrossEntropy",
|
|
80
|
+
"Dapo",
|
|
81
|
+
"Datum",
|
|
82
|
+
"DatumResult",
|
|
83
|
+
"DependencyFailed",
|
|
84
|
+
"EmptyStep",
|
|
85
|
+
"ErrorCode",
|
|
86
|
+
"ForwardBackwardResult",
|
|
87
|
+
"Grpo",
|
|
88
|
+
"Full",
|
|
89
|
+
"GradientsPending",
|
|
90
|
+
"IdempotencyConflict",
|
|
91
|
+
"IncompleteAccumulation",
|
|
92
|
+
"Internal",
|
|
93
|
+
"InvalidArgument",
|
|
94
|
+
"Lora",
|
|
95
|
+
"ModelSpec",
|
|
96
|
+
"Loss",
|
|
97
|
+
"LooperError",
|
|
98
|
+
"NotFound",
|
|
99
|
+
"Operation",
|
|
100
|
+
"OperationState",
|
|
101
|
+
"PayloadTooLarge",
|
|
102
|
+
"PermissionDenied",
|
|
103
|
+
"QueueTimeout",
|
|
104
|
+
"QuotaExceeded",
|
|
105
|
+
"RunNotReady",
|
|
106
|
+
"RunRolledBack",
|
|
107
|
+
"RunState",
|
|
108
|
+
"RunStateLost",
|
|
109
|
+
"RunStatus",
|
|
110
|
+
"RunSuspended",
|
|
111
|
+
"FinishReason",
|
|
112
|
+
"SampleRequest",
|
|
113
|
+
"SampleResult",
|
|
114
|
+
"SampleSequence",
|
|
115
|
+
"SamplerConfig",
|
|
116
|
+
"SamplerFailed",
|
|
117
|
+
"SamplerState",
|
|
118
|
+
"SamplerStatus",
|
|
119
|
+
"SamplingParams",
|
|
120
|
+
"ServiceClient",
|
|
121
|
+
"SessionExpired",
|
|
122
|
+
"SpendRefused",
|
|
123
|
+
"StepResult",
|
|
124
|
+
"TrainingClient",
|
|
125
|
+
"TrainingConfig",
|
|
126
|
+
"Unauthenticated",
|
|
127
|
+
"Unavailable",
|
|
128
|
+
"Version",
|
|
129
|
+
"VersionSuperseded",
|
|
130
|
+
"WriterSuperseded",
|
|
131
|
+
"__version__",
|
|
132
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Named checkpoint values returned by the registry (sdk.md §4.6)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import enum
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from looper_sdk.errors import InvalidArgument
|
|
11
|
+
from looper_sdk.values import ModelSpec, Version
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CheckpointState(enum.StrEnum):
|
|
15
|
+
"""Where a checkpoint is (sdk.md §4.6). A save succeeds once its step is
|
|
16
|
+
on the worker group's local SSD, and its checkpoint is ``UPLOADING``; only
|
|
17
|
+
a ``READY_TO_USE`` one is on the bucket, named and loadable."""
|
|
18
|
+
|
|
19
|
+
UPLOADING = "uploading"
|
|
20
|
+
READY_TO_USE = "ready_to_use"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
|
25
|
+
class StateCheckpoint:
|
|
26
|
+
"""A checkpoint observed at save or registry lookup time.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
name: The name it binds once ``READY_TO_USE``; until then, the name its
|
|
30
|
+
save asked for, which still resolves to its previous target.
|
|
31
|
+
expires_at: Set once ``READY_TO_USE``, when its TTL starts.
|
|
32
|
+
model_spec: Source run model name and exact trainable parameters.
|
|
33
|
+
principal: Creating session's user or service identity.
|
|
34
|
+
version: Exact source run, epoch and committed optimizer step.
|
|
35
|
+
size_bytes: Physical directory size, known once ``READY_TO_USE``, and 0
|
|
36
|
+
before; aliases do not duplicate bytes.
|
|
37
|
+
created_at: Time of registry publication once ``READY_TO_USE``; before,
|
|
38
|
+
the time its save succeeded. With timezone.
|
|
39
|
+
state: Where it is; see ``CheckpointState``.
|
|
40
|
+
failure: Why a ``FAILED`` checkpoint failed, for people; None otherwise.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
expires_at: datetime | None
|
|
45
|
+
id: str
|
|
46
|
+
operation_id: str
|
|
47
|
+
project: str
|
|
48
|
+
principal: str
|
|
49
|
+
source_run_id: str
|
|
50
|
+
version: Version
|
|
51
|
+
model_spec: ModelSpec
|
|
52
|
+
size_bytes: int
|
|
53
|
+
created_at: datetime
|
|
54
|
+
state: CheckpointState
|
|
55
|
+
failure: str | None = None
|
|
56
|
+
kind: ClassVar[str] = "state"
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
if not isinstance(self.model_spec, ModelSpec):
|
|
60
|
+
raise InvalidArgument("Checkpoint.model_spec must be a ModelSpec")
|
|
61
|
+
if not isinstance(self.state, CheckpointState):
|
|
62
|
+
raise InvalidArgument("Checkpoint.state must be a CheckpointState")
|
|
63
|
+
if (self.failure is not None) != (self.state is CheckpointState.FAILED):
|
|
64
|
+
raise InvalidArgument("Checkpoint.failure is set exactly when its state is FAILED")
|