flopscope-server 0.4.1__tar.gz → 0.4.3__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.1 → flopscope_server-0.4.3}/PKG-INFO +2 -2
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/pyproject.toml +2 -2
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/__init__.py +1 -1
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/_protocol.py +4 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/_request_handler.py +90 -9
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/_session.py +26 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/.gitignore +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/README.md +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/__main__.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/_array_store.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/_comms_tracker.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/src/flopscope_server/_server.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_array_store.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_bugfixes_round2.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_bugfixes_round3.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_comms_tracker.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_new_types.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_protocol.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_request_handler.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_server.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_session.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/tests/test_version_handshake.py +0 -0
- {flopscope_server-0.4.1 → flopscope_server-0.4.3}/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.3
|
|
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.3
|
|
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.3"
|
|
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.3",
|
|
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)."""
|
|
@@ -28,6 +49,28 @@ def _make_serializable(obj):
|
|
|
28
49
|
return obj
|
|
29
50
|
|
|
30
51
|
|
|
52
|
+
_MSGPACK_SCALARS = (type(None), bool, int, float, str, bytes)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_msgpack_native(obj) -> bool:
|
|
56
|
+
"""True if *obj* is composed only of msgpack-encodable Python types.
|
|
57
|
+
|
|
58
|
+
``_make_serializable`` flattens numpy types but passes unknown objects
|
|
59
|
+
through unchanged; this distinguishes a genuinely-encodable result from one
|
|
60
|
+
that would only fail later inside ``msgpack.packb``.
|
|
61
|
+
"""
|
|
62
|
+
if isinstance(obj, _MSGPACK_SCALARS):
|
|
63
|
+
return True
|
|
64
|
+
if isinstance(obj, (list, tuple)):
|
|
65
|
+
return all(_is_msgpack_native(item) for item in obj)
|
|
66
|
+
if isinstance(obj, dict):
|
|
67
|
+
return all(
|
|
68
|
+
isinstance(k, (str, bytes, int)) and _is_msgpack_native(v)
|
|
69
|
+
for k, v in obj.items()
|
|
70
|
+
)
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
31
74
|
#: Maximum allowed array size in bytes (configurable via environment variable).
|
|
32
75
|
MAX_ARRAY_BYTES = int(os.environ.get("FLOPSCOPE_MAX_ARRAY_BYTES", 100 * 1024 * 1024))
|
|
33
76
|
|
|
@@ -92,6 +135,12 @@ class RequestHandler:
|
|
|
92
135
|
"error_type": "UnsupportedFunctionError",
|
|
93
136
|
"message": str(e),
|
|
94
137
|
}
|
|
138
|
+
except flops.UnsupportedReturnType as e:
|
|
139
|
+
return {
|
|
140
|
+
"status": "error",
|
|
141
|
+
"error_type": "UnsupportedReturnType",
|
|
142
|
+
"message": str(e),
|
|
143
|
+
}
|
|
95
144
|
except (ValueError, TypeError) as e:
|
|
96
145
|
return {
|
|
97
146
|
"status": "error",
|
|
@@ -221,6 +270,24 @@ class RequestHandler:
|
|
|
221
270
|
result = arr.astype(dtype)
|
|
222
271
|
return self._pack_result(result)
|
|
223
272
|
|
|
273
|
+
# Generator method calls: op is "Generator.<method>" with the remote
|
|
274
|
+
# generator handle as the first arg. Resolve it and call the method
|
|
275
|
+
# server-side, so the RNG state lives + advances on the server (the
|
|
276
|
+
# stream stays deterministic per seed and FLOP-counted).
|
|
277
|
+
if op.startswith("Generator."):
|
|
278
|
+
method = op[len("Generator.") :]
|
|
279
|
+
if method not in _ALLOWED_GEN_METHODS:
|
|
280
|
+
return {
|
|
281
|
+
"status": "error",
|
|
282
|
+
"error_type": "UnsupportedFunctionError",
|
|
283
|
+
"message": f"Generator.{method} is not supported by the flopscope server",
|
|
284
|
+
}
|
|
285
|
+
gen = self._resolve_arg(raw_args[0])
|
|
286
|
+
rest = [self._resolve_arg(a) for a in raw_args[1:]]
|
|
287
|
+
resolved_kwargs = {k: self._resolve_arg(v) for k, v in kwargs.items()}
|
|
288
|
+
result = getattr(gen, method)(*rest, **resolved_kwargs)
|
|
289
|
+
return self._pack_result(result)
|
|
290
|
+
|
|
224
291
|
func = _get_flopscope_func(op)
|
|
225
292
|
resolved_args = [self._resolve_arg(a) for a in raw_args]
|
|
226
293
|
resolved_kwargs = {k: self._resolve_arg(v) for k, v in kwargs.items()}
|
|
@@ -247,6 +314,13 @@ class RequestHandler:
|
|
|
247
314
|
if isinstance(handle, bytes):
|
|
248
315
|
handle = handle.decode("utf-8")
|
|
249
316
|
return self._session.get_array(handle)
|
|
317
|
+
gen_handle = arg.get("__gen__")
|
|
318
|
+
if gen_handle is None:
|
|
319
|
+
gen_handle = arg.get(b"__gen__")
|
|
320
|
+
if gen_handle is not None:
|
|
321
|
+
if isinstance(gen_handle, bytes):
|
|
322
|
+
gen_handle = gen_handle.decode("utf-8")
|
|
323
|
+
return self._session.get_generator(gen_handle)
|
|
250
324
|
# SymmetryGroup wire format
|
|
251
325
|
pg_data = arg.get("__symmetry_group__") or arg.get(b"__symmetry_group__")
|
|
252
326
|
if pg_data is not None:
|
|
@@ -370,16 +444,23 @@ class RequestHandler:
|
|
|
370
444
|
"result": {"value": str(result), "dtype": "str"},
|
|
371
445
|
"budget": budget,
|
|
372
446
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
447
|
+
if isinstance(result, np.random.Generator):
|
|
448
|
+
handle = self._session.store_generator(result)
|
|
449
|
+
return {"status": "ok", "result": {"gen_id": handle}, "budget": budget}
|
|
450
|
+
|
|
451
|
+
# Fallback: flatten nested numpy structures to JSON-safe values. If the
|
|
452
|
+
# result still isn't msgpack-native, fail loudly + attributably rather
|
|
453
|
+
# than silently str()-degrading (which previously surfaced downstream as
|
|
454
|
+
# an opaque "failed to serialize response" error). The registry-driven
|
|
455
|
+
# conformance test (tests/test_registry_conformance.py) catches any op
|
|
456
|
+
# whose return type lands here.
|
|
457
|
+
serializable = _make_serializable(result)
|
|
458
|
+
if _is_msgpack_native(serializable):
|
|
376
459
|
return {"status": "ok", "result": {"value": serializable}, "budget": budget}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
"budget": budget,
|
|
382
|
-
}
|
|
460
|
+
raise flops.UnsupportedReturnType(
|
|
461
|
+
f"{type(result).__name__} is not serializable across the "
|
|
462
|
+
f"client/server boundary"
|
|
463
|
+
)
|
|
383
464
|
|
|
384
465
|
|
|
385
466
|
# ---------------------------------------------------------------------------
|
|
@@ -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
|