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.
Files changed (23) hide show
  1. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/PKG-INFO +2 -2
  2. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/pyproject.toml +2 -2
  3. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/__init__.py +1 -1
  4. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_protocol.py +4 -0
  5. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_request_handler.py +50 -0
  6. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_session.py +26 -0
  7. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/.gitignore +0 -0
  8. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/README.md +0 -0
  9. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/__main__.py +0 -0
  10. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_array_store.py +0 -0
  11. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_comms_tracker.py +0 -0
  12. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/src/flopscope_server/_server.py +0 -0
  13. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_array_store.py +0 -0
  14. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_bugfixes_round2.py +0 -0
  15. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_bugfixes_round3.py +0 -0
  16. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_comms_tracker.py +0 -0
  17. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_new_types.py +0 -0
  18. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_protocol.py +0 -0
  19. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_request_handler.py +0 -0
  20. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_server.py +0 -0
  21. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_session.py +0 -0
  22. {flopscope_server-0.4.0 → flopscope_server-0.4.2}/tests/test_version_handshake.py +0 -0
  23. {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.0
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.0
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.0"
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.0",
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",
@@ -1,3 +1,3 @@
1
1
  """Flopscope backend server — executes numpy operations on behalf of remote clients."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.2"
@@ -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 {