positronic-model-server 0.2.2__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.
- positronic_model_server-0.2.2/.gitignore +167 -0
- positronic_model_server-0.2.2/PKG-INFO +214 -0
- positronic_model_server-0.2.2/README.md +197 -0
- positronic_model_server-0.2.2/positronic_model_server/grpc_wire.py +166 -0
- positronic_model_server-0.2.2/positronic_model_server/keys.py +10 -0
- positronic_model_server-0.2.2/positronic_model_server/protocol.py +60 -0
- positronic_model_server-0.2.2/positronic_model_server/py.typed +0 -0
- positronic_model_server-0.2.2/positronic_model_server/serialization.py +144 -0
- positronic_model_server-0.2.2/positronic_model_server/server.py +309 -0
- positronic_model_server-0.2.2/positronic_model_server/server_wire.py +106 -0
- positronic_model_server-0.2.2/positronic_model_server/spec.py +78 -0
- positronic_model_server-0.2.2/positronic_model_server/tests/test_model_server.py +472 -0
- positronic_model_server-0.2.2/positronic_model_server/tests/test_serialization.py +115 -0
- positronic_model_server-0.2.2/positronic_model_server/tests/test_spec.py +85 -0
- positronic_model_server-0.2.2/positronic_model_server/websocket_wire.py +337 -0
- positronic_model_server-0.2.2/pyproject.toml +24 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
.venv-typecheck
|
|
126
|
+
env/
|
|
127
|
+
venv/
|
|
128
|
+
ENV/
|
|
129
|
+
env.bak/
|
|
130
|
+
venv.bak/
|
|
131
|
+
|
|
132
|
+
# Spyder project settings
|
|
133
|
+
.spyderproject
|
|
134
|
+
.spyproject
|
|
135
|
+
|
|
136
|
+
# Rope project settings
|
|
137
|
+
.ropeproject
|
|
138
|
+
|
|
139
|
+
# mkdocs documentation
|
|
140
|
+
/site
|
|
141
|
+
|
|
142
|
+
# mypy
|
|
143
|
+
.mypy_cache/
|
|
144
|
+
.dmypy.json
|
|
145
|
+
dmypy.json
|
|
146
|
+
|
|
147
|
+
# Pyre type checker
|
|
148
|
+
.pyre/
|
|
149
|
+
|
|
150
|
+
# pytype static type analyzer
|
|
151
|
+
.pytype/
|
|
152
|
+
|
|
153
|
+
# Cython debug symbols
|
|
154
|
+
cython_debug/
|
|
155
|
+
|
|
156
|
+
# PyCharm
|
|
157
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
158
|
+
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
|
159
|
+
# project, it is generally recommended to exclude comment-based line additions.
|
|
160
|
+
.idea/
|
|
161
|
+
|
|
162
|
+
# VSCode
|
|
163
|
+
.vscode/
|
|
164
|
+
*.code-workspace
|
|
165
|
+
|
|
166
|
+
# Environment variables
|
|
167
|
+
.env*
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: positronic-model-server
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Model serving, session lifecycle and serialization without Positronic dependencies
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: msgpack>=1
|
|
8
|
+
Requires-Dist: numpy>=1.26
|
|
9
|
+
Requires-Dist: pillow>=10
|
|
10
|
+
Requires-Dist: positronic-wire==0.12.0
|
|
11
|
+
Provides-Extra: grpc
|
|
12
|
+
Requires-Dist: positronic-wire[grpc]==0.12.0; extra == 'grpc'
|
|
13
|
+
Provides-Extra: websocket
|
|
14
|
+
Requires-Dist: positronic-wire[websocket]==0.12.0; extra == 'websocket'
|
|
15
|
+
Requires-Dist: uvicorn>=0.35; extra == 'websocket'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# positronic-model-server
|
|
19
|
+
|
|
20
|
+
Serve a native model inside its own Python environment, without Positronic, Torch or JAX dependencies.
|
|
21
|
+
The wrapper owns transport, sessions, serialization and cleanup. Vendor scripts load and call their
|
|
22
|
+
model directly in the same process and describe the client pipeline as plain data.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv pip install "positronic-model-server==0.2.2"
|
|
26
|
+
uv pip install "positronic-model-server[websocket]==0.2.2"
|
|
27
|
+
uv pip install "positronic-model-server[grpc]==0.2.2"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The core depends on NumPy, Pillow, msgpack and `positronic-wire`'s dependency-free transport
|
|
31
|
+
definitions. The extras select transport libraries independently; combine them as
|
|
32
|
+
`[websocket,grpc]` when both are needed. WebSocket serving uses Uvicorn directly; gRPC does not
|
|
33
|
+
require it. Starlette, AnyIO, FastAPI and Pydantic are not dependencies of either installation.
|
|
34
|
+
In a checkout, install the workspace packages together: `uv pip install ./wire ./model_server`.
|
|
35
|
+
|
|
36
|
+
## Serving and lifetime
|
|
37
|
+
|
|
38
|
+
This test server returns each observation as a one-element result list:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from positronic_model_server.server import Model, ModelServer, Session
|
|
42
|
+
from positronic_model_server.server_wire import ServedHostPort
|
|
43
|
+
from positronic_model_server.spec import component
|
|
44
|
+
from positronic_model_server.websocket_wire import WebsocketWire
|
|
45
|
+
|
|
46
|
+
class EchoSession(Session):
|
|
47
|
+
def __init__(self, fps):
|
|
48
|
+
super().__init__(component("chunked_schedule", version=2, fps=fps))
|
|
49
|
+
|
|
50
|
+
def infer(self, observation):
|
|
51
|
+
return [observation]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EchoModel(Model):
|
|
55
|
+
def __init__(self):
|
|
56
|
+
super().__init__(parameters={"fps": 20}, metadata={"checkpoint_id": "example"})
|
|
57
|
+
|
|
58
|
+
def prepare_session(self, params):
|
|
59
|
+
if params["fps"] <= 0:
|
|
60
|
+
raise ValueError("fps must be positive")
|
|
61
|
+
return EchoSession(params["fps"])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
server = ModelServer(EchoModel, idle_timeout_min=10)
|
|
65
|
+
server.serve([WebsocketWire(ServedHostPort("0.0.0.0", 8000))])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Implement `Model.prepare_session` and `Session.infer` in subclasses. Each `Session` owns its state
|
|
69
|
+
and may reference the loaded model. Override `close()` to release owned resources; its default does
|
|
70
|
+
nothing. The constructors accept the client description, parameters, metadata and output image settings.
|
|
71
|
+
|
|
72
|
+
Each vendor supplies its own script and argument parsing. The factory passed to `ModelServer` can be
|
|
73
|
+
a model class or a function. It loads weights and finishes shared warm-up before returning.
|
|
74
|
+
Listeners bind afterwards, so any successful keepalive answer means shared startup has finished.
|
|
75
|
+
The factory owns cleanup if loading raises.
|
|
76
|
+
|
|
77
|
+
`Model.parameters` declares accepted session names and defaults. Query values are decoded as JSON,
|
|
78
|
+
or retained as strings when they are not JSON. Duplicate or unknown names are refused; strings
|
|
79
|
+
never name Python imports. Every session gets an independent copy of the resolved parameters.
|
|
80
|
+
`prepare_session` decides how they affect inference and the client description, validates their
|
|
81
|
+
meaning and completes configuration-specific warm-up. It returns a `Session` only when the first
|
|
82
|
+
real inference needs no deferred loading or compilation. Warm-up must preserve the intended initial
|
|
83
|
+
session state. A preparation that raises owns cleanup of anything it acquired.
|
|
84
|
+
|
|
85
|
+
The server sends `waiting` messages during preparation, including while another session holds the
|
|
86
|
+
model. It then sends `ready` with a session ID, protocol v3, the pipeline description and metadata:
|
|
87
|
+
|
|
88
|
+
- `session_params`: the received query parameters, after decoding;
|
|
89
|
+
- `effective_params`: those parameters with declared defaults applied;
|
|
90
|
+
- `model_server_version`: this distribution's version;
|
|
91
|
+
- model/session metadata supplied by the vendor, plus the listener address.
|
|
92
|
+
|
|
93
|
+
The client may report any additional metadata and owns how it records or combines that information.
|
|
94
|
+
The server checks description structure without importing client components.
|
|
95
|
+
|
|
96
|
+
All model operations run on one worker thread in the server process: load, preparation, inference,
|
|
97
|
+
session cleanup and model cleanup. Each session retains its own settings and history.
|
|
98
|
+
Inference is serialized across all listeners. Queue time is reported separately from model
|
|
99
|
+
time. The event loop answers keepalive while the worker is busy; no waiting messages interrupt an
|
|
100
|
+
inference response. A failed inference reports an error and keeps the session open. A malformed
|
|
101
|
+
session request ends that session.
|
|
102
|
+
|
|
103
|
+
Clients own recovery from reported inference errors. The recommended client behavior is to close
|
|
104
|
+
the affected session and, if retrying, open a fresh session and wait for `ready`. Continuing in the
|
|
105
|
+
same session is an explicit client choice for failures it knows are recoverable. An open session
|
|
106
|
+
does not guarantee that a failed inference left the model's state valid.
|
|
107
|
+
|
|
108
|
+
`Session.close` runs after outstanding work, on explicit close, disconnect or server shutdown.
|
|
109
|
+
The end-session acknowledgement follows cleanup. `Model.close` runs after sessions finish and
|
|
110
|
+
listeners stop, including when startup fails after loading. Call `server.shutdown()` from any thread;
|
|
111
|
+
an idle timeout ends the server only when no sessions remain. A model operation must return for
|
|
112
|
+
graceful shutdown to complete.
|
|
113
|
+
|
|
114
|
+
Model-specific implementations own cleanup and its failure policy. Cleanup methods are expected
|
|
115
|
+
to release their resources and complete without raising. If `Session.close` raises, the wrapper logs
|
|
116
|
+
the failure, attempts to report it to the client and ends the connection; it continues serving other
|
|
117
|
+
sessions. A cleanup failure can leak resources or leave shared model state invalid. The implementation
|
|
118
|
+
is responsible for recovery or for requesting `server.shutdown()` when continued serving is unsafe.
|
|
119
|
+
|
|
120
|
+
Pass `auth_token` to require a bearer token on both session and keepalive calls. `None` serves open;
|
|
121
|
+
empty or malformed tokens are refused at construction. For gRPC, use `grpc_wire.GrpcWire` with its
|
|
122
|
+
own `ServedHostPort`. Both listeners can be passed to one server and share its loaded model.
|
|
123
|
+
|
|
124
|
+
## Protocol compatibility
|
|
125
|
+
|
|
126
|
+
The shared server sends v3 results without interpreting robot-command fields or reshaping chunks.
|
|
127
|
+
Updated Positronic clients select this behavior while preserving v1/v2 command decoding for legacy
|
|
128
|
+
servers. A client that only supports v1/v2 rejects v3 during opening. Upgrade clients before migrating
|
|
129
|
+
their endpoint. Legacy servers and their server-side codecs remain supported in `positronic.offboard`.
|
|
130
|
+
|
|
131
|
+
## Client pipeline descriptions
|
|
132
|
+
|
|
133
|
+
Vendor code describes components as JSON-compatible data. Only the Positronic client imports
|
|
134
|
+
and instantiates their implementations:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from positronic_model_server.spec import component, parallel, sequence
|
|
138
|
+
|
|
139
|
+
description = sequence(
|
|
140
|
+
component("chunked_schedule", version=2, fps=20),
|
|
141
|
+
parallel(
|
|
142
|
+
component("restrict_image_size", width=320, height=180),
|
|
143
|
+
component("metadata", values={"model": "example"}),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`component` accepts a name, explicit version and keyword arguments, normalizing arguments through
|
|
149
|
+
JSON. `sequence` and `parallel` reject empty compositions. Component names and argument meanings
|
|
150
|
+
belong to the client registry; these helpers do not resolve classes or validate model parameters.
|
|
151
|
+
The structural keys (`NAME`, `VERSION`, `ARGS`, `SEQ`, `PAR`) have one definition in `spec`.
|
|
152
|
+
|
|
153
|
+
The wrapper defines only constants used by its own production code. Vendor settings belong to small
|
|
154
|
+
vendor modules that training and serving can both import; dataset file conventions belong to the
|
|
155
|
+
Positronic exporters and their producers. Client component
|
|
156
|
+
names belong to the client implementations; vendor descriptions spell those names as part of the
|
|
157
|
+
client's wire contract. Validate each description against the real client registry in vendor tests.
|
|
158
|
+
The wrapper forwards model metadata and parameter values without owning their field names.
|
|
159
|
+
|
|
160
|
+
## Serialization
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from positronic_model_server.serialization import deserialise, encode_jpeg, serialise
|
|
164
|
+
|
|
165
|
+
message = serialise({"camera": encode_jpeg(image, quality=90), "state": state})
|
|
166
|
+
restored = deserialise(message)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Plain values, nested mappings/sequences, NumPy arrays and NumPy scalars use the shared msgpack
|
|
170
|
+
representation. Ordinary arrays are lossless, including arrays whose dimensions resemble images.
|
|
171
|
+
JPEG compression is explicit: call `encode_jpeg` on the values to compress. It accepts RGB arrays
|
|
172
|
+
shaped `(..., H, W, 3)`, casts pixels to uint8 and preserves every leading dimension. It is lossy.
|
|
173
|
+
The receiver recognizes each encoded value without a field schema. The same functions encode
|
|
174
|
+
observations or model results, including returned images.
|
|
175
|
+
|
|
176
|
+
`serialization.JpegEncoding(path, quality)` selects a nested mapping/list value, with an empty path
|
|
177
|
+
selecting the whole result. Supply these selectors through `Session.output_images` for returned
|
|
178
|
+
images. `serialization.encode_images` applies the same selections to observations without changing
|
|
179
|
+
the source containers. A missing path fails instead of silently skipping compression.
|
|
180
|
+
|
|
181
|
+
The client component `encode_images` defaults to recursively JPEG-encoding `uint8` arrays shaped
|
|
182
|
+
`(..., H, W, 3)` with positive height and width, including batches, inside mappings, lists and tuples.
|
|
183
|
+
Supply `paths` to encode only selected values; `paths=[]` disables compression. `quality` sets the
|
|
184
|
+
JPEG quality. Automatic detection is a heuristic: float images are left lossless, and unrelated
|
|
185
|
+
`uint8` arrays matching the image shape are compressed. Its result decoding leaves native values
|
|
186
|
+
unchanged. The wire serializer itself does not infer image fields.
|
|
187
|
+
|
|
188
|
+
Nonempty 3D and 4D images retain the v1/v2 JPEG representation. Extra leading dimensions and empty
|
|
189
|
+
batches carry a full-shape marker; send those only to receivers supporting it. NumPy object,
|
|
190
|
+
structured and complex dtypes are unsupported. Python tuples arrive as lists.
|
|
191
|
+
|
|
192
|
+
`protocol` owns shared message fields, authentication, timing and protocol version names.
|
|
193
|
+
Serialization here never interprets robot-command fields or `__cmd__` envelopes. Positronic's
|
|
194
|
+
legacy offboard protocol owns those hooks and its supported-version selection.
|
|
195
|
+
|
|
196
|
+
## Package checks
|
|
197
|
+
|
|
198
|
+
Run `uv run pytest model_server positronic/offboard/tests positronic/policy/tests wire` from the
|
|
199
|
+
repository root. CI also builds wheels and installs them in isolated environments with NumPy
|
|
200
|
+
1.26.4 on Python 3.11 and 3.12, selecting no transport, WebSocket alone, or gRPC alone. Root tests
|
|
201
|
+
exercise the repository's locked NumPy version.
|
|
202
|
+
|
|
203
|
+
Runtime dependencies are a design constraint. Before installing test tools, CI checks every
|
|
204
|
+
installed distribution against the per-transport allowlist in
|
|
205
|
+
`utilities/check_model_server_package.py`. Any additional runtime package fails the check,
|
|
206
|
+
whether direct or indirect, including dependencies introduced by upstream package updates.
|
|
207
|
+
Transport dependencies must stay optional. Changes to the allowlist require explicit design
|
|
208
|
+
review; do not expand it merely to make CI pass. The dependency check uses only the standard library.
|
|
209
|
+
|
|
210
|
+
The same check scans every wrapper Python module for module-level constants with no internal use.
|
|
211
|
+
Adding one fails CI, including in a new module. Test/example references and imports or assignments
|
|
212
|
+
that only re-export a constant do not count. Move such definitions to their owning vendor, client or
|
|
213
|
+
legacy protocol module; do not add artificial uses to satisfy the check. Source references establish
|
|
214
|
+
use, while review checks that the behavior itself belongs to the wrapper.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# positronic-model-server
|
|
2
|
+
|
|
3
|
+
Serve a native model inside its own Python environment, without Positronic, Torch or JAX dependencies.
|
|
4
|
+
The wrapper owns transport, sessions, serialization and cleanup. Vendor scripts load and call their
|
|
5
|
+
model directly in the same process and describe the client pipeline as plain data.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv pip install "positronic-model-server==0.2.2"
|
|
9
|
+
uv pip install "positronic-model-server[websocket]==0.2.2"
|
|
10
|
+
uv pip install "positronic-model-server[grpc]==0.2.2"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The core depends on NumPy, Pillow, msgpack and `positronic-wire`'s dependency-free transport
|
|
14
|
+
definitions. The extras select transport libraries independently; combine them as
|
|
15
|
+
`[websocket,grpc]` when both are needed. WebSocket serving uses Uvicorn directly; gRPC does not
|
|
16
|
+
require it. Starlette, AnyIO, FastAPI and Pydantic are not dependencies of either installation.
|
|
17
|
+
In a checkout, install the workspace packages together: `uv pip install ./wire ./model_server`.
|
|
18
|
+
|
|
19
|
+
## Serving and lifetime
|
|
20
|
+
|
|
21
|
+
This test server returns each observation as a one-element result list:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from positronic_model_server.server import Model, ModelServer, Session
|
|
25
|
+
from positronic_model_server.server_wire import ServedHostPort
|
|
26
|
+
from positronic_model_server.spec import component
|
|
27
|
+
from positronic_model_server.websocket_wire import WebsocketWire
|
|
28
|
+
|
|
29
|
+
class EchoSession(Session):
|
|
30
|
+
def __init__(self, fps):
|
|
31
|
+
super().__init__(component("chunked_schedule", version=2, fps=fps))
|
|
32
|
+
|
|
33
|
+
def infer(self, observation):
|
|
34
|
+
return [observation]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EchoModel(Model):
|
|
38
|
+
def __init__(self):
|
|
39
|
+
super().__init__(parameters={"fps": 20}, metadata={"checkpoint_id": "example"})
|
|
40
|
+
|
|
41
|
+
def prepare_session(self, params):
|
|
42
|
+
if params["fps"] <= 0:
|
|
43
|
+
raise ValueError("fps must be positive")
|
|
44
|
+
return EchoSession(params["fps"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
server = ModelServer(EchoModel, idle_timeout_min=10)
|
|
48
|
+
server.serve([WebsocketWire(ServedHostPort("0.0.0.0", 8000))])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Implement `Model.prepare_session` and `Session.infer` in subclasses. Each `Session` owns its state
|
|
52
|
+
and may reference the loaded model. Override `close()` to release owned resources; its default does
|
|
53
|
+
nothing. The constructors accept the client description, parameters, metadata and output image settings.
|
|
54
|
+
|
|
55
|
+
Each vendor supplies its own script and argument parsing. The factory passed to `ModelServer` can be
|
|
56
|
+
a model class or a function. It loads weights and finishes shared warm-up before returning.
|
|
57
|
+
Listeners bind afterwards, so any successful keepalive answer means shared startup has finished.
|
|
58
|
+
The factory owns cleanup if loading raises.
|
|
59
|
+
|
|
60
|
+
`Model.parameters` declares accepted session names and defaults. Query values are decoded as JSON,
|
|
61
|
+
or retained as strings when they are not JSON. Duplicate or unknown names are refused; strings
|
|
62
|
+
never name Python imports. Every session gets an independent copy of the resolved parameters.
|
|
63
|
+
`prepare_session` decides how they affect inference and the client description, validates their
|
|
64
|
+
meaning and completes configuration-specific warm-up. It returns a `Session` only when the first
|
|
65
|
+
real inference needs no deferred loading or compilation. Warm-up must preserve the intended initial
|
|
66
|
+
session state. A preparation that raises owns cleanup of anything it acquired.
|
|
67
|
+
|
|
68
|
+
The server sends `waiting` messages during preparation, including while another session holds the
|
|
69
|
+
model. It then sends `ready` with a session ID, protocol v3, the pipeline description and metadata:
|
|
70
|
+
|
|
71
|
+
- `session_params`: the received query parameters, after decoding;
|
|
72
|
+
- `effective_params`: those parameters with declared defaults applied;
|
|
73
|
+
- `model_server_version`: this distribution's version;
|
|
74
|
+
- model/session metadata supplied by the vendor, plus the listener address.
|
|
75
|
+
|
|
76
|
+
The client may report any additional metadata and owns how it records or combines that information.
|
|
77
|
+
The server checks description structure without importing client components.
|
|
78
|
+
|
|
79
|
+
All model operations run on one worker thread in the server process: load, preparation, inference,
|
|
80
|
+
session cleanup and model cleanup. Each session retains its own settings and history.
|
|
81
|
+
Inference is serialized across all listeners. Queue time is reported separately from model
|
|
82
|
+
time. The event loop answers keepalive while the worker is busy; no waiting messages interrupt an
|
|
83
|
+
inference response. A failed inference reports an error and keeps the session open. A malformed
|
|
84
|
+
session request ends that session.
|
|
85
|
+
|
|
86
|
+
Clients own recovery from reported inference errors. The recommended client behavior is to close
|
|
87
|
+
the affected session and, if retrying, open a fresh session and wait for `ready`. Continuing in the
|
|
88
|
+
same session is an explicit client choice for failures it knows are recoverable. An open session
|
|
89
|
+
does not guarantee that a failed inference left the model's state valid.
|
|
90
|
+
|
|
91
|
+
`Session.close` runs after outstanding work, on explicit close, disconnect or server shutdown.
|
|
92
|
+
The end-session acknowledgement follows cleanup. `Model.close` runs after sessions finish and
|
|
93
|
+
listeners stop, including when startup fails after loading. Call `server.shutdown()` from any thread;
|
|
94
|
+
an idle timeout ends the server only when no sessions remain. A model operation must return for
|
|
95
|
+
graceful shutdown to complete.
|
|
96
|
+
|
|
97
|
+
Model-specific implementations own cleanup and its failure policy. Cleanup methods are expected
|
|
98
|
+
to release their resources and complete without raising. If `Session.close` raises, the wrapper logs
|
|
99
|
+
the failure, attempts to report it to the client and ends the connection; it continues serving other
|
|
100
|
+
sessions. A cleanup failure can leak resources or leave shared model state invalid. The implementation
|
|
101
|
+
is responsible for recovery or for requesting `server.shutdown()` when continued serving is unsafe.
|
|
102
|
+
|
|
103
|
+
Pass `auth_token` to require a bearer token on both session and keepalive calls. `None` serves open;
|
|
104
|
+
empty or malformed tokens are refused at construction. For gRPC, use `grpc_wire.GrpcWire` with its
|
|
105
|
+
own `ServedHostPort`. Both listeners can be passed to one server and share its loaded model.
|
|
106
|
+
|
|
107
|
+
## Protocol compatibility
|
|
108
|
+
|
|
109
|
+
The shared server sends v3 results without interpreting robot-command fields or reshaping chunks.
|
|
110
|
+
Updated Positronic clients select this behavior while preserving v1/v2 command decoding for legacy
|
|
111
|
+
servers. A client that only supports v1/v2 rejects v3 during opening. Upgrade clients before migrating
|
|
112
|
+
their endpoint. Legacy servers and their server-side codecs remain supported in `positronic.offboard`.
|
|
113
|
+
|
|
114
|
+
## Client pipeline descriptions
|
|
115
|
+
|
|
116
|
+
Vendor code describes components as JSON-compatible data. Only the Positronic client imports
|
|
117
|
+
and instantiates their implementations:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from positronic_model_server.spec import component, parallel, sequence
|
|
121
|
+
|
|
122
|
+
description = sequence(
|
|
123
|
+
component("chunked_schedule", version=2, fps=20),
|
|
124
|
+
parallel(
|
|
125
|
+
component("restrict_image_size", width=320, height=180),
|
|
126
|
+
component("metadata", values={"model": "example"}),
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`component` accepts a name, explicit version and keyword arguments, normalizing arguments through
|
|
132
|
+
JSON. `sequence` and `parallel` reject empty compositions. Component names and argument meanings
|
|
133
|
+
belong to the client registry; these helpers do not resolve classes or validate model parameters.
|
|
134
|
+
The structural keys (`NAME`, `VERSION`, `ARGS`, `SEQ`, `PAR`) have one definition in `spec`.
|
|
135
|
+
|
|
136
|
+
The wrapper defines only constants used by its own production code. Vendor settings belong to small
|
|
137
|
+
vendor modules that training and serving can both import; dataset file conventions belong to the
|
|
138
|
+
Positronic exporters and their producers. Client component
|
|
139
|
+
names belong to the client implementations; vendor descriptions spell those names as part of the
|
|
140
|
+
client's wire contract. Validate each description against the real client registry in vendor tests.
|
|
141
|
+
The wrapper forwards model metadata and parameter values without owning their field names.
|
|
142
|
+
|
|
143
|
+
## Serialization
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from positronic_model_server.serialization import deserialise, encode_jpeg, serialise
|
|
147
|
+
|
|
148
|
+
message = serialise({"camera": encode_jpeg(image, quality=90), "state": state})
|
|
149
|
+
restored = deserialise(message)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Plain values, nested mappings/sequences, NumPy arrays and NumPy scalars use the shared msgpack
|
|
153
|
+
representation. Ordinary arrays are lossless, including arrays whose dimensions resemble images.
|
|
154
|
+
JPEG compression is explicit: call `encode_jpeg` on the values to compress. It accepts RGB arrays
|
|
155
|
+
shaped `(..., H, W, 3)`, casts pixels to uint8 and preserves every leading dimension. It is lossy.
|
|
156
|
+
The receiver recognizes each encoded value without a field schema. The same functions encode
|
|
157
|
+
observations or model results, including returned images.
|
|
158
|
+
|
|
159
|
+
`serialization.JpegEncoding(path, quality)` selects a nested mapping/list value, with an empty path
|
|
160
|
+
selecting the whole result. Supply these selectors through `Session.output_images` for returned
|
|
161
|
+
images. `serialization.encode_images` applies the same selections to observations without changing
|
|
162
|
+
the source containers. A missing path fails instead of silently skipping compression.
|
|
163
|
+
|
|
164
|
+
The client component `encode_images` defaults to recursively JPEG-encoding `uint8` arrays shaped
|
|
165
|
+
`(..., H, W, 3)` with positive height and width, including batches, inside mappings, lists and tuples.
|
|
166
|
+
Supply `paths` to encode only selected values; `paths=[]` disables compression. `quality` sets the
|
|
167
|
+
JPEG quality. Automatic detection is a heuristic: float images are left lossless, and unrelated
|
|
168
|
+
`uint8` arrays matching the image shape are compressed. Its result decoding leaves native values
|
|
169
|
+
unchanged. The wire serializer itself does not infer image fields.
|
|
170
|
+
|
|
171
|
+
Nonempty 3D and 4D images retain the v1/v2 JPEG representation. Extra leading dimensions and empty
|
|
172
|
+
batches carry a full-shape marker; send those only to receivers supporting it. NumPy object,
|
|
173
|
+
structured and complex dtypes are unsupported. Python tuples arrive as lists.
|
|
174
|
+
|
|
175
|
+
`protocol` owns shared message fields, authentication, timing and protocol version names.
|
|
176
|
+
Serialization here never interprets robot-command fields or `__cmd__` envelopes. Positronic's
|
|
177
|
+
legacy offboard protocol owns those hooks and its supported-version selection.
|
|
178
|
+
|
|
179
|
+
## Package checks
|
|
180
|
+
|
|
181
|
+
Run `uv run pytest model_server positronic/offboard/tests positronic/policy/tests wire` from the
|
|
182
|
+
repository root. CI also builds wheels and installs them in isolated environments with NumPy
|
|
183
|
+
1.26.4 on Python 3.11 and 3.12, selecting no transport, WebSocket alone, or gRPC alone. Root tests
|
|
184
|
+
exercise the repository's locked NumPy version.
|
|
185
|
+
|
|
186
|
+
Runtime dependencies are a design constraint. Before installing test tools, CI checks every
|
|
187
|
+
installed distribution against the per-transport allowlist in
|
|
188
|
+
`utilities/check_model_server_package.py`. Any additional runtime package fails the check,
|
|
189
|
+
whether direct or indirect, including dependencies introduced by upstream package updates.
|
|
190
|
+
Transport dependencies must stay optional. Changes to the allowlist require explicit design
|
|
191
|
+
review; do not expand it merely to make CI pass. The dependency check uses only the standard library.
|
|
192
|
+
|
|
193
|
+
The same check scans every wrapper Python module for module-level constants with no internal use.
|
|
194
|
+
Adding one fails CI, including in a new module. Test/example references and imports or assignments
|
|
195
|
+
that only re-export a constant do not count. Move such definitions to their owning vendor, client or
|
|
196
|
+
legacy protocol module; do not add artificial uses to satisfy the check. Source references establish
|
|
197
|
+
use, while review checks that the behavior itself belongs to the wrapper.
|