flopscope-server 0.3.0__py3-none-any.whl
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/__init__.py +3 -0
- flopscope_server/__main__.py +42 -0
- flopscope_server/_array_store.py +97 -0
- flopscope_server/_comms_tracker.py +77 -0
- flopscope_server/_protocol.py +233 -0
- flopscope_server/_request_handler.py +440 -0
- flopscope_server/_server.py +375 -0
- flopscope_server/_session.py +171 -0
- flopscope_server-0.3.0.dist-info/METADATA +9 -0
- flopscope_server-0.3.0.dist-info/RECORD +12 -0
- flopscope_server-0.3.0.dist-info/WHEEL +4 -0
- flopscope_server-0.3.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""FlopscopeServer -- ZMQ REP loop that dispatches requests to a Session + RequestHandler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from time import monotonic, perf_counter_ns
|
|
7
|
+
|
|
8
|
+
import msgpack
|
|
9
|
+
import zmq
|
|
10
|
+
|
|
11
|
+
import flopscope
|
|
12
|
+
from flopscope_server._protocol import (
|
|
13
|
+
InvalidRequestError,
|
|
14
|
+
decode_request,
|
|
15
|
+
encode_error_response,
|
|
16
|
+
encode_response,
|
|
17
|
+
validate_request,
|
|
18
|
+
)
|
|
19
|
+
from flopscope_server._request_handler import RequestHandler
|
|
20
|
+
from flopscope_server._session import Session
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FlopscopeServer:
|
|
24
|
+
"""ZMQ REP server for the flopscope budget-controlled compute service.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
url:
|
|
29
|
+
ZMQ endpoint to bind (e.g. ``"ipc:///tmp/flopscope.sock"`` or
|
|
30
|
+
``"tcp://127.0.0.1:15555"``).
|
|
31
|
+
session_timeout_s:
|
|
32
|
+
Seconds of inactivity after which an open session is reaped.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
url: str = "ipc:///tmp/flopscope.sock",
|
|
38
|
+
session_timeout_s: float = 60.0,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._url = url
|
|
41
|
+
self._session_timeout_s = session_timeout_s
|
|
42
|
+
self._running = False
|
|
43
|
+
|
|
44
|
+
# Session state (at most one session at a time)
|
|
45
|
+
self._session: Session | None = None
|
|
46
|
+
self._handler: RequestHandler | None = None
|
|
47
|
+
self._last_activity: float = 0.0
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
# Public API
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def run(self) -> None:
|
|
54
|
+
"""Start the blocking ZMQ REP loop.
|
|
55
|
+
|
|
56
|
+
Creates a ZMQ Context and REP socket, binds to ``self._url``, and
|
|
57
|
+
loops until :meth:`stop` is called.
|
|
58
|
+
"""
|
|
59
|
+
ctx = zmq.Context()
|
|
60
|
+
sock = ctx.socket(zmq.REP)
|
|
61
|
+
sock.setsockopt(zmq.RCVTIMEO, 1000) # 1 s poll interval
|
|
62
|
+
sock.bind(self._url)
|
|
63
|
+
self._running = True
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
while self._running:
|
|
67
|
+
try:
|
|
68
|
+
raw = sock.recv()
|
|
69
|
+
except zmq.Again:
|
|
70
|
+
self._reap_session_if_stale()
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
response = self._process_request(raw)
|
|
74
|
+
sock.send(response)
|
|
75
|
+
finally:
|
|
76
|
+
sock.close(linger=0)
|
|
77
|
+
ctx.term()
|
|
78
|
+
|
|
79
|
+
def stop(self) -> None:
|
|
80
|
+
"""Signal the run loop to exit."""
|
|
81
|
+
self._running = False
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Request processing
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _process_request(self, raw: bytes) -> bytes:
|
|
88
|
+
"""Decode, dispatch, encode a single request.
|
|
89
|
+
|
|
90
|
+
Returns msgpack bytes ready to send back to the client.
|
|
91
|
+
"""
|
|
92
|
+
t0 = perf_counter_ns()
|
|
93
|
+
|
|
94
|
+
# --- Decode ---
|
|
95
|
+
try:
|
|
96
|
+
msg = decode_request(raw)
|
|
97
|
+
except InvalidRequestError as exc:
|
|
98
|
+
return encode_error_response("InvalidRequestError", str(exc))
|
|
99
|
+
|
|
100
|
+
# Normalise remaining bytes→str fields that decode_request leaves as
|
|
101
|
+
# raw bytes (it only converts _STRING_FIELDS: op, dtype, request_id).
|
|
102
|
+
_normalize_msg(msg)
|
|
103
|
+
|
|
104
|
+
t1 = perf_counter_ns()
|
|
105
|
+
op = msg["op"]
|
|
106
|
+
|
|
107
|
+
# --- Pre-session ops (no active session required) ---
|
|
108
|
+
if op == "hello":
|
|
109
|
+
return self._handle_hello(msg)
|
|
110
|
+
|
|
111
|
+
# --- Session lifecycle ops ---
|
|
112
|
+
if op == "budget_open":
|
|
113
|
+
return self._handle_budget_open(msg, t0, t1)
|
|
114
|
+
|
|
115
|
+
if op == "budget_close":
|
|
116
|
+
return self._handle_budget_close(t0, t1)
|
|
117
|
+
|
|
118
|
+
# --- Require active session for everything else ---
|
|
119
|
+
if self._session is None:
|
|
120
|
+
return encode_error_response(
|
|
121
|
+
"NoBudgetContextError",
|
|
122
|
+
"no active session -- send budget_open first",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# --- Validate (whitelist check) ---
|
|
126
|
+
try:
|
|
127
|
+
validate_request(msg)
|
|
128
|
+
except InvalidRequestError as exc:
|
|
129
|
+
return encode_error_response("InvalidRequestError", str(exc))
|
|
130
|
+
|
|
131
|
+
# --- Dispatch ---
|
|
132
|
+
assert self._handler is not None
|
|
133
|
+
result = self._handler.handle(msg)
|
|
134
|
+
|
|
135
|
+
t2 = perf_counter_ns()
|
|
136
|
+
|
|
137
|
+
# --- Encode response ---
|
|
138
|
+
is_fetch = op in ("fetch", "fetch_slice")
|
|
139
|
+
response_bytes: bytes
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if result.get("status") == "error":
|
|
143
|
+
response_bytes = encode_error_response(
|
|
144
|
+
result["error_type"], result["message"]
|
|
145
|
+
)
|
|
146
|
+
elif "data" in result:
|
|
147
|
+
# fetch / fetch_slice -- use raw response packing
|
|
148
|
+
payload = {
|
|
149
|
+
"status": "ok",
|
|
150
|
+
"data": result["data"],
|
|
151
|
+
"shape": result["shape"],
|
|
152
|
+
"dtype": result["dtype"],
|
|
153
|
+
"comms_overhead_ns": 0, # placeholder, updated below
|
|
154
|
+
}
|
|
155
|
+
response_bytes = msgpack.packb(payload, use_bin_type=True)
|
|
156
|
+
else:
|
|
157
|
+
response_bytes = encode_response(
|
|
158
|
+
result.get("result"),
|
|
159
|
+
result.get(
|
|
160
|
+
"budget", self._session.budget_remaining if self._session else 0
|
|
161
|
+
),
|
|
162
|
+
comms_overhead_ns=0, # placeholder
|
|
163
|
+
)
|
|
164
|
+
except Exception as enc_err:
|
|
165
|
+
# Response serialization failed -- return error instead of crashing
|
|
166
|
+
response_bytes = encode_error_response(
|
|
167
|
+
"FlopscopeServerError",
|
|
168
|
+
f"failed to serialize response: {type(enc_err).__name__}",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
t3 = perf_counter_ns()
|
|
172
|
+
|
|
173
|
+
# --- Record comms overhead ---
|
|
174
|
+
comms_ns = (t1 - t0) + (t3 - t2)
|
|
175
|
+
compute_ns = t2 - t1
|
|
176
|
+
|
|
177
|
+
self._session.comms_tracker.record_request(
|
|
178
|
+
bytes_received=len(raw),
|
|
179
|
+
bytes_sent=len(response_bytes),
|
|
180
|
+
comms_overhead_ns=comms_ns,
|
|
181
|
+
compute_time_ns=compute_ns,
|
|
182
|
+
is_fetch=is_fetch,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
self._last_activity = monotonic()
|
|
186
|
+
return response_bytes
|
|
187
|
+
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
# Budget lifecycle handlers
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _handle_budget_open(self, msg: dict, t0: int, t1: int) -> bytes:
|
|
193
|
+
"""Open a new session; error if one is already open."""
|
|
194
|
+
if self._session is not None and self._session.is_open:
|
|
195
|
+
return encode_error_response(
|
|
196
|
+
"RuntimeError",
|
|
197
|
+
"session already open -- send budget_close first",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Support both top-level and kwargs-based flop_budget
|
|
201
|
+
flop_budget = msg.get("flop_budget")
|
|
202
|
+
flop_multiplier = msg.get("flop_multiplier")
|
|
203
|
+
if flop_budget is None:
|
|
204
|
+
kwargs = msg.get("kwargs") or {}
|
|
205
|
+
flop_budget = kwargs.get("flop_budget", 1_000_000)
|
|
206
|
+
if flop_multiplier is None:
|
|
207
|
+
flop_multiplier = kwargs.get("flop_multiplier", 1.0)
|
|
208
|
+
if flop_multiplier is None:
|
|
209
|
+
flop_multiplier = 1.0
|
|
210
|
+
self._session = Session(
|
|
211
|
+
flop_budget=flop_budget, flop_multiplier=flop_multiplier
|
|
212
|
+
)
|
|
213
|
+
self._handler = RequestHandler(self._session)
|
|
214
|
+
self._last_activity = monotonic()
|
|
215
|
+
|
|
216
|
+
t2 = perf_counter_ns()
|
|
217
|
+
response_bytes = encode_response(
|
|
218
|
+
{"session": "opened", "flop_budget": flop_budget},
|
|
219
|
+
budget=flop_budget,
|
|
220
|
+
comms_overhead_ns=0,
|
|
221
|
+
)
|
|
222
|
+
t3 = perf_counter_ns()
|
|
223
|
+
|
|
224
|
+
comms_ns = (t1 - t0) + (t3 - t2)
|
|
225
|
+
compute_ns = t2 - t1
|
|
226
|
+
self._session.comms_tracker.record_request(
|
|
227
|
+
bytes_received=0,
|
|
228
|
+
bytes_sent=len(response_bytes),
|
|
229
|
+
comms_overhead_ns=comms_ns,
|
|
230
|
+
compute_time_ns=compute_ns,
|
|
231
|
+
is_fetch=False,
|
|
232
|
+
)
|
|
233
|
+
return response_bytes
|
|
234
|
+
|
|
235
|
+
def _handle_budget_close(self, t0: int, t1: int) -> bytes:
|
|
236
|
+
"""Close the active session and return a summary."""
|
|
237
|
+
if self._session is None or not self._session.is_open:
|
|
238
|
+
return encode_error_response(
|
|
239
|
+
"NoBudgetContextError",
|
|
240
|
+
"no active session to close",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
summary = self._session.close()
|
|
244
|
+
self._session = None
|
|
245
|
+
self._handler = None
|
|
246
|
+
|
|
247
|
+
t2 = perf_counter_ns()
|
|
248
|
+
response_bytes = encode_response(summary, budget=0, comms_overhead_ns=0)
|
|
249
|
+
t3 = perf_counter_ns()
|
|
250
|
+
# No session to record to (already closed); that's fine.
|
|
251
|
+
return response_bytes
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
# Handshake
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def _handle_hello(self, msg: dict) -> bytes:
|
|
258
|
+
"""Compare client and server flopscope versions.
|
|
259
|
+
|
|
260
|
+
Returns an ok response carrying the server's leading X.Y.Z when
|
|
261
|
+
the client reports a matching version, or a ``VersionMismatch``
|
|
262
|
+
error otherwise. The handshake is intentionally session-free so
|
|
263
|
+
clients can detect mismatch before opening a budget.
|
|
264
|
+
"""
|
|
265
|
+
kwargs = msg.get("kwargs") or {}
|
|
266
|
+
client_version = (
|
|
267
|
+
kwargs.get("client_version") if isinstance(kwargs, dict) else None
|
|
268
|
+
)
|
|
269
|
+
server_xyz = flopscope.__version__.split("+", 1)[0]
|
|
270
|
+
|
|
271
|
+
if not isinstance(client_version, str) or not client_version:
|
|
272
|
+
return encode_error_response(
|
|
273
|
+
"VersionMismatch",
|
|
274
|
+
f"client did not report a flopscope version; server is {server_xyz}",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if client_version != server_xyz:
|
|
278
|
+
return encode_error_response(
|
|
279
|
+
"VersionMismatch",
|
|
280
|
+
(
|
|
281
|
+
f"flopscope-client {client_version} cannot talk to "
|
|
282
|
+
f"flopscope-server {server_xyz}: versions must match. "
|
|
283
|
+
"Install matched versions: see "
|
|
284
|
+
"https://pypi.org/project/flopscope/ for the latest."
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return msgpack.packb(
|
|
289
|
+
{"status": "ok", "server_version": server_xyz},
|
|
290
|
+
use_bin_type=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
# Session reaping
|
|
295
|
+
# ------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def _reap_session_if_stale(self) -> None:
|
|
298
|
+
"""Close and discard the session if it has been idle too long."""
|
|
299
|
+
if self._session is None or not self._session.is_open:
|
|
300
|
+
return
|
|
301
|
+
if monotonic() - self._last_activity > self._session_timeout_s:
|
|
302
|
+
print(
|
|
303
|
+
"[flopscope-server] session timed out -- reaping",
|
|
304
|
+
file=sys.stderr,
|
|
305
|
+
)
|
|
306
|
+
self._session.close()
|
|
307
|
+
self._session = None
|
|
308
|
+
self._handler = None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
# Message normalisation helper
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _decode_if_bytes(v: object) -> object:
|
|
317
|
+
"""Decode bytes to str if possible, otherwise return as-is."""
|
|
318
|
+
if isinstance(v, bytes):
|
|
319
|
+
try:
|
|
320
|
+
return v.decode("utf-8")
|
|
321
|
+
except UnicodeDecodeError:
|
|
322
|
+
return v
|
|
323
|
+
return v
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _normalize_arg(a: object) -> object:
|
|
327
|
+
"""Normalize a single arg: decode short bytes to str, normalize dict keys/values.
|
|
328
|
+
|
|
329
|
+
Binary data payloads (array bytes) must stay as bytes. Heuristic: only
|
|
330
|
+
decode if short AND contains no null bytes (handles, dtype strings, etc.
|
|
331
|
+
are always short ASCII).
|
|
332
|
+
"""
|
|
333
|
+
if isinstance(a, bytes):
|
|
334
|
+
# Only decode bytes that are short AND contain only ASCII printable
|
|
335
|
+
# characters (no bytes > 127). This catches handle IDs and dtype
|
|
336
|
+
# strings but never touches binary array data (which often contains
|
|
337
|
+
# high bytes even if it happens to be valid UTF-8).
|
|
338
|
+
if len(a) > 0 and len(a) <= 32 and all(32 <= b < 128 for b in a):
|
|
339
|
+
return a.decode("ascii")
|
|
340
|
+
return a # keep as raw bytes (likely binary payload)
|
|
341
|
+
if isinstance(a, dict):
|
|
342
|
+
return {_decode_if_bytes(k): _normalize_arg(v) for k, v in a.items()}
|
|
343
|
+
if isinstance(a, list):
|
|
344
|
+
return [_normalize_arg(x) for x in a]
|
|
345
|
+
return a
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _normalize_msg(msg: dict) -> None:
|
|
349
|
+
"""In-place normalise a decoded request dict.
|
|
350
|
+
|
|
351
|
+
``decode_request`` only converts a small set of known string fields
|
|
352
|
+
(op, dtype, request_id) from bytes to str. Other string-valued
|
|
353
|
+
fields -- ``id``, ``ids``, and items inside ``args`` -- may still be
|
|
354
|
+
bytes after msgpack decoding with ``raw=True``. This helper converts
|
|
355
|
+
them so the downstream RequestHandler receives plain strings.
|
|
356
|
+
"""
|
|
357
|
+
# id (used by fetch, fetch_slice, free)
|
|
358
|
+
if "id" in msg:
|
|
359
|
+
msg["id"] = _decode_if_bytes(msg["id"])
|
|
360
|
+
|
|
361
|
+
# ids (used by free)
|
|
362
|
+
if "ids" in msg and isinstance(msg["ids"], list):
|
|
363
|
+
msg["ids"] = [_decode_if_bytes(x) for x in msg["ids"]]
|
|
364
|
+
|
|
365
|
+
# args -- handle IDs are short ASCII strings, may also contain dicts
|
|
366
|
+
if "args" in msg and isinstance(msg["args"], list):
|
|
367
|
+
msg["args"] = [_normalize_arg(a) for a in msg["args"]]
|
|
368
|
+
|
|
369
|
+
# kwargs keys are already str (decode_request normalises all keys).
|
|
370
|
+
# kwargs *values* may contain handles, dicts, or lists that need full
|
|
371
|
+
# normalization (not just _decode_if_bytes).
|
|
372
|
+
if "kwargs" in msg and isinstance(msg["kwargs"], dict):
|
|
373
|
+
msg["kwargs"] = {
|
|
374
|
+
_decode_if_bytes(k): _normalize_arg(v) for k, v in msg["kwargs"].items()
|
|
375
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Session — ties together ArrayStore, BudgetContext, and CommsTracker for a single participant session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
import flopscope as flops
|
|
10
|
+
from flopscope_server._array_store import ArrayStore
|
|
11
|
+
from flopscope_server._comms_tracker import CommsTracker
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Session:
|
|
15
|
+
"""A single participant session combining ArrayStore, BudgetContext, and CommsTracker.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
flop_budget : int
|
|
20
|
+
Maximum number of FLOPs allowed for this session.
|
|
21
|
+
flop_multiplier : float
|
|
22
|
+
Multiplier applied to each operation's raw FLOP cost.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, flop_budget: int, flop_multiplier: float = 1.0) -> None:
|
|
26
|
+
self._store = ArrayStore()
|
|
27
|
+
self._comms_tracker = CommsTracker()
|
|
28
|
+
self._budget_ctx = flops.BudgetContext(
|
|
29
|
+
flop_budget=flop_budget,
|
|
30
|
+
flop_multiplier=flop_multiplier,
|
|
31
|
+
quiet=True,
|
|
32
|
+
)
|
|
33
|
+
self._budget_ctx.__enter__()
|
|
34
|
+
self._is_open = True
|
|
35
|
+
|
|
36
|
+
# ------------------------------------------------------------------
|
|
37
|
+
# Properties
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_open(self) -> bool:
|
|
42
|
+
"""True if this session is still active (not yet closed)."""
|
|
43
|
+
return self._is_open
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def budget_remaining(self) -> int:
|
|
47
|
+
"""FLOPs remaining in the current budget."""
|
|
48
|
+
return self._budget_ctx.flops_remaining
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def budget_context(self) -> flops.BudgetContext:
|
|
52
|
+
"""The active BudgetContext for this session.
|
|
53
|
+
|
|
54
|
+
Raises
|
|
55
|
+
------
|
|
56
|
+
RuntimeError
|
|
57
|
+
If the session is already closed.
|
|
58
|
+
"""
|
|
59
|
+
if not self._is_open:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
"Session is closed; BudgetContext is no longer available."
|
|
62
|
+
)
|
|
63
|
+
return self._budget_ctx
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def comms_tracker(self) -> CommsTracker:
|
|
67
|
+
"""The CommsTracker for this session."""
|
|
68
|
+
return self._comms_tracker
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# Array operations (delegate to ArrayStore)
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def store_array(self, arr: Any) -> str:
|
|
75
|
+
"""Store *arr* and return its handle ID.
|
|
76
|
+
|
|
77
|
+
Delegates to :meth:`ArrayStore.put`.
|
|
78
|
+
"""
|
|
79
|
+
return self._store.put(arr)
|
|
80
|
+
|
|
81
|
+
def get_array(self, handle: str) -> Any:
|
|
82
|
+
"""Return the array for *handle*.
|
|
83
|
+
|
|
84
|
+
Delegates to :meth:`ArrayStore.get`.
|
|
85
|
+
|
|
86
|
+
Raises
|
|
87
|
+
------
|
|
88
|
+
KeyError
|
|
89
|
+
If *handle* is not in the store.
|
|
90
|
+
"""
|
|
91
|
+
return self._store.get(handle)
|
|
92
|
+
|
|
93
|
+
def array_metadata(self, handle: str) -> dict:
|
|
94
|
+
"""Return metadata dict for *handle*.
|
|
95
|
+
|
|
96
|
+
Delegates to :meth:`ArrayStore.metadata`.
|
|
97
|
+
|
|
98
|
+
Raises
|
|
99
|
+
------
|
|
100
|
+
KeyError
|
|
101
|
+
If *handle* is not in the store.
|
|
102
|
+
"""
|
|
103
|
+
return self._store.metadata(handle)
|
|
104
|
+
|
|
105
|
+
def free_arrays(self, handles: list) -> None:
|
|
106
|
+
"""Remove arrays by handle; silently ignore unknown handles.
|
|
107
|
+
|
|
108
|
+
Delegates to :meth:`ArrayStore.free`.
|
|
109
|
+
"""
|
|
110
|
+
self._store.free(handles)
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Budget
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def budget_status(self) -> dict:
|
|
117
|
+
"""Return the current FLOP budget status.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
dict with keys:
|
|
122
|
+
flop_budget: total budget
|
|
123
|
+
flops_used: FLOPs consumed so far
|
|
124
|
+
flops_remaining: budget minus used
|
|
125
|
+
"""
|
|
126
|
+
return {
|
|
127
|
+
"flop_budget": self._budget_ctx.flop_budget,
|
|
128
|
+
"flops_used": self._budget_ctx.flops_used,
|
|
129
|
+
"flops_remaining": self._budget_ctx.flops_remaining,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Session lifecycle
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def close(self) -> dict:
|
|
137
|
+
"""Close the session, exiting the BudgetContext and clearing the ArrayStore.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
dict with keys:
|
|
142
|
+
budget_summary: str — human-readable FLOP budget summary,
|
|
143
|
+
including a namespace section when labeled ops were recorded
|
|
144
|
+
budget_breakdown: dict — machine-readable summary data with
|
|
145
|
+
``by_namespace`` buckets for direct ingestion
|
|
146
|
+
comms_summary: dict — CommsTracker summary
|
|
147
|
+
|
|
148
|
+
Raises
|
|
149
|
+
------
|
|
150
|
+
RuntimeError
|
|
151
|
+
If the session is already closed.
|
|
152
|
+
"""
|
|
153
|
+
if not self._is_open:
|
|
154
|
+
raise RuntimeError("Session is already closed.")
|
|
155
|
+
|
|
156
|
+
self._budget_ctx.__exit__(None, None, None)
|
|
157
|
+
budget_breakdown = self._budget_ctx.summary_dict(by_namespace=True)
|
|
158
|
+
show_namespaces = any(
|
|
159
|
+
namespace is not None
|
|
160
|
+
for namespace in budget_breakdown.get("by_namespace", {})
|
|
161
|
+
)
|
|
162
|
+
budget_summary = self._budget_ctx.summary(by_namespace=show_namespaces)
|
|
163
|
+
comms_summary = self._comms_tracker.summary()
|
|
164
|
+
self._store.clear()
|
|
165
|
+
self._is_open = False
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"budget_summary": budget_summary,
|
|
169
|
+
"budget_breakdown": budget_breakdown,
|
|
170
|
+
"comms_summary": comms_summary,
|
|
171
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flopscope-server
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Backend server for flopscope client-server architecture
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: flopscope==0.3.0
|
|
7
|
+
Requires-Dist: msgpack>=1.0.0
|
|
8
|
+
Requires-Dist: numpy<2.5.0,>=2.0.0
|
|
9
|
+
Requires-Dist: pyzmq>=26.0.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
flopscope_server/__init__.py,sha256=AVnTf0x1li_DqBv19AszabChlmxAN-bqCQqJx7H5uIY,113
|
|
2
|
+
flopscope_server/__main__.py,sha256=3dhGzDNLgrni3LsTjYvA5aeNz_Ny2bVXpZ2Wrj-ABr4,1056
|
|
3
|
+
flopscope_server/_array_store.py,sha256=iIoDuBQcdnk3UPUdKBee8mczOWhbo5yuO77k4fPzrj4,2947
|
|
4
|
+
flopscope_server/_comms_tracker.py,sha256=T8HC00xbMbFOhQ_gK9mLyCCJhJJpZMTbPUSmKfgT4uc,2833
|
|
5
|
+
flopscope_server/_protocol.py,sha256=jiwx-lJd9xik0HBR72h_vuUPWkWmgs9m-4xp4SBuL6g,6588
|
|
6
|
+
flopscope_server/_request_handler.py,sha256=UXBwrxvB1zM3hfCGDk4nlaXALA8Rs8Jm6nsvDK5RE0M,17025
|
|
7
|
+
flopscope_server/_server.py,sha256=WZ9bYiJjYFavyaHLfYG9h1BQrFHJTbDDxK46u6RcQME,13466
|
|
8
|
+
flopscope_server/_session.py,sha256=23wf19c9uJs_Kiq9mgBrdHR2W48_3jpWLoQE5pa7YJE,5401
|
|
9
|
+
flopscope_server-0.3.0.dist-info/METADATA,sha256=zya9tXmhH1ie2HlzQrh_KBA3uJbqPTXUpNFwaNmR4uM,275
|
|
10
|
+
flopscope_server-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
flopscope_server-0.3.0.dist-info/entry_points.txt,sha256=_fVtS1QpIrRT22oEsh9PEuucsyUfH8v2FHctWdYAzU4,68
|
|
12
|
+
flopscope_server-0.3.0.dist-info/RECORD,,
|