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
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async Task Management System
|
|
3
|
+
|
|
4
|
+
Provides task queue and execution management for long-running functions
|
|
5
|
+
that shouldn't be limited by HTTP timeouts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
import time
|
|
10
|
+
import pickle
|
|
11
|
+
import threading
|
|
12
|
+
import queue
|
|
13
|
+
import logging
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
from dataclasses import dataclass, asdict
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskStatus(Enum):
|
|
23
|
+
"""Task execution status"""
|
|
24
|
+
PENDING = "pending" # Task submitted, waiting to start
|
|
25
|
+
RUNNING = "running" # Task currently executing
|
|
26
|
+
COMPLETED = "completed" # Task finished successfully
|
|
27
|
+
FAILED = "failed" # Task failed with error
|
|
28
|
+
CANCELLED = "cancelled" # Task was cancelled
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TaskInfo:
|
|
33
|
+
"""Task information container"""
|
|
34
|
+
task_id: str
|
|
35
|
+
function_path: str
|
|
36
|
+
args_hex: str
|
|
37
|
+
kwargs_hex: str
|
|
38
|
+
options: Dict[str, Any]
|
|
39
|
+
status: TaskStatus
|
|
40
|
+
created_at: float
|
|
41
|
+
started_at: Optional[float] = None
|
|
42
|
+
completed_at: Optional[float] = None
|
|
43
|
+
result_hex: Optional[str] = None
|
|
44
|
+
error_message: Optional[str] = None
|
|
45
|
+
progress: Optional[Dict[str, Any]] = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
48
|
+
"""Convert to dictionary for JSON serialization"""
|
|
49
|
+
data = asdict(self)
|
|
50
|
+
data['status'] = self.status.value
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'TaskInfo':
|
|
55
|
+
"""Create from dictionary"""
|
|
56
|
+
data['status'] = TaskStatus(data['status'])
|
|
57
|
+
return cls(**data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TaskExecutor:
|
|
61
|
+
"""Executes tasks in background threads"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, max_workers: int = 4):
|
|
64
|
+
self.max_workers = max_workers
|
|
65
|
+
self.executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
66
|
+
self.running_tasks: Dict[str, threading.Future] = {}
|
|
67
|
+
self._shutdown = False
|
|
68
|
+
|
|
69
|
+
def submit_task(self, task_info: TaskInfo, task_manager: 'TaskManager') -> bool:
|
|
70
|
+
"""Submit a task for execution"""
|
|
71
|
+
if self._shutdown:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
future = self.executor.submit(self._execute_task, task_info, task_manager)
|
|
76
|
+
self.running_tasks[task_info.task_id] = future
|
|
77
|
+
return True
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Failed to submit task {task_info.task_id}: {e}")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def _execute_task(self, task_info: TaskInfo, task_manager: 'TaskManager'):
|
|
83
|
+
"""Execute a single task"""
|
|
84
|
+
from shared_tensor.utils import import_function_from_path, serialize_result, deserialize_args
|
|
85
|
+
|
|
86
|
+
task_id = task_info.task_id
|
|
87
|
+
logger.info(f"Starting task execution: {task_id}")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Update task status to running
|
|
91
|
+
task_info.status = TaskStatus.RUNNING
|
|
92
|
+
task_info.started_at = time.time()
|
|
93
|
+
task_manager.update_task(task_info)
|
|
94
|
+
|
|
95
|
+
# Import the function
|
|
96
|
+
func = import_function_from_path(task_info.function_path)
|
|
97
|
+
|
|
98
|
+
# Deserialize arguments using ForkingPickler
|
|
99
|
+
args = deserialize_args(task_info.args_hex)
|
|
100
|
+
kwargs = deserialize_args(task_info.kwargs_hex) if task_info.kwargs_hex else {}
|
|
101
|
+
|
|
102
|
+
logger.debug(f"Executing {task_info.function_path} with args={args}, kwargs={kwargs}")
|
|
103
|
+
|
|
104
|
+
# Execute the function
|
|
105
|
+
result = func(*args, **kwargs)
|
|
106
|
+
|
|
107
|
+
# Serialize result using ForkingPickler
|
|
108
|
+
result_hex = serialize_result(result).hex()
|
|
109
|
+
|
|
110
|
+
# Update task with success
|
|
111
|
+
task_info.status = TaskStatus.COMPLETED
|
|
112
|
+
task_info.completed_at = time.time()
|
|
113
|
+
task_info.result_hex = result_hex
|
|
114
|
+
task_manager.update_task(task_info)
|
|
115
|
+
|
|
116
|
+
logger.info(f"Task completed successfully: {task_id}")
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Task execution failed: {task_id} - {e}")
|
|
120
|
+
|
|
121
|
+
# Update task with error
|
|
122
|
+
task_info.status = TaskStatus.FAILED
|
|
123
|
+
task_info.completed_at = time.time()
|
|
124
|
+
task_info.error_message = str(e)
|
|
125
|
+
task_manager.update_task(task_info)
|
|
126
|
+
|
|
127
|
+
finally:
|
|
128
|
+
# Clean up
|
|
129
|
+
self.running_tasks.pop(task_id, None)
|
|
130
|
+
|
|
131
|
+
def cancel_task(self, task_id: str) -> bool:
|
|
132
|
+
"""Cancel a running task"""
|
|
133
|
+
future = self.running_tasks.get(task_id)
|
|
134
|
+
if future:
|
|
135
|
+
return future.cancel()
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def shutdown(self, wait: bool = True):
|
|
139
|
+
"""Shutdown the executor"""
|
|
140
|
+
self._shutdown = True
|
|
141
|
+
self.executor.shutdown(wait=wait)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TaskManager:
|
|
145
|
+
"""Manages task lifecycle and storage"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, max_tasks: int = 1000, cleanup_interval: int = 3600):
|
|
148
|
+
self.max_tasks = max_tasks
|
|
149
|
+
self.cleanup_interval = cleanup_interval
|
|
150
|
+
self._tasks: Dict[str, TaskInfo] = {}
|
|
151
|
+
self._lock = threading.RLock()
|
|
152
|
+
self._task_queue = queue.Queue()
|
|
153
|
+
self._executor = TaskExecutor()
|
|
154
|
+
self._running = False
|
|
155
|
+
self._worker_thread = None
|
|
156
|
+
self._cleanup_thread = None
|
|
157
|
+
|
|
158
|
+
def start(self):
|
|
159
|
+
"""Start the task manager"""
|
|
160
|
+
if self._running:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
self._running = True
|
|
164
|
+
|
|
165
|
+
# Start worker thread for processing tasks
|
|
166
|
+
self._worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
|
|
167
|
+
self._worker_thread.start()
|
|
168
|
+
|
|
169
|
+
# Start cleanup thread
|
|
170
|
+
self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
|
|
171
|
+
self._cleanup_thread.start()
|
|
172
|
+
|
|
173
|
+
logger.info("TaskManager started")
|
|
174
|
+
|
|
175
|
+
def stop(self):
|
|
176
|
+
"""Stop the task manager"""
|
|
177
|
+
self._running = False
|
|
178
|
+
|
|
179
|
+
if self._worker_thread:
|
|
180
|
+
self._worker_thread.join(timeout=5)
|
|
181
|
+
|
|
182
|
+
if self._cleanup_thread:
|
|
183
|
+
self._cleanup_thread.join(timeout=5)
|
|
184
|
+
|
|
185
|
+
self._executor.shutdown(wait=True)
|
|
186
|
+
logger.info("TaskManager stopped")
|
|
187
|
+
|
|
188
|
+
def submit_task(self, function_path: str, args: tuple = (), kwargs: Dict[str, Any] = None, options: Dict[str, Any] = None) -> str:
|
|
189
|
+
"""Submit a new task for execution"""
|
|
190
|
+
if kwargs is None:
|
|
191
|
+
kwargs = {}
|
|
192
|
+
|
|
193
|
+
# Create task info
|
|
194
|
+
task_id = str(uuid.uuid4())
|
|
195
|
+
|
|
196
|
+
from shared_tensor.utils import serialize_result
|
|
197
|
+
|
|
198
|
+
args_hex = serialize_result(args).hex() if args else ""
|
|
199
|
+
kwargs_hex = serialize_result(kwargs).hex() if kwargs else ""
|
|
200
|
+
|
|
201
|
+
task_info = TaskInfo(
|
|
202
|
+
task_id=task_id,
|
|
203
|
+
function_path=function_path,
|
|
204
|
+
args_hex=args_hex,
|
|
205
|
+
kwargs_hex=kwargs_hex,
|
|
206
|
+
status=TaskStatus.PENDING,
|
|
207
|
+
created_at=time.time()
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
with self._lock:
|
|
211
|
+
self._tasks[task_id] = task_info
|
|
212
|
+
self._task_queue.put(task_id)
|
|
213
|
+
|
|
214
|
+
logger.info(f"Task submitted: {task_id} - {function_path}")
|
|
215
|
+
return task_id
|
|
216
|
+
|
|
217
|
+
def get_task(self, task_id: str) -> Optional[TaskInfo]:
|
|
218
|
+
"""Get task information"""
|
|
219
|
+
with self._lock:
|
|
220
|
+
return self._tasks.get(task_id)
|
|
221
|
+
|
|
222
|
+
def update_task(self, task_info: TaskInfo):
|
|
223
|
+
"""Update task information"""
|
|
224
|
+
with self._lock:
|
|
225
|
+
self._tasks[task_info.task_id] = task_info
|
|
226
|
+
|
|
227
|
+
def list_tasks(self, status: Optional[TaskStatus] = None) -> Dict[str, TaskInfo]:
|
|
228
|
+
"""List tasks, optionally filtered by status"""
|
|
229
|
+
with self._lock:
|
|
230
|
+
if status is None:
|
|
231
|
+
return self._tasks.copy()
|
|
232
|
+
else:
|
|
233
|
+
return {tid: task for tid, task in self._tasks.items()
|
|
234
|
+
if task.status == status}
|
|
235
|
+
|
|
236
|
+
def cancel_task(self, task_id: str) -> bool:
|
|
237
|
+
"""Cancel a task"""
|
|
238
|
+
task = self.get_task(task_id)
|
|
239
|
+
if not task:
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# Try to cancel if running
|
|
246
|
+
if task.status == TaskStatus.RUNNING:
|
|
247
|
+
cancelled = self._executor.cancel_task(task_id)
|
|
248
|
+
else:
|
|
249
|
+
cancelled = True
|
|
250
|
+
|
|
251
|
+
if cancelled:
|
|
252
|
+
task.status = TaskStatus.CANCELLED
|
|
253
|
+
task.completed_at = time.time()
|
|
254
|
+
self.update_task(task)
|
|
255
|
+
|
|
256
|
+
return cancelled
|
|
257
|
+
|
|
258
|
+
def get_task_result(self, task_id: str) -> Any:
|
|
259
|
+
"""Get task result (raises exception if not completed)"""
|
|
260
|
+
task = self.get_task(task_id)
|
|
261
|
+
if not task:
|
|
262
|
+
raise ValueError(f"Task {task_id} not found")
|
|
263
|
+
|
|
264
|
+
if task.status == TaskStatus.FAILED:
|
|
265
|
+
raise RuntimeError(f"Task failed: {task.error_message}")
|
|
266
|
+
|
|
267
|
+
if task.status != TaskStatus.COMPLETED:
|
|
268
|
+
raise RuntimeError(f"Task not completed, current status: {task.status.value}")
|
|
269
|
+
|
|
270
|
+
if task.result_hex:
|
|
271
|
+
# Import deserialize function
|
|
272
|
+
from .utils import deserialize_args
|
|
273
|
+
|
|
274
|
+
# For consistency, we should actually use a deserialize_result function
|
|
275
|
+
# but for now we'll use the existing deserialize_args
|
|
276
|
+
try:
|
|
277
|
+
import torch
|
|
278
|
+
result_bytes = bytes.fromhex(task.result_hex)
|
|
279
|
+
return torch.multiprocessing.reducer.ForkingPickler.loads(result_bytes)
|
|
280
|
+
except ImportError:
|
|
281
|
+
return pickle.loads(bytes.fromhex(task.result_hex))
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def _worker_loop(self):
|
|
285
|
+
"""Main worker loop for processing tasks"""
|
|
286
|
+
while self._running:
|
|
287
|
+
try:
|
|
288
|
+
# Get next task with timeout
|
|
289
|
+
try:
|
|
290
|
+
task_id = self._task_queue.get(timeout=1)
|
|
291
|
+
except queue.Empty:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
task = self.get_task(task_id)
|
|
295
|
+
if not task or task.status != TaskStatus.PENDING:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Submit to executor
|
|
299
|
+
self._executor.submit_task(task, self)
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.error(f"Error in worker loop: {e}")
|
|
303
|
+
|
|
304
|
+
def _cleanup_loop(self):
|
|
305
|
+
"""Cleanup old completed tasks"""
|
|
306
|
+
while self._running:
|
|
307
|
+
try:
|
|
308
|
+
time.sleep(self.cleanup_interval)
|
|
309
|
+
|
|
310
|
+
current_time = time.time()
|
|
311
|
+
to_remove = []
|
|
312
|
+
|
|
313
|
+
with self._lock:
|
|
314
|
+
for task_id, task in self._tasks.items():
|
|
315
|
+
# Remove completed tasks older than 1 hour
|
|
316
|
+
if (task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]
|
|
317
|
+
and task.completed_at
|
|
318
|
+
and current_time - task.completed_at > 3600):
|
|
319
|
+
to_remove.append(task_id)
|
|
320
|
+
|
|
321
|
+
# Also remove if we have too many tasks
|
|
322
|
+
if len(self._tasks) > self.max_tasks:
|
|
323
|
+
# Remove oldest completed tasks
|
|
324
|
+
completed_tasks = [(tid, task) for tid, task in self._tasks.items()
|
|
325
|
+
if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]]
|
|
326
|
+
completed_tasks.sort(key=lambda x: x[1].completed_at or 0)
|
|
327
|
+
|
|
328
|
+
excess_count = len(self._tasks) - self.max_tasks
|
|
329
|
+
for tid, _ in completed_tasks[:excess_count]:
|
|
330
|
+
to_remove.append(tid)
|
|
331
|
+
|
|
332
|
+
# Remove tasks
|
|
333
|
+
for task_id in to_remove:
|
|
334
|
+
self._tasks.pop(task_id, None)
|
|
335
|
+
|
|
336
|
+
if to_remove:
|
|
337
|
+
logger.info(f"Cleaned up {len(to_remove)} old tasks")
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Error in cleanup loop: {e}")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Global task manager instance
|
|
344
|
+
_task_manager: Optional[TaskManager] = None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def get_task_manager() -> TaskManager:
|
|
348
|
+
"""Get the global task manager instance"""
|
|
349
|
+
global _task_manager
|
|
350
|
+
if _task_manager is None:
|
|
351
|
+
_task_manager = TaskManager()
|
|
352
|
+
_task_manager.start()
|
|
353
|
+
return _task_manager
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def shutdown_task_manager():
|
|
357
|
+
"""Shutdown the global task manager"""
|
|
358
|
+
global _task_manager
|
|
359
|
+
if _task_manager:
|
|
360
|
+
_task_manager.stop()
|
|
361
|
+
_task_manager = None
|
shared_tensor/client.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared Tensor JSON-RPC Client
|
|
3
|
+
|
|
4
|
+
Handles communication with remote shared tensor servers using JSON-RPC 2.0 protocol.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pickle
|
|
8
|
+
import requests
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
from shared_tensor.errors import SharedTensorClientError, SharedTensorServerError
|
|
15
|
+
from shared_tensor.jsonrpc import (
|
|
16
|
+
JsonRpcRequest,
|
|
17
|
+
JsonRpcResponse,
|
|
18
|
+
JsonRpcErrorCodes,
|
|
19
|
+
parse_response,
|
|
20
|
+
)
|
|
21
|
+
from shared_tensor.utils import serialize_result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["SharedTensorClient", "execute_remote_function"]
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SharedTensorClient:
|
|
30
|
+
"""
|
|
31
|
+
JSON-RPC client for shared tensor communication
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, server_port: int = 2537, timeout: int = 30, verbose_debug: bool = False):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the client
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
server_port: PORT of the shared tensor server
|
|
40
|
+
timeout: Request timeout in seconds, critical when sync provider used
|
|
41
|
+
verbose_debug: Whether to log detailed debug messages
|
|
42
|
+
"""
|
|
43
|
+
self.server_url = f"http://localhost:{server_port}"
|
|
44
|
+
self.verbose_debug = verbose_debug
|
|
45
|
+
self.timeout = timeout
|
|
46
|
+
|
|
47
|
+
logger.debug(f"SharedTensorClient initialized with server URL: {self.server_url} with timeout {self.timeout} and verbose_debug {self.verbose_debug}")
|
|
48
|
+
|
|
49
|
+
self.session = requests.Session()
|
|
50
|
+
|
|
51
|
+
self.session.headers.update({
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'User-Agent': 'SharedTensorClient/1.0'
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
def _send_request(self, request: JsonRpcRequest) -> JsonRpcResponse:
|
|
57
|
+
"""
|
|
58
|
+
Send a JSON-RPC request to the server
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
request: JsonRpcRequest object
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
JsonRpcResponse object
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
SharedTensorClientError: If unable to connect to server
|
|
68
|
+
SharedTensorClientError: If request times out
|
|
69
|
+
SharedTensorClientError: If server response is invalid
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
if self.verbose_debug:
|
|
73
|
+
logger.debug(f"Sending request to server {self.server_url}: {request.to_json()}")
|
|
74
|
+
else:
|
|
75
|
+
logger.debug(f"Sending request to server {self.server_url}")
|
|
76
|
+
|
|
77
|
+
response = self.session.post(
|
|
78
|
+
f"{self.server_url}/jsonrpc",
|
|
79
|
+
data=request.to_json(),
|
|
80
|
+
timeout=self.timeout
|
|
81
|
+
)
|
|
82
|
+
if self.verbose_debug:
|
|
83
|
+
logger.debug(f"Received response from server {self.server_url}: {response.text}")
|
|
84
|
+
else:
|
|
85
|
+
logger.debug(f"Received response from server {self.server_url}")
|
|
86
|
+
|
|
87
|
+
if response.status_code != 200:
|
|
88
|
+
logger.warning(f"Server {self.server_url} returned HTTP {response.status_code}: {response.text}")
|
|
89
|
+
raise SharedTensorClientError(
|
|
90
|
+
f"Server {self.server_url} returned HTTP {response.status_code}: {response.text}"
|
|
91
|
+
)
|
|
92
|
+
try:
|
|
93
|
+
rpc_response = parse_response(response.text)
|
|
94
|
+
if self.verbose_debug:
|
|
95
|
+
logger.debug(f"Parsed JSON-RPC response: {rpc_response}")
|
|
96
|
+
else:
|
|
97
|
+
logger.debug(f"Parsed JSON-RPC response from server {self.server_url}")
|
|
98
|
+
return rpc_response
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
logger.warning(f"Invalid JSON-RPC response: {e}")
|
|
101
|
+
raise ValueError(f"Invalid JSON-RPC response: {e}")
|
|
102
|
+
|
|
103
|
+
except requests.exceptions.ConnectionError as e:
|
|
104
|
+
logger.warning(f"Unable to connect to server {self.server_url}: {e}")
|
|
105
|
+
raise SharedTensorClientError(f"Unable to connect to server {self.server_url}: {e}")
|
|
106
|
+
except requests.exceptions.Timeout as e:
|
|
107
|
+
logger.warning(f"Request to server {self.server_url} timed out after {self.timeout} seconds: {e}")
|
|
108
|
+
raise SharedTensorClientError(f"Request to server {self.server_url} timed out after {self.timeout} seconds: {e}")
|
|
109
|
+
except requests.exceptions.RequestException as e:
|
|
110
|
+
logger.warning(f"Request to server {self.server_url} failed: {e}")
|
|
111
|
+
raise SharedTensorClientError(f"Request to server {self.server_url} failed: {e}")
|
|
112
|
+
|
|
113
|
+
def _create_request(self, method: str, params: Optional[Dict[str, Any]] = None) -> JsonRpcRequest:
|
|
114
|
+
"""Create a JSON-RPC request"""
|
|
115
|
+
return JsonRpcRequest(method=method, params=params)
|
|
116
|
+
|
|
117
|
+
def execute_function(
|
|
118
|
+
self,
|
|
119
|
+
function_path: str,
|
|
120
|
+
args: tuple = (),
|
|
121
|
+
kwargs: Dict[str, Any] = None,
|
|
122
|
+
options: Dict[str, Any] = None
|
|
123
|
+
) -> Any:
|
|
124
|
+
"""
|
|
125
|
+
Execute a function on the remote server
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
function_path: Function path in format "module.submodule:function_name"
|
|
129
|
+
args: Positional arguments for the function
|
|
130
|
+
kwargs: Keyword arguments for the function
|
|
131
|
+
options: Options for the function
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The result returned by the remote function
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
SharedTensorClientError: If unable to communicate with server
|
|
138
|
+
SharedTensorServerError: If remote execution fails
|
|
139
|
+
"""
|
|
140
|
+
if kwargs is None:
|
|
141
|
+
kwargs = {}
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
serialized_args = serialize_result(args).hex()
|
|
145
|
+
serialized_kwargs = serialize_result(kwargs).hex()
|
|
146
|
+
|
|
147
|
+
request = JsonRpcRequest(
|
|
148
|
+
method="execute_function",
|
|
149
|
+
params={
|
|
150
|
+
"function_path": function_path,
|
|
151
|
+
"args": serialized_args,
|
|
152
|
+
"kwargs": serialized_kwargs,
|
|
153
|
+
"options": options,
|
|
154
|
+
"encoding": "pickle_hex"
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
response = self._send_request(request)
|
|
159
|
+
|
|
160
|
+
if response.error:
|
|
161
|
+
error_code = response.error.get('code', JsonRpcErrorCodes.INTERNAL_ERROR)
|
|
162
|
+
error_message = response.error.get('message', 'Unknown error')
|
|
163
|
+
error_data = response.error.get('data')
|
|
164
|
+
|
|
165
|
+
raise SharedTensorServerError(
|
|
166
|
+
f"Remote execution failed [{error_code}]: {error_message}"
|
|
167
|
+
+ (f" - {error_data}" if error_data else "")
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if response.result is None:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
result_data = response.result
|
|
174
|
+
if not isinstance(result_data, dict) or 'handler' not in result_data:
|
|
175
|
+
raise SharedTensorClientError("Invalid response format: missing 'handler' field")
|
|
176
|
+
|
|
177
|
+
handler_hex = result_data['handler']
|
|
178
|
+
handler_bytes = bytes.fromhex(handler_hex)
|
|
179
|
+
return torch.multiprocessing.reducer.ForkingPickler.loads(handler_bytes)
|
|
180
|
+
|
|
181
|
+
except (pickle.PickleError, ValueError) as e:
|
|
182
|
+
raise SharedTensorClientError(f"Serialization error: {e}")
|
|
183
|
+
|
|
184
|
+
def ping(self) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Ping the server to check if it's alive
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if server is responding, False otherwise
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
request = JsonRpcRequest(method="ping")
|
|
193
|
+
response = self._send_request(request)
|
|
194
|
+
return response.error is None
|
|
195
|
+
except Exception:
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def get_server_info(self) -> Dict[str, Any]:
|
|
199
|
+
"""
|
|
200
|
+
Get information about the server
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Dictionary containing server information
|
|
204
|
+
"""
|
|
205
|
+
request = JsonRpcRequest(method="get_server_info")
|
|
206
|
+
response = self._send_request(request)
|
|
207
|
+
|
|
208
|
+
if response.error:
|
|
209
|
+
raise SharedTensorServerError(f"Failed to get server info: {response.error}")
|
|
210
|
+
|
|
211
|
+
return response.result or {}
|
|
212
|
+
|
|
213
|
+
def list_functions(self) -> Dict[str, str]:
|
|
214
|
+
"""
|
|
215
|
+
List all available functions on the server
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Dictionary mapping function names to their paths
|
|
219
|
+
"""
|
|
220
|
+
request = JsonRpcRequest(method="list_functions")
|
|
221
|
+
response = self._send_request(request)
|
|
222
|
+
|
|
223
|
+
if response.error:
|
|
224
|
+
raise SharedTensorServerError(f"Failed to list functions: {response.error}")
|
|
225
|
+
|
|
226
|
+
return response.result or {}
|
|
227
|
+
|
|
228
|
+
def close(self):
|
|
229
|
+
"""Close the client session"""
|
|
230
|
+
self.session.close()
|
|
231
|
+
|
|
232
|
+
def __enter__(self):
|
|
233
|
+
return self
|
|
234
|
+
|
|
235
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
236
|
+
self.close()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def execute_remote_function(
|
|
240
|
+
function_path: str,
|
|
241
|
+
args: tuple = (),
|
|
242
|
+
kwargs: Dict[str, Any] = None,
|
|
243
|
+
server_port: int = 2537,
|
|
244
|
+
timeout: int = 30,
|
|
245
|
+
verbose_debug: bool = False
|
|
246
|
+
) -> Any:
|
|
247
|
+
"""
|
|
248
|
+
Convenience function to execute a remote function
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
function_path: Function path in format "module.submodule:function_name"
|
|
252
|
+
args: Positional arguments
|
|
253
|
+
kwargs: Keyword arguments
|
|
254
|
+
server_port: Server port
|
|
255
|
+
timeout: Request timeout in seconds
|
|
256
|
+
verbose_debug: Whether to log detailed debug messages
|
|
257
|
+
Returns:
|
|
258
|
+
Function execution result
|
|
259
|
+
"""
|
|
260
|
+
if verbose_debug:
|
|
261
|
+
logger.debug(f"Executing remote function {function_path} with args {args} and kwargs {kwargs} on server {server_port} with timeout {timeout} and verbose_debug {verbose_debug}")
|
|
262
|
+
else:
|
|
263
|
+
logger.debug(f"Executing remote function {function_path} on server {server_port} with timeout {timeout}")
|
|
264
|
+
with SharedTensorClient(server_port=server_port, timeout=timeout, verbose_debug=verbose_debug) as client:
|
|
265
|
+
return client.execute_function(function_path, args, kwargs)
|
shared_tensor/errors.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
__all__ = ["SharedTensorError", "SharedTensorServerError", "SharedTensorClientError", "SharedTensorProviderError"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SharedTensorError(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class SharedTensorServerError(SharedTensorError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class SharedTensorClientError(SharedTensorError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class SharedTensorProviderError(SharedTensorError):
|
|
16
|
+
pass
|