flopscope-server 0.4.3__tar.gz → 0.6.0__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.3 → flopscope_server-0.6.0}/PKG-INFO +2 -2
  2. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/pyproject.toml +2 -2
  3. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/__init__.py +1 -1
  4. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/_request_handler.py +20 -4
  5. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/_server.py +2 -2
  6. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_server.py +41 -0
  7. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_session.py +18 -0
  8. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/.gitignore +0 -0
  9. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/README.md +0 -0
  10. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/__main__.py +0 -0
  11. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/_array_store.py +0 -0
  12. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/_comms_tracker.py +0 -0
  13. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/_protocol.py +0 -0
  14. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/src/flopscope_server/_session.py +0 -0
  15. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_array_store.py +0 -0
  16. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_bugfixes_round2.py +0 -0
  17. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_bugfixes_round3.py +0 -0
  18. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_comms_tracker.py +0 -0
  19. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_new_types.py +0 -0
  20. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_protocol.py +0 -0
  21. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_request_handler.py +0 -0
  22. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/tests/test_version_handshake.py +0 -0
  23. {flopscope_server-0.4.3 → flopscope_server-0.6.0}/uv.lock +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flopscope-server
3
- Version: 0.4.3
3
+ Version: 0.6.0
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.3
7
+ Requires-Dist: flopscope==0.6.0
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.3"
7
+ version = "0.6.0"
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.3",
13
+ "flopscope==0.6.0",
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.3"
3
+ __version__ = "0.6.0"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import re
7
+ from time import perf_counter_ns
7
8
  from typing import Any
8
9
 
9
10
  import numpy as np
@@ -86,6 +87,20 @@ class RequestHandler:
86
87
 
87
88
  def __init__(self, session: Session) -> None:
88
89
  self._session = session
90
+ self._kernel_ns: int = 0
91
+
92
+ @property
93
+ def kernel_ns(self) -> int:
94
+ """Pure numpy-kernel nanoseconds accumulated during the last handle()."""
95
+ return self._kernel_ns
96
+
97
+ def _run_kernel(self, fn, *args, **kwargs):
98
+ """Invoke a numpy compute call, attributing only its wall to kernel time."""
99
+ t0 = perf_counter_ns()
100
+ try:
101
+ return fn(*args, **kwargs)
102
+ finally:
103
+ self._kernel_ns += perf_counter_ns() - t0
89
104
 
90
105
  # ------------------------------------------------------------------
91
106
  # Public entry point
@@ -96,6 +111,7 @@ class RequestHandler:
96
111
 
97
112
  The ``request["op"]`` field determines which handler is invoked.
98
113
  """
114
+ self._kernel_ns = 0
99
115
  try:
100
116
  op = request["op"]
101
117
 
@@ -249,7 +265,7 @@ class RequestHandler:
249
265
  raise ValueError("__getitem__ requires [handle, key]")
250
266
  arr = self._resolve_arg(args[0])
251
267
  key = self._decode_index_key(args[1])
252
- result = arr[key]
268
+ result = self._run_kernel(lambda: arr[key])
253
269
  return self._pack_result(result)
254
270
 
255
271
  # ------------------------------------------------------------------
@@ -267,7 +283,7 @@ class RequestHandler:
267
283
  dtype = raw_args[1] if len(raw_args) > 1 else kwargs.get("dtype")
268
284
  if isinstance(dtype, bytes):
269
285
  dtype = dtype.decode("utf-8")
270
- result = arr.astype(dtype)
286
+ result = self._run_kernel(arr.astype, dtype)
271
287
  return self._pack_result(result)
272
288
 
273
289
  # Generator method calls: op is "Generator.<method>" with the remote
@@ -285,14 +301,14 @@ class RequestHandler:
285
301
  gen = self._resolve_arg(raw_args[0])
286
302
  rest = [self._resolve_arg(a) for a in raw_args[1:]]
287
303
  resolved_kwargs = {k: self._resolve_arg(v) for k, v in kwargs.items()}
288
- result = getattr(gen, method)(*rest, **resolved_kwargs)
304
+ result = self._run_kernel(getattr(gen, method), *rest, **resolved_kwargs)
289
305
  return self._pack_result(result)
290
306
 
291
307
  func = _get_flopscope_func(op)
292
308
  resolved_args = [self._resolve_arg(a) for a in raw_args]
293
309
  resolved_kwargs = {k: self._resolve_arg(v) for k, v in kwargs.items()}
294
310
 
295
- result = func(*resolved_args, **resolved_kwargs)
311
+ result = self._run_kernel(func, *resolved_args, **resolved_kwargs)
296
312
 
297
313
  return self._pack_result(result)
298
314
 
@@ -172,7 +172,7 @@ class FlopscopeServer:
172
172
 
173
173
  # --- Record comms overhead ---
174
174
  comms_ns = (t1 - t0) + (t3 - t2)
175
- compute_ns = t2 - t1
175
+ compute_ns = self._handler.kernel_ns
176
176
 
177
177
  self._session.comms_tracker.record_request(
178
178
  bytes_received=len(raw),
@@ -222,7 +222,7 @@ class FlopscopeServer:
222
222
  t3 = perf_counter_ns()
223
223
 
224
224
  comms_ns = (t1 - t0) + (t3 - t2)
225
- compute_ns = t2 - t1
225
+ compute_ns = 0 # session creation is not a numpy kernel
226
226
  self._session.comms_tracker.record_request(
227
227
  bytes_received=0,
228
228
  bytes_sent=len(response_bytes),
@@ -308,3 +308,44 @@ def test_normalize_msg_kwargs_handle_dict():
308
308
  assert isinstance(out_val, dict)
309
309
  assert "__handle__" in out_val
310
310
  assert out_val["__handle__"] == "a5"
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # kernel_ns timing tests (pure numpy kernel attribution)
315
+ # ---------------------------------------------------------------------------
316
+
317
+
318
+ def test_compute_time_is_kernel_only():
319
+ """A compute op records kernel time that does not exceed the full handle() wall."""
320
+ import time
321
+
322
+ import numpy as np
323
+ from flopscope_server._request_handler import RequestHandler
324
+ from flopscope_server._session import Session
325
+
326
+ session = Session(flop_budget=10**12)
327
+ handler = RequestHandler(session)
328
+ h = session.store_array(np.ones((256, 256)))
329
+
330
+ t0 = time.perf_counter_ns()
331
+ handler.handle({"op": "dot", "args": [h, h], "kwargs": None})
332
+ handle_ns = time.perf_counter_ns() - t0
333
+
334
+ assert handler.kernel_ns > 0
335
+ assert handler.kernel_ns <= handle_ns
336
+ session.close()
337
+
338
+
339
+ def test_fetch_contributes_no_kernel():
340
+ """A fetch op is data movement, not a numpy kernel — kernel_ns stays 0."""
341
+ import numpy as np
342
+ from flopscope_server._request_handler import RequestHandler
343
+ from flopscope_server._session import Session
344
+
345
+ session = Session(flop_budget=10**12)
346
+ handler = RequestHandler(session)
347
+ h = session.store_array(np.ones((8, 8)))
348
+
349
+ handler.handle({"op": "fetch", "id": h})
350
+ assert handler.kernel_ns == 0
351
+ session.close()
@@ -253,3 +253,21 @@ def test_budget_remaining_reflects_context():
253
253
  s = Session(flop_budget=100_000)
254
254
  assert s.budget_remaining == 100_000
255
255
  s.close()
256
+
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # Server contract: close() surfaces recorded compute time
260
+ # ---------------------------------------------------------------------------
261
+
262
+
263
+ def test_close_surfaces_recorded_compute_time(session):
264
+ """close()'s comms_summary carries the compute time the client reads as backend."""
265
+ session.comms_tracker.record_request(
266
+ bytes_received=10,
267
+ bytes_sent=20,
268
+ comms_overhead_ns=100,
269
+ compute_time_ns=5000,
270
+ is_fetch=False,
271
+ )
272
+ summary = session.close()
273
+ assert summary["comms_summary"]["total_compute_time_ns"] == 5000