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.
@@ -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)