flopscope-server 0.4.0__tar.gz → 0.4.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.
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/PKG-INFO +2 -2
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/pyproject.toml +2 -2
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/__init__.py +1 -1
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_protocol.py +4 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_request_handler.py +50 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_session.py +26 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/.gitignore +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/README.md +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/__main__.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_array_store.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_comms_tracker.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_server.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_array_store.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_bugfixes_round2.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_bugfixes_round3.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_comms_tracker.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_new_types.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_protocol.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_request_handler.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_server.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_session.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_version_handshake.py +0 -0
- {flopscope_server-0.4.0 → flopscope_server-0.4.2}/uv.lock +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flopscope-server
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Backend server for flopscope client-server architecture
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
|
-
Requires-Dist: flopscope==0.4.
|
|
7
|
+
Requires-Dist: flopscope==0.4.2
|
|
8
8
|
Requires-Dist: msgpack>=1.0.0
|
|
9
9
|
Requires-Dist: numpy<2.5.0,>=2.0.0
|
|
10
10
|
Requires-Dist: pyzmq>=26.0.0
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flopscope-server"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Backend server for flopscope client-server architecture"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
dependencies = [
|
|
13
|
-
"flopscope==0.4.
|
|
13
|
+
"flopscope==0.4.2",
|
|
14
14
|
"numpy>=2.0.0,<2.5.0",
|
|
15
15
|
"pyzmq>=26.0.0",
|
|
16
16
|
"msgpack>=1.0.0",
|
|
@@ -130,6 +130,10 @@ def validate_request(msg: dict) -> None:
|
|
|
130
130
|
If the op name is not in :data:`WHITELIST`.
|
|
131
131
|
"""
|
|
132
132
|
op = msg.get("op", "")
|
|
133
|
+
# Generator.<method> ops are dispatched to a server-side RNG handle and are
|
|
134
|
+
# method-whitelisted in the request handler (_ALLOWED_GEN_METHODS).
|
|
135
|
+
if op.startswith("Generator."):
|
|
136
|
+
return
|
|
133
137
|
if op not in WHITELIST:
|
|
134
138
|
raise InvalidRequestError(f"unknown op: {op!r}")
|
|
135
139
|
|
|
@@ -14,6 +14,27 @@ from flopscope_server._session import Session
|
|
|
14
14
|
|
|
15
15
|
_HANDLE_RE = re.compile(r"^a\d+$")
|
|
16
16
|
|
|
17
|
+
# Generator methods the server will dispatch to a server-side numpy Generator
|
|
18
|
+
# resolved from a ``{"__gen__": handle}`` argument. Array-returning samplers
|
|
19
|
+
# only (``shuffle`` mutates in place and is intentionally excluded).
|
|
20
|
+
_ALLOWED_GEN_METHODS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"uniform",
|
|
23
|
+
"standard_normal",
|
|
24
|
+
"normal",
|
|
25
|
+
"integers",
|
|
26
|
+
"random",
|
|
27
|
+
"standard_exponential",
|
|
28
|
+
"exponential",
|
|
29
|
+
"poisson",
|
|
30
|
+
"binomial",
|
|
31
|
+
"beta",
|
|
32
|
+
"gamma",
|
|
33
|
+
"choice",
|
|
34
|
+
"permutation",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
17
38
|
|
|
18
39
|
def _make_serializable(obj):
|
|
19
40
|
"""Convert a nested structure to be msgpack-safe (no numpy types)."""
|
|
@@ -221,6 +242,24 @@ class RequestHandler:
|
|
|
221
242
|
result = arr.astype(dtype)
|
|
222
243
|
return self._pack_result(result)
|
|
223
244
|
|
|
245
|
+
# Generator method calls: op is "Generator.<method>" with the remote
|
|
246
|
+
# generator handle as the first arg. Resolve it and call the method
|
|
247
|
+
# server-side, so the RNG state lives + advances on the server (the
|
|
248
|
+
# stream stays deterministic per seed and FLOP-counted).
|
|
249
|
+
if op.startswith("Generator."):
|
|
250
|
+
method = op[len("Generator.") :]
|
|
251
|
+
if method not in _ALLOWED_GEN_METHODS:
|
|
252
|
+
return {
|
|
253
|
+
"status": "error",
|
|
254
|
+
"error_type": "UnsupportedFunctionError",
|
|
255
|
+
"message": f"Generator.{method} is not supported by the flopscope server",
|
|
256
|
+
}
|
|
257
|
+
gen = self._resolve_arg(raw_args[0])
|
|
258
|
+
rest = [self._resolve_arg(a) for a in raw_args[1:]]
|
|
259
|
+
resolved_kwargs = {k: self._resolve_arg(v) for k, v in kwargs.items()}
|
|
260
|
+
result = getattr(gen, method)(*rest, **resolved_kwargs)
|
|
261
|
+
return self._pack_result(result)
|
|
262
|
+
|
|
224
263
|
func = _get_flopscope_func(op)
|
|
225
264
|
resolved_args = [self._resolve_arg(a) for a in raw_args]
|
|
226
265
|
resolved_kwargs = {k: self._resolve_arg(v) for k, v in kwargs.items()}
|
|
@@ -247,6 +286,13 @@ class RequestHandler:
|
|
|
247
286
|
if isinstance(handle, bytes):
|
|
248
287
|
handle = handle.decode("utf-8")
|
|
249
288
|
return self._session.get_array(handle)
|
|
289
|
+
gen_handle = arg.get("__gen__")
|
|
290
|
+
if gen_handle is None:
|
|
291
|
+
gen_handle = arg.get(b"__gen__")
|
|
292
|
+
if gen_handle is not None:
|
|
293
|
+
if isinstance(gen_handle, bytes):
|
|
294
|
+
gen_handle = gen_handle.decode("utf-8")
|
|
295
|
+
return self._session.get_generator(gen_handle)
|
|
250
296
|
# SymmetryGroup wire format
|
|
251
297
|
pg_data = arg.get("__symmetry_group__") or arg.get(b"__symmetry_group__")
|
|
252
298
|
if pg_data is not None:
|
|
@@ -370,6 +416,10 @@ class RequestHandler:
|
|
|
370
416
|
"result": {"value": str(result), "dtype": "str"},
|
|
371
417
|
"budget": budget,
|
|
372
418
|
}
|
|
419
|
+
if isinstance(result, np.random.Generator):
|
|
420
|
+
handle = self._session.store_generator(result)
|
|
421
|
+
return {"status": "ok", "result": {"gen_id": handle}, "budget": budget}
|
|
422
|
+
|
|
373
423
|
# Fallback: try to make it serializable
|
|
374
424
|
try:
|
|
375
425
|
serializable = _make_serializable(result)
|
|
@@ -24,6 +24,8 @@ class Session:
|
|
|
24
24
|
|
|
25
25
|
def __init__(self, flop_budget: int, flop_multiplier: float = 1.0) -> None:
|
|
26
26
|
self._store = ArrayStore()
|
|
27
|
+
self._generators: dict[str, Any] = {}
|
|
28
|
+
self._gen_counter: int = 0
|
|
27
29
|
self._comms_tracker = CommsTracker()
|
|
28
30
|
self._budget_ctx = flops.BudgetContext(
|
|
29
31
|
flop_budget=flop_budget,
|
|
@@ -109,6 +111,29 @@ class Session:
|
|
|
109
111
|
"""
|
|
110
112
|
self._store.free(handles)
|
|
111
113
|
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Generator operations (server-side RNG handles)
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def store_generator(self, gen: Any) -> str:
|
|
119
|
+
"""Store an RNG ``Generator`` and return its handle ID (``g0``, ``g1`` …)."""
|
|
120
|
+
handle = f"g{self._gen_counter}"
|
|
121
|
+
self._generators[handle] = gen
|
|
122
|
+
self._gen_counter += 1
|
|
123
|
+
return handle
|
|
124
|
+
|
|
125
|
+
def get_generator(self, handle: str) -> Any:
|
|
126
|
+
"""Return the ``Generator`` for *handle*.
|
|
127
|
+
|
|
128
|
+
Raises
|
|
129
|
+
------
|
|
130
|
+
KeyError
|
|
131
|
+
If *handle* is not a known generator handle.
|
|
132
|
+
"""
|
|
133
|
+
if handle not in self._generators:
|
|
134
|
+
raise KeyError(f"Generator handle {handle!r} not found in store")
|
|
135
|
+
return self._generators[handle]
|
|
136
|
+
|
|
112
137
|
# ------------------------------------------------------------------
|
|
113
138
|
# Budget
|
|
114
139
|
# ------------------------------------------------------------------
|
|
@@ -162,6 +187,7 @@ class Session:
|
|
|
162
187
|
budget_summary = self._budget_ctx.summary(by_namespace=show_namespaces)
|
|
163
188
|
comms_summary = self._comms_tracker.summary()
|
|
164
189
|
self._store.clear()
|
|
190
|
+
self._generators.clear()
|
|
165
191
|
self._is_open = False
|
|
166
192
|
|
|
167
193
|
return {
|
|
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
|