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/jsonrpc.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON-RPC 2.0 implementation for shared tensor communication
|
|
3
|
+
|
|
4
|
+
Implements JSON-RPC 2.0 specification: https://www.jsonrpc.org/specification
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any, Dict, Optional, Union
|
|
10
|
+
from dataclasses import dataclass, asdict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class JsonRpcRequest:
|
|
15
|
+
"""JSON-RPC 2.0 Request object"""
|
|
16
|
+
method: str
|
|
17
|
+
params: Optional[Union[Dict[str, Any], list]] = None
|
|
18
|
+
id: Optional[Union[str, int]] = None
|
|
19
|
+
jsonrpc: str = "2.0"
|
|
20
|
+
|
|
21
|
+
def __post_init__(self):
|
|
22
|
+
if self.id is None:
|
|
23
|
+
self.id = str(uuid.uuid4())
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
26
|
+
"""Convert to dictionary for JSON serialization"""
|
|
27
|
+
result = asdict(self)
|
|
28
|
+
# Remove None params to keep the request clean
|
|
29
|
+
if result['params'] is None:
|
|
30
|
+
del result['params']
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
def to_json(self) -> str:
|
|
34
|
+
"""Convert to JSON string"""
|
|
35
|
+
return json.dumps(self.to_dict())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class JsonRpcResponse:
|
|
40
|
+
"""JSON-RPC 2.0 Response object"""
|
|
41
|
+
id: Optional[Union[str, int]]
|
|
42
|
+
result: Optional[Any] = None
|
|
43
|
+
error: Optional[Dict[str, Any]] = None
|
|
44
|
+
jsonrpc: str = "2.0"
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
47
|
+
"""Convert to dictionary for JSON serialization"""
|
|
48
|
+
result = asdict(self)
|
|
49
|
+
# Only include result OR error, not both
|
|
50
|
+
if self.error is not None:
|
|
51
|
+
result.pop('result', None)
|
|
52
|
+
else:
|
|
53
|
+
result.pop('error', None)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
def to_json(self) -> str:
|
|
57
|
+
"""Convert to JSON string"""
|
|
58
|
+
return json.dumps(self.to_dict())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class JsonRpcError:
|
|
63
|
+
"""JSON-RPC 2.0 Error object"""
|
|
64
|
+
code: int
|
|
65
|
+
message: str
|
|
66
|
+
data: Optional[Any] = None
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
"""Convert to dictionary"""
|
|
70
|
+
result = asdict(self)
|
|
71
|
+
if result['data'] is None:
|
|
72
|
+
del result['data']
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Standard JSON-RPC error codes
|
|
77
|
+
class JsonRpcErrorCodes:
|
|
78
|
+
PARSE_ERROR = -32700
|
|
79
|
+
INVALID_REQUEST = -32600
|
|
80
|
+
METHOD_NOT_FOUND = -32601
|
|
81
|
+
INVALID_PARAMS = -32602
|
|
82
|
+
INTERNAL_ERROR = -32603
|
|
83
|
+
|
|
84
|
+
# Custom error codes for shared tensor
|
|
85
|
+
FUNCTION_IMPORT_ERROR = -32001
|
|
86
|
+
FUNCTION_EXECUTION_ERROR = -32002
|
|
87
|
+
SERIALIZATION_ERROR = -32003
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_success_response(request_id: Optional[Union[str, int]], result: Any) -> JsonRpcResponse:
|
|
91
|
+
"""Create a successful JSON-RPC response"""
|
|
92
|
+
return JsonRpcResponse(id=request_id, result=result)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_error_response(
|
|
96
|
+
request_id: Optional[Union[str, int]],
|
|
97
|
+
code: int,
|
|
98
|
+
message: str,
|
|
99
|
+
data: Optional[Any] = None
|
|
100
|
+
) -> JsonRpcResponse:
|
|
101
|
+
"""Create an error JSON-RPC response"""
|
|
102
|
+
error = JsonRpcError(code=code, message=message, data=data)
|
|
103
|
+
return JsonRpcResponse(id=request_id, error=error.to_dict())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_request(json_str: str) -> JsonRpcRequest:
|
|
107
|
+
"""Parse JSON string into JsonRpcRequest object"""
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(json_str)
|
|
110
|
+
except json.JSONDecodeError as e:
|
|
111
|
+
raise ValueError(f"Invalid JSON: {e}")
|
|
112
|
+
|
|
113
|
+
# Validate required fields
|
|
114
|
+
if not isinstance(data, dict):
|
|
115
|
+
raise ValueError("Request must be a JSON object")
|
|
116
|
+
|
|
117
|
+
if data.get('jsonrpc') != '2.0':
|
|
118
|
+
raise ValueError("Invalid jsonrpc version, must be '2.0'")
|
|
119
|
+
|
|
120
|
+
if 'method' not in data:
|
|
121
|
+
raise ValueError("Missing required field 'method'")
|
|
122
|
+
|
|
123
|
+
return JsonRpcRequest(
|
|
124
|
+
method=data['method'],
|
|
125
|
+
params=data.get('params'),
|
|
126
|
+
id=data.get('id'),
|
|
127
|
+
jsonrpc=data['jsonrpc']
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_response(json_str: str) -> JsonRpcResponse:
|
|
132
|
+
"""Parse JSON string into JsonRpcResponse object"""
|
|
133
|
+
try:
|
|
134
|
+
data = json.loads(json_str)
|
|
135
|
+
except json.JSONDecodeError as e:
|
|
136
|
+
raise ValueError(f"Invalid JSON: {e}")
|
|
137
|
+
|
|
138
|
+
# Validate required fields
|
|
139
|
+
if not isinstance(data, dict):
|
|
140
|
+
raise ValueError("Response must be a JSON object")
|
|
141
|
+
|
|
142
|
+
if data.get('jsonrpc') != '2.0':
|
|
143
|
+
raise ValueError("Invalid jsonrpc version, must be '2.0'")
|
|
144
|
+
|
|
145
|
+
if 'id' not in data:
|
|
146
|
+
raise ValueError("Missing required field 'id'")
|
|
147
|
+
|
|
148
|
+
# Must have either result or error
|
|
149
|
+
has_result = 'result' in data
|
|
150
|
+
has_error = 'error' in data
|
|
151
|
+
|
|
152
|
+
if not has_result and not has_error:
|
|
153
|
+
raise ValueError("Response must have either 'result' or 'error'")
|
|
154
|
+
|
|
155
|
+
if has_result and has_error:
|
|
156
|
+
raise ValueError("Response cannot have both 'result' and 'error'")
|
|
157
|
+
|
|
158
|
+
return JsonRpcResponse(
|
|
159
|
+
id=data['id'],
|
|
160
|
+
result=data.get('result'),
|
|
161
|
+
error=data.get('error'),
|
|
162
|
+
jsonrpc=data['jsonrpc']
|
|
163
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared Tensor Provider
|
|
3
|
+
|
|
4
|
+
This module provides a provider for sharing functions across processes using JSON-RPC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from typing import Any, Dict, Callable, Optional
|
|
12
|
+
from shared_tensor.client import SharedTensorClient
|
|
13
|
+
from shared_tensor.errors import SharedTensorProviderError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["SharedTensorProvider"]
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
global_rank = int(os.getenv("RANK", 0))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SharedTensorProvider:
|
|
23
|
+
|
|
24
|
+
def __init__(self, server_port: int = 2537 + global_rank, verbose_debug: bool = False):
|
|
25
|
+
self.server_port: int = server_port
|
|
26
|
+
self.server_mode = os.getenv("__SHARED_TENSOR_SERVER_MODE__", "false")
|
|
27
|
+
self.verbose_debug = verbose_debug
|
|
28
|
+
logger.debug(f"SharedTensorProvider initialized with server port {self.server_port}, server mode {self.server_mode}, and verbose debug {self.verbose_debug}")
|
|
29
|
+
self._registered_functions: Dict[str, Dict[str, Any]] = {}
|
|
30
|
+
self._client = None
|
|
31
|
+
|
|
32
|
+
def _get_function_path(self, func: Callable) -> str:
|
|
33
|
+
"""Get the importable path of a function in format 'module.submodule:function_name'"""
|
|
34
|
+
module = inspect.getmodule(func)
|
|
35
|
+
|
|
36
|
+
if module is None:
|
|
37
|
+
raise SharedTensorProviderError(f"Failed to get full qualified name for function {func.__name__}, function module is missing")
|
|
38
|
+
|
|
39
|
+
module_name = module.__name__
|
|
40
|
+
|
|
41
|
+
if module_name == "__main__":
|
|
42
|
+
if hasattr(module, '__file__') and module.__file__:
|
|
43
|
+
file_path = module.__file__
|
|
44
|
+
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
45
|
+
# For test files, we need to include the full path
|
|
46
|
+
if 'tests' in file_path:
|
|
47
|
+
# Extract the module path from the file path
|
|
48
|
+
path_parts = file_path.split(os.sep)
|
|
49
|
+
if 'tests' in path_parts:
|
|
50
|
+
test_idx = path_parts.index('tests')
|
|
51
|
+
module_parts = path_parts[test_idx:]
|
|
52
|
+
# Remove .py extension from last part
|
|
53
|
+
module_parts[-1] = os.path.splitext(module_parts[-1])[0]
|
|
54
|
+
module_name = '.'.join(module_parts)
|
|
55
|
+
else:
|
|
56
|
+
module_name = file_name
|
|
57
|
+
else:
|
|
58
|
+
module_name = file_name
|
|
59
|
+
else:
|
|
60
|
+
raise SharedTensorProviderError(f"Failed to get full qualified name for function {func.__name__}, function module file path is empty")
|
|
61
|
+
|
|
62
|
+
if hasattr(func, '__qualname__'):
|
|
63
|
+
qualname = func.__qualname__
|
|
64
|
+
# Only reject true nested functions with <locals>, but allow test functions
|
|
65
|
+
# Test functions might have qualname like "TestClass.test_method.<locals>.test_function"
|
|
66
|
+
# but we can still use them by extracting the actual function name
|
|
67
|
+
if '<locals>' in qualname:
|
|
68
|
+
# For nested functions, try to extract the innermost function name
|
|
69
|
+
# This allows test functions defined inside test methods to work
|
|
70
|
+
func_path = func.__name__
|
|
71
|
+
logger.warning(f"Function {func.__name__} appears to be nested (qualname: {qualname}), using function name only")
|
|
72
|
+
else:
|
|
73
|
+
func_path = qualname
|
|
74
|
+
else:
|
|
75
|
+
func_path = func.__name__
|
|
76
|
+
|
|
77
|
+
return f"{module_name}:{func_path}"
|
|
78
|
+
|
|
79
|
+
def share(self, name: Optional[str] = None, singleton: bool = True, singleton_key_formatter: Optional[str] = None):
|
|
80
|
+
"""Decorator to register a function for remote sharing
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
name: Optional custom name for the function
|
|
84
|
+
singleton: Whether to use a singleton instance of the function result
|
|
85
|
+
singleton_key_formatter: Formatter for cached results
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Decorator function that registers the function for remote sharing
|
|
89
|
+
"""
|
|
90
|
+
def decorator(func: Callable):
|
|
91
|
+
func_name = name or func.__name__
|
|
92
|
+
|
|
93
|
+
if self.server_mode == "true":
|
|
94
|
+
logger.debug(f"Server mode is true, returning function {func_name} without registering")
|
|
95
|
+
return func
|
|
96
|
+
|
|
97
|
+
logger.debug(f"Server mode is false, registering function {func_name}")
|
|
98
|
+
function_path = self._get_function_path(func)
|
|
99
|
+
|
|
100
|
+
logger.debug(f"Function {func_name} registered with function path {function_path}")
|
|
101
|
+
options = {
|
|
102
|
+
'name': func_name,
|
|
103
|
+
'singleton': singleton,
|
|
104
|
+
'singleton_key_formatter': singleton_key_formatter,
|
|
105
|
+
}
|
|
106
|
+
function_info = {
|
|
107
|
+
'name': func_name,
|
|
108
|
+
'function_path': function_path,
|
|
109
|
+
'options': options,
|
|
110
|
+
}
|
|
111
|
+
self._registered_functions[func_name] = function_info
|
|
112
|
+
|
|
113
|
+
@wraps(func)
|
|
114
|
+
def wrapper(*args, **kwargs):
|
|
115
|
+
return self._execute_remote_function(func_name, args, kwargs, options)
|
|
116
|
+
|
|
117
|
+
return wrapper
|
|
118
|
+
return decorator
|
|
119
|
+
|
|
120
|
+
def _get_client(self) -> SharedTensorClient:
|
|
121
|
+
"""Get or create JSON-RPC client"""
|
|
122
|
+
if self._client is None:
|
|
123
|
+
logger.debug(f"Creating new JSON-RPC client with server port {self.server_port}")
|
|
124
|
+
self._client = SharedTensorClient(self.server_port, verbose_debug=self.verbose_debug)
|
|
125
|
+
logger.debug(f"JSON-RPC client created with server port {self.server_port}")
|
|
126
|
+
return self._client
|
|
127
|
+
|
|
128
|
+
def _execute_remote_function(self, func_name: str, args: tuple, kwargs: dict, options: dict) -> Any:
|
|
129
|
+
"""Execute function remotely using JSON-RPC client"""
|
|
130
|
+
try:
|
|
131
|
+
if self.verbose_debug:
|
|
132
|
+
logger.debug(f"Executing remote function {func_name} with args {args} and kwargs {kwargs}")
|
|
133
|
+
else:
|
|
134
|
+
logger.debug(f"Executing remote function {func_name}")
|
|
135
|
+
|
|
136
|
+
if func_name not in self._registered_functions:
|
|
137
|
+
raise SharedTensorProviderError(f"Function {func_name} not registered")
|
|
138
|
+
|
|
139
|
+
function_info = self._registered_functions[func_name]
|
|
140
|
+
function_path = function_info['function_path']
|
|
141
|
+
client = self._get_client()
|
|
142
|
+
logger.debug(f"Executing remote function {func_name} with function path {function_path} and options {options}")
|
|
143
|
+
return client.execute_function(function_path, args, kwargs, options)
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning(f"Failed to execute remote function {func_name}: {str(e)}")
|
|
147
|
+
raise SharedTensorProviderError(f"Failed to execute remote function {func_name}: {str(e)}")
|
|
148
|
+
|
|
149
|
+
def close(self):
|
|
150
|
+
"""Close the provider and its client connection"""
|
|
151
|
+
if self._client:
|
|
152
|
+
logger.debug(f"Closing JSON-RPC client with server port {self.server_port}")
|
|
153
|
+
self._client.close()
|
|
154
|
+
logger.debug(f"JSON-RPC client closed with server port {self.server_port}")
|
|
155
|
+
self._client = None
|