shared-tensor 0.1.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.
- shared_tensor/__init__.py +27 -0
- shared_tensor/async_client.py +302 -0
- shared_tensor/async_provider.py +173 -0
- shared_tensor/async_task.py +361 -0
- shared_tensor/client.py +265 -0
- shared_tensor/errors.py +16 -0
- shared_tensor/jsonrpc.py +163 -0
- shared_tensor/provider.py +155 -0
- shared_tensor/server.py +458 -0
- shared_tensor/utils.py +122 -0
- shared_tensor-0.1.0.dist-info/METADATA +420 -0
- shared_tensor-0.1.0.dist-info/RECORD +16 -0
- shared_tensor-0.1.0.dist-info/WHEEL +5 -0
- shared_tensor-0.1.0.dist-info/entry_points.txt +2 -0
- shared_tensor-0.1.0.dist-info/licenses/LICENSE +181 -0
- shared_tensor-0.1.0.dist-info/top_level.txt +1 -0
shared_tensor/server.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared Tensor JSON-RPC Server
|
|
3
|
+
|
|
4
|
+
Handles remote function execution requests using JSON-RPC 2.0 protocol.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import pickle
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
15
|
+
from socketserver import ThreadingMixIn
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
|
|
20
|
+
from shared_tensor.jsonrpc import (
|
|
21
|
+
JsonRpcErrorCodes,
|
|
22
|
+
parse_request,
|
|
23
|
+
create_success_response,
|
|
24
|
+
create_error_response
|
|
25
|
+
)
|
|
26
|
+
from shared_tensor.utils import import_function_from_path, serialize_result, deserialize_args, format_cache_key
|
|
27
|
+
from shared_tensor.async_task import get_task_manager, TaskStatus
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SharedTensorRequestHandler(BaseHTTPRequestHandler):
|
|
33
|
+
"""HTTP request handler for JSON-RPC requests"""
|
|
34
|
+
|
|
35
|
+
def log_message(self, format, *args):
|
|
36
|
+
"""Override to use our logger"""
|
|
37
|
+
logger.info(format % args)
|
|
38
|
+
|
|
39
|
+
def do_POST(self):
|
|
40
|
+
"""Handle POST requests"""
|
|
41
|
+
# Only accept requests to /jsonrpc endpoint
|
|
42
|
+
if self.path != '/jsonrpc':
|
|
43
|
+
self.send_error(404, "Not Found")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Check content type
|
|
47
|
+
content_type = self.headers.get('Content-Type', '')
|
|
48
|
+
if not content_type.startswith('application/json'):
|
|
49
|
+
self.send_error(400, "Content-Type must be application/json")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Read request body
|
|
53
|
+
try:
|
|
54
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
55
|
+
request_data = self.rfile.read(content_length).decode('utf-8')
|
|
56
|
+
except (ValueError, UnicodeDecodeError) as e:
|
|
57
|
+
self.send_error(400, f"Invalid request body: {e}")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Process JSON-RPC request
|
|
61
|
+
response = self.server.process_jsonrpc_request(request_data)
|
|
62
|
+
|
|
63
|
+
# Send response
|
|
64
|
+
self.send_response(200)
|
|
65
|
+
self.send_header('Content-Type', 'application/json')
|
|
66
|
+
self.send_header('Content-Length', str(len(response)))
|
|
67
|
+
self.end_headers()
|
|
68
|
+
self.wfile.write(response.encode('utf-8'))
|
|
69
|
+
|
|
70
|
+
def do_GET(self):
|
|
71
|
+
"""Handle GET requests (for health checks, etc.)"""
|
|
72
|
+
if self.path == '/health':
|
|
73
|
+
self.send_response(200)
|
|
74
|
+
self.send_header('Content-Type', 'application/json')
|
|
75
|
+
response = json.dumps({"status": "healthy", "timestamp": time.time()})
|
|
76
|
+
self.send_header('Content-Length', str(len(response)))
|
|
77
|
+
self.end_headers()
|
|
78
|
+
self.wfile.write(response.encode('utf-8'))
|
|
79
|
+
else:
|
|
80
|
+
self.send_error(404, "Not Found")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
|
84
|
+
"""Multi-threaded HTTP server"""
|
|
85
|
+
daemon_threads = True
|
|
86
|
+
allow_reuse_address = True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SharedTensorServer:
|
|
90
|
+
"""
|
|
91
|
+
JSON-RPC server for shared tensor remote execution
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, host: str = "localhost", port: int = 8080):
|
|
95
|
+
"""
|
|
96
|
+
Initialize the server
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
host: Server host address
|
|
100
|
+
port: Server port number
|
|
101
|
+
"""
|
|
102
|
+
self.host = host
|
|
103
|
+
self.port = port
|
|
104
|
+
self.server = None
|
|
105
|
+
self.server_thread = None
|
|
106
|
+
self.running = False
|
|
107
|
+
|
|
108
|
+
# Server statistics
|
|
109
|
+
self.stats = {
|
|
110
|
+
'start_time': None,
|
|
111
|
+
'requests_processed': 0,
|
|
112
|
+
'errors_encountered': 0,
|
|
113
|
+
'functions_executed': {}
|
|
114
|
+
}
|
|
115
|
+
self.singleton_cache = {}
|
|
116
|
+
|
|
117
|
+
def process_jsonrpc_request(self, request_data: str) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Process a JSON-RPC request
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
request_data: Raw JSON request string
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
JSON response string
|
|
126
|
+
"""
|
|
127
|
+
request_id = None
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Parse JSON-RPC request
|
|
131
|
+
try:
|
|
132
|
+
request = parse_request(request_data)
|
|
133
|
+
request_id = request.id
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
logger.error(f"Invalid JSON-RPC request: {e}")
|
|
136
|
+
response = create_error_response(
|
|
137
|
+
None,
|
|
138
|
+
JsonRpcErrorCodes.INVALID_REQUEST,
|
|
139
|
+
str(e)
|
|
140
|
+
)
|
|
141
|
+
return response.to_json()
|
|
142
|
+
|
|
143
|
+
# Update statistics
|
|
144
|
+
self.stats['requests_processed'] += 1
|
|
145
|
+
|
|
146
|
+
# Route method calls
|
|
147
|
+
method = request.method
|
|
148
|
+
params = request.params or {}
|
|
149
|
+
|
|
150
|
+
logger.info(f"Processing method: {method}, params: {params}")
|
|
151
|
+
|
|
152
|
+
if method == "execute_function":
|
|
153
|
+
result = self._handle_execute_function(params)
|
|
154
|
+
elif method == "submit_task":
|
|
155
|
+
result = self._handle_submit_task(params)
|
|
156
|
+
elif method == "get_task_status":
|
|
157
|
+
result = self._handle_get_task_status(params)
|
|
158
|
+
elif method == "cancel_task":
|
|
159
|
+
result = self._handle_cancel_task(params)
|
|
160
|
+
elif method == "list_tasks":
|
|
161
|
+
result = self._handle_list_tasks(params)
|
|
162
|
+
elif method == "ping":
|
|
163
|
+
result = {"pong": True, "timestamp": time.time()}
|
|
164
|
+
elif method == "get_server_info":
|
|
165
|
+
result = self._get_server_info()
|
|
166
|
+
elif method == "list_functions":
|
|
167
|
+
result = self._list_functions()
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(f"Unknown method: {method}")
|
|
170
|
+
|
|
171
|
+
# Create success response
|
|
172
|
+
response = create_success_response(request_id, result)
|
|
173
|
+
return response.to_json()
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Error processing request: {e}")
|
|
177
|
+
self.stats['errors_encountered'] += 1
|
|
178
|
+
|
|
179
|
+
# Determine error code based on exception type
|
|
180
|
+
if isinstance(e, ImportError):
|
|
181
|
+
error_code = JsonRpcErrorCodes.FUNCTION_IMPORT_ERROR
|
|
182
|
+
elif isinstance(e, (pickle.PickleError, ValueError)):
|
|
183
|
+
error_code = JsonRpcErrorCodes.SERIALIZATION_ERROR
|
|
184
|
+
else:
|
|
185
|
+
error_code = JsonRpcErrorCodes.FUNCTION_EXECUTION_ERROR
|
|
186
|
+
|
|
187
|
+
response = create_error_response(request_id, error_code, str(e))
|
|
188
|
+
return response.to_json()
|
|
189
|
+
|
|
190
|
+
def _handle_execute_function(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
191
|
+
"""Handle execute_function method"""
|
|
192
|
+
# Validate parameters
|
|
193
|
+
required_params = ['function_path', 'args', 'kwargs', 'encoding']
|
|
194
|
+
for param in required_params:
|
|
195
|
+
if param not in params:
|
|
196
|
+
raise ValueError(f"Missing required parameter: {param}")
|
|
197
|
+
|
|
198
|
+
function_path = params['function_path']
|
|
199
|
+
args_hex = params['args']
|
|
200
|
+
kwargs_hex = params['kwargs']
|
|
201
|
+
options = params.get('options', {})
|
|
202
|
+
encoding = params['encoding']
|
|
203
|
+
|
|
204
|
+
cache_name = options.get('name')
|
|
205
|
+
singleton = options.get('singleton', False)
|
|
206
|
+
singleton_key_formatter = options.get('singleton_key_formatter') or '{_name_}'
|
|
207
|
+
|
|
208
|
+
if encoding != 'pickle_hex':
|
|
209
|
+
raise ValueError(f"Unsupported encoding: {encoding}")
|
|
210
|
+
|
|
211
|
+
logger.info(f"Executing function: {function_path}")
|
|
212
|
+
|
|
213
|
+
# Update function execution statistics
|
|
214
|
+
if function_path not in self.stats['functions_executed']:
|
|
215
|
+
self.stats['functions_executed'][function_path] = 0
|
|
216
|
+
self.stats['functions_executed'][function_path] += 1
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
func = import_function_from_path(function_path)
|
|
220
|
+
|
|
221
|
+
# Deserialize arguments using ForkingPickler
|
|
222
|
+
args = deserialize_args(args_hex)
|
|
223
|
+
kwargs = deserialize_args(kwargs_hex) if kwargs_hex else {}
|
|
224
|
+
|
|
225
|
+
cache_key = format_cache_key(cache_name, func, args, kwargs, singleton_key_formatter)
|
|
226
|
+
logger.debug(f"Cache key: {cache_key}")
|
|
227
|
+
|
|
228
|
+
if singleton and cache_key in self.singleton_cache:
|
|
229
|
+
logger.info(f"Returning cached result for {cache_key}")
|
|
230
|
+
return self.singleton_cache[cache_key]
|
|
231
|
+
|
|
232
|
+
# Execute the function
|
|
233
|
+
result = func(*args, **kwargs)
|
|
234
|
+
|
|
235
|
+
# Serialize result using ForkingPickler
|
|
236
|
+
result_bytes = serialize_result(result)
|
|
237
|
+
|
|
238
|
+
result_info = {
|
|
239
|
+
'handler': result_bytes.hex(),
|
|
240
|
+
'function_path': function_path,
|
|
241
|
+
'execution_time': time.time()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if singleton:
|
|
245
|
+
logger.info(f"Caching result for {cache_key}")
|
|
246
|
+
self.singleton_cache[cache_key] = result_info
|
|
247
|
+
|
|
248
|
+
return result_info
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.error(f"Function execution failed: {e}")
|
|
251
|
+
raise
|
|
252
|
+
|
|
253
|
+
def _handle_submit_task(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
254
|
+
"""Handle submit_task method - submit task for async execution"""
|
|
255
|
+
required_params = ['function_path', 'args', 'kwargs', 'encoding']
|
|
256
|
+
for param in required_params:
|
|
257
|
+
if param not in params:
|
|
258
|
+
raise ValueError(f"Missing required parameter: {param}")
|
|
259
|
+
|
|
260
|
+
function_path = params['function_path']
|
|
261
|
+
args_hex = params['args']
|
|
262
|
+
kwargs_hex = params['kwargs']
|
|
263
|
+
options = params.get('options', {})
|
|
264
|
+
encoding = params['encoding']
|
|
265
|
+
|
|
266
|
+
if encoding != 'pickle_hex':
|
|
267
|
+
raise ValueError(f"Unsupported encoding: {encoding}")
|
|
268
|
+
|
|
269
|
+
logger.info(f"Submitting async task: {function_path}")
|
|
270
|
+
|
|
271
|
+
# Deserialize arguments to validate them
|
|
272
|
+
try:
|
|
273
|
+
args = deserialize_args(args_hex)
|
|
274
|
+
kwargs = deserialize_args(kwargs_hex) if kwargs_hex else {}
|
|
275
|
+
except Exception as e:
|
|
276
|
+
raise ValueError(f"Failed to deserialize arguments: {e}")
|
|
277
|
+
|
|
278
|
+
# Submit task to task manager
|
|
279
|
+
task_manager = get_task_manager()
|
|
280
|
+
task_id = task_manager.submit_task(function_path, args, kwargs, options)
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
'task_id': task_id,
|
|
284
|
+
'function_path': function_path,
|
|
285
|
+
'submitted_at': time.time()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
def _handle_get_task_status(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
289
|
+
"""Handle get_task_status method"""
|
|
290
|
+
if 'task_id' not in params:
|
|
291
|
+
raise ValueError("Missing required parameter: task_id")
|
|
292
|
+
|
|
293
|
+
task_id = params['task_id']
|
|
294
|
+
task_manager = get_task_manager()
|
|
295
|
+
|
|
296
|
+
task_info = task_manager.get_task(task_id)
|
|
297
|
+
if not task_info:
|
|
298
|
+
raise ValueError(f"Task {task_id} not found")
|
|
299
|
+
|
|
300
|
+
return task_info.to_dict()
|
|
301
|
+
|
|
302
|
+
def _handle_cancel_task(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
303
|
+
"""Handle cancel_task method"""
|
|
304
|
+
if 'task_id' not in params:
|
|
305
|
+
raise ValueError("Missing required parameter: task_id")
|
|
306
|
+
|
|
307
|
+
task_id = params['task_id']
|
|
308
|
+
task_manager = get_task_manager()
|
|
309
|
+
|
|
310
|
+
cancelled = task_manager.cancel_task(task_id)
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
'task_id': task_id,
|
|
314
|
+
'cancelled': cancelled,
|
|
315
|
+
'timestamp': time.time()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
def _handle_list_tasks(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
319
|
+
"""Handle list_tasks method"""
|
|
320
|
+
status_filter = params.get('status')
|
|
321
|
+
task_manager = get_task_manager()
|
|
322
|
+
|
|
323
|
+
# Convert status string to enum if provided
|
|
324
|
+
status_enum = None
|
|
325
|
+
if status_filter:
|
|
326
|
+
try:
|
|
327
|
+
status_enum = TaskStatus(status_filter)
|
|
328
|
+
except ValueError:
|
|
329
|
+
raise ValueError(f"Invalid status: {status_filter}")
|
|
330
|
+
|
|
331
|
+
tasks = task_manager.list_tasks(status_enum)
|
|
332
|
+
|
|
333
|
+
# Convert to dict format
|
|
334
|
+
result = {}
|
|
335
|
+
for task_id, task_info in tasks.items():
|
|
336
|
+
result[task_id] = task_info.to_dict()
|
|
337
|
+
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
def _get_server_info(self) -> Dict[str, Any]:
|
|
341
|
+
"""Get server information"""
|
|
342
|
+
uptime = time.time() - self.stats['start_time'] if self.stats['start_time'] else 0
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
'server': 'SharedTensorServer',
|
|
346
|
+
'version': '1.0.0',
|
|
347
|
+
'host': self.host,
|
|
348
|
+
'port': self.port,
|
|
349
|
+
'uptime': uptime,
|
|
350
|
+
'stats': self.stats.copy(),
|
|
351
|
+
'python_version': sys.version,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
def _list_functions(self) -> Dict[str, Any]:
|
|
355
|
+
"""List available functions"""
|
|
356
|
+
return {
|
|
357
|
+
'executed_functions': list(self.stats['functions_executed'].keys()),
|
|
358
|
+
'execution_counts': self.stats['functions_executed'].copy()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
def start(self, blocking: bool = True):
|
|
362
|
+
"""
|
|
363
|
+
Start the server
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
blocking: If True, block until server stops. If False, run in background.
|
|
367
|
+
"""
|
|
368
|
+
if self.running:
|
|
369
|
+
raise RuntimeError("Server is already running")
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
# Create HTTP server
|
|
373
|
+
self.server = ThreadedHTTPServer((self.host, self.port), SharedTensorRequestHandler)
|
|
374
|
+
self.server.process_jsonrpc_request = self.process_jsonrpc_request
|
|
375
|
+
|
|
376
|
+
self.running = True
|
|
377
|
+
self.stats['start_time'] = time.time()
|
|
378
|
+
|
|
379
|
+
logger.info(f"Starting SharedTensorServer on {self.host}:{self.port}")
|
|
380
|
+
|
|
381
|
+
if blocking:
|
|
382
|
+
self.server.serve_forever()
|
|
383
|
+
else:
|
|
384
|
+
self.server_thread = threading.Thread(target=self.server.serve_forever)
|
|
385
|
+
self.server_thread.daemon = True
|
|
386
|
+
self.server_thread.start()
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
self.running = False
|
|
390
|
+
raise RuntimeError(f"Failed to start server: {e}")
|
|
391
|
+
|
|
392
|
+
def stop(self):
|
|
393
|
+
"""Stop the server"""
|
|
394
|
+
if not self.running:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
logger.info("Stopping SharedTensorServer")
|
|
398
|
+
|
|
399
|
+
if self.server:
|
|
400
|
+
self.server.shutdown()
|
|
401
|
+
self.server.server_close()
|
|
402
|
+
|
|
403
|
+
if self.server_thread and self.server_thread.is_alive():
|
|
404
|
+
self.server_thread.join(timeout=5)
|
|
405
|
+
|
|
406
|
+
self.running = False
|
|
407
|
+
logger.info("Server stopped")
|
|
408
|
+
|
|
409
|
+
def __enter__(self):
|
|
410
|
+
self.start(blocking=False)
|
|
411
|
+
return self
|
|
412
|
+
|
|
413
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
414
|
+
self.stop()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def main():
|
|
418
|
+
"""Main entry point for running the server"""
|
|
419
|
+
import argparse
|
|
420
|
+
|
|
421
|
+
rank = int(os.getenv("RANK", 0))
|
|
422
|
+
local_rank = int(os.getenv("LOCAL_RANK", 0))
|
|
423
|
+
torch.cuda.set_device(local_rank)
|
|
424
|
+
logger.info(f"Using device {local_rank}")
|
|
425
|
+
|
|
426
|
+
parser = argparse.ArgumentParser(description='Shared Tensor JSON-RPC Server')
|
|
427
|
+
parser.add_argument('--host', default='localhost', help='Server host (default: localhost)')
|
|
428
|
+
parser.add_argument('--port', type=int, default=2537 + rank, help='Server port (default: {})'.format(2537 + rank))
|
|
429
|
+
parser.add_argument('--log-level', default='INFO',
|
|
430
|
+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
|
431
|
+
help='Log level (default: INFO)')
|
|
432
|
+
|
|
433
|
+
args = parser.parse_args()
|
|
434
|
+
|
|
435
|
+
# Configure logging
|
|
436
|
+
logging.basicConfig(
|
|
437
|
+
level=getattr(logging, args.log_level),
|
|
438
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Create and start server
|
|
442
|
+
server = SharedTensorServer(host=args.host, port=args.port)
|
|
443
|
+
os.environ["__SHARED_TENSOR_SERVER_MODE__"] = "true"
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
server.start(blocking=True)
|
|
447
|
+
except KeyboardInterrupt:
|
|
448
|
+
logger.info("Received interrupt signal")
|
|
449
|
+
server.stop()
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error(f"Server error: {e}")
|
|
452
|
+
return 1
|
|
453
|
+
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
if __name__ == "__main__":
|
|
458
|
+
sys.exit(main())
|
shared_tensor/utils.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Remote function executor utility
|
|
3
|
+
|
|
4
|
+
This module provides utilities for importing and executing functions
|
|
5
|
+
from their string paths on the remote server side.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import logging
|
|
10
|
+
import io
|
|
11
|
+
import pickle
|
|
12
|
+
import inspect
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["serialize_result", "deserialize_args", "import_function_from_path", "format_cache_key"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def serialize_result(obj: Any) -> bytes:
|
|
25
|
+
"""
|
|
26
|
+
Serialize result using ForkingPickler if obj is a torch.nn.Module or torch.Tensor on GPU, otherwise use standard pickle.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
obj: Object to serialize
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Serialized bytes
|
|
33
|
+
"""
|
|
34
|
+
buffer = io.BytesIO()
|
|
35
|
+
|
|
36
|
+
use_forking_pickler = False
|
|
37
|
+
if isinstance(obj, torch.nn.Module):
|
|
38
|
+
obj.cuda()
|
|
39
|
+
obj.share_memory()
|
|
40
|
+
use_forking_pickler = True
|
|
41
|
+
elif isinstance(obj, torch.Tensor):
|
|
42
|
+
obj.cuda()
|
|
43
|
+
obj.share_memory_()
|
|
44
|
+
use_forking_pickler = True
|
|
45
|
+
|
|
46
|
+
if use_forking_pickler:
|
|
47
|
+
torch.multiprocessing.reducer.ForkingPickler(buffer).dump(obj)
|
|
48
|
+
else:
|
|
49
|
+
pickle.dump(obj, buffer)
|
|
50
|
+
return buffer.getvalue()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def deserialize_args(data_hex: str) -> Any:
|
|
54
|
+
"""
|
|
55
|
+
Deserialize arguments from hex string.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data_hex: Hex-encoded serialized data
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Deserialized object
|
|
62
|
+
"""
|
|
63
|
+
if not data_hex:
|
|
64
|
+
return ()
|
|
65
|
+
|
|
66
|
+
if isinstance(data_hex, bytes):
|
|
67
|
+
data_bytes = data_hex
|
|
68
|
+
else:
|
|
69
|
+
data_bytes = bytes.fromhex(data_hex)
|
|
70
|
+
return pickle.loads(data_bytes)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def import_function_from_path(function_path: str) -> Callable:
|
|
74
|
+
"""
|
|
75
|
+
Import a function from its string path.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
function_path: Function path in format "module.submodule:function_name"
|
|
79
|
+
e.g., "abc.addd:get_model" or "mypackage.models:create_model"
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The imported function object
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ImportError: If the module or function cannot be imported
|
|
86
|
+
ValueError: If the function_path format is invalid
|
|
87
|
+
"""
|
|
88
|
+
if ':' not in function_path:
|
|
89
|
+
raise ValueError(f"Invalid function path format: {function_path}. Expected 'module:function'")
|
|
90
|
+
|
|
91
|
+
module_path, func_name = function_path.split(':', 1)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Import the module
|
|
95
|
+
module = importlib.import_module(module_path)
|
|
96
|
+
|
|
97
|
+
# Handle nested function names (e.g., "ClassName.method_name")
|
|
98
|
+
func_obj = module
|
|
99
|
+
for attr in func_name.split('.'):
|
|
100
|
+
func_obj = getattr(func_obj, attr)
|
|
101
|
+
|
|
102
|
+
if not callable(func_obj):
|
|
103
|
+
raise ValueError(f"Object at {function_path} is not callable")
|
|
104
|
+
|
|
105
|
+
return func_obj
|
|
106
|
+
|
|
107
|
+
except ImportError as e:
|
|
108
|
+
raise ImportError(f"Cannot import module '{module_path}': {e}")
|
|
109
|
+
except AttributeError as e:
|
|
110
|
+
raise ImportError(f"Cannot find function '{func_name}' in module '{module_path}': {e}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def format_cache_key(name, func, args, kwargs, singleton_key_formatter):
|
|
114
|
+
sig = inspect.signature(func)
|
|
115
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
116
|
+
bound_args.apply_defaults()
|
|
117
|
+
fullkwargs = {}
|
|
118
|
+
for param_name in sig.parameters:
|
|
119
|
+
if param_name in bound_args.arguments:
|
|
120
|
+
value = bound_args.arguments[param_name]
|
|
121
|
+
fullkwargs[param_name] = value
|
|
122
|
+
return singleton_key_formatter.format(_name_=name, **fullkwargs)
|