wandb 0.19.6rc4__py3-none-win_amd64.whl → 0.19.8__py3-none-win_amd64.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.
- wandb/__init__.py +1 -1
- wandb/__init__.pyi +56 -6
- wandb/apis/public/_generated/__init__.py +21 -0
- wandb/apis/public/_generated/base.py +128 -0
- wandb/apis/public/_generated/enums.py +4 -0
- wandb/apis/public/_generated/input_types.py +4 -0
- wandb/apis/public/_generated/operations.py +15 -0
- wandb/apis/public/_generated/server_features_query.py +27 -0
- wandb/apis/public/_generated/typing_compat.py +14 -0
- wandb/apis/public/api.py +192 -6
- wandb/apis/public/artifacts.py +13 -45
- wandb/apis/public/registries.py +573 -0
- wandb/apis/public/utils.py +36 -0
- wandb/bin/gpu_stats.exe +0 -0
- wandb/bin/wandb-core +0 -0
- wandb/cli/cli.py +11 -20
- wandb/data_types.py +1 -1
- wandb/env.py +10 -0
- wandb/filesync/dir_watcher.py +2 -1
- wandb/proto/v3/wandb_internal_pb2.py +243 -222
- wandb/proto/v3/wandb_server_pb2.py +4 -4
- wandb/proto/v3/wandb_settings_pb2.py +2 -2
- wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v4/wandb_internal_pb2.py +226 -222
- wandb/proto/v4/wandb_server_pb2.py +4 -4
- wandb/proto/v4/wandb_settings_pb2.py +2 -2
- wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v5/wandb_internal_pb2.py +226 -222
- wandb/proto/v5/wandb_server_pb2.py +4 -4
- wandb/proto/v5/wandb_settings_pb2.py +2 -2
- wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
- wandb/sdk/artifacts/_graphql_fragments.py +126 -0
- wandb/sdk/artifacts/artifact.py +51 -95
- wandb/sdk/backend/backend.py +17 -6
- wandb/sdk/data_types/helper_types/bounding_boxes_2d.py +14 -6
- wandb/sdk/data_types/helper_types/image_mask.py +12 -6
- wandb/sdk/data_types/saved_model.py +35 -46
- wandb/sdk/data_types/video.py +7 -16
- wandb/sdk/interface/interface.py +87 -49
- wandb/sdk/interface/interface_queue.py +5 -15
- wandb/sdk/interface/interface_relay.py +7 -22
- wandb/sdk/interface/interface_shared.py +65 -136
- wandb/sdk/interface/interface_sock.py +3 -21
- wandb/sdk/interface/router.py +42 -68
- wandb/sdk/interface/router_queue.py +13 -11
- wandb/sdk/interface/router_relay.py +26 -13
- wandb/sdk/interface/router_sock.py +12 -16
- wandb/sdk/internal/handler.py +4 -3
- wandb/sdk/internal/internal_api.py +12 -1
- wandb/sdk/internal/sender.py +3 -19
- wandb/sdk/lib/apikey.py +87 -26
- wandb/sdk/lib/asyncio_compat.py +210 -0
- wandb/sdk/lib/console_capture.py +172 -0
- wandb/sdk/lib/progress.py +78 -16
- wandb/sdk/lib/redirect.py +102 -76
- wandb/sdk/lib/service_connection.py +37 -17
- wandb/sdk/lib/sock_client.py +6 -56
- wandb/sdk/mailbox/__init__.py +23 -0
- wandb/sdk/mailbox/mailbox.py +135 -0
- wandb/sdk/mailbox/mailbox_handle.py +127 -0
- wandb/sdk/mailbox/response_handle.py +167 -0
- wandb/sdk/mailbox/wait_with_progress.py +135 -0
- wandb/sdk/service/server_sock.py +9 -3
- wandb/sdk/service/streams.py +75 -78
- wandb/sdk/verify/verify.py +54 -2
- wandb/sdk/wandb_init.py +72 -75
- wandb/sdk/wandb_login.py +7 -4
- wandb/sdk/wandb_metadata.py +65 -34
- wandb/sdk/wandb_require.py +14 -8
- wandb/sdk/wandb_run.py +90 -97
- wandb/sdk/wandb_settings.py +10 -4
- wandb/sdk/wandb_setup.py +19 -8
- wandb/sdk/wandb_sync.py +2 -10
- wandb/util.py +3 -1
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/METADATA +2 -2
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/RECORD +79 -66
- wandb/sdk/interface/message_future.py +0 -27
- wandb/sdk/interface/message_future_poll.py +0 -50
- wandb/sdk/lib/mailbox.py +0 -442
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/WHEEL +0 -0
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/entry_points.txt +0 -0
- {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
"""A message protocol for the internal service process.
|
2
|
+
|
3
|
+
The core of W&B is implemented by a side process that asynchronously uploads
|
4
|
+
data. The client process (such as this Python code) sends requests to the
|
5
|
+
service, and for some requests, the service eventually sends a response.
|
6
|
+
|
7
|
+
The client can send multiple requests before the service provides a response.
|
8
|
+
The Mailbox handles matching responses to requests. An internal thread
|
9
|
+
continuously reads data from the service and passes it to the mailbox.
|
10
|
+
"""
|
11
|
+
|
12
|
+
from .mailbox import Mailbox, MailboxClosedError
|
13
|
+
from .mailbox_handle import HandleAbandonedError, MailboxHandle
|
14
|
+
from .wait_with_progress import wait_all_with_progress, wait_with_progress
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
"Mailbox",
|
18
|
+
"MailboxClosedError",
|
19
|
+
"HandleAbandonedError",
|
20
|
+
"MailboxHandle",
|
21
|
+
"wait_all_with_progress",
|
22
|
+
"wait_with_progress",
|
23
|
+
]
|
@@ -0,0 +1,135 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import secrets
|
5
|
+
import string
|
6
|
+
import threading
|
7
|
+
|
8
|
+
from wandb.proto import wandb_internal_pb2 as pb
|
9
|
+
from wandb.proto import wandb_server_pb2 as spb
|
10
|
+
|
11
|
+
from .mailbox_handle import MailboxHandle
|
12
|
+
from .response_handle import MailboxResponseHandle
|
13
|
+
|
14
|
+
_logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class MailboxClosedError(Exception):
|
18
|
+
"""The mailbox has been closed and cannot be used."""
|
19
|
+
|
20
|
+
|
21
|
+
class Mailbox:
|
22
|
+
"""Matches service responses to requests.
|
23
|
+
|
24
|
+
The mailbox can set an address on a server request and create a handle for
|
25
|
+
waiting for a response to that record. Responses are delivered by calling
|
26
|
+
`deliver()`. The `close()` method abandons all handles in case the
|
27
|
+
service process becomes unreachable.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self) -> None:
|
31
|
+
self._handles: dict[str, MailboxResponseHandle] = {}
|
32
|
+
self._handles_lock = threading.Lock()
|
33
|
+
self._closed = False
|
34
|
+
|
35
|
+
def require_response(
|
36
|
+
self,
|
37
|
+
request: spb.ServerRequest | pb.Record,
|
38
|
+
) -> MailboxHandle[spb.ServerResponse]:
|
39
|
+
"""Set a response address on a request.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
request: The request on which to set a request ID or mailbox slot.
|
43
|
+
This is mutated. An address must not already be set.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
A handle for waiting for the response to the request.
|
47
|
+
|
48
|
+
Raises:
|
49
|
+
MailboxClosedError: If the mailbox has been closed, in which case
|
50
|
+
no new responses are expected to be delivered and new handles
|
51
|
+
cannot be created.
|
52
|
+
"""
|
53
|
+
if isinstance(request, spb.ServerRequest):
|
54
|
+
if address := request.request_id:
|
55
|
+
raise ValueError(f"Request already has an address ({address})")
|
56
|
+
|
57
|
+
address = self._new_address()
|
58
|
+
request.request_id = address
|
59
|
+
else:
|
60
|
+
if address := request.control.mailbox_slot:
|
61
|
+
raise ValueError(f"Request already has an address ({address})")
|
62
|
+
|
63
|
+
address = self._new_address()
|
64
|
+
request.control.mailbox_slot = address
|
65
|
+
|
66
|
+
with self._handles_lock:
|
67
|
+
if self._closed:
|
68
|
+
raise MailboxClosedError()
|
69
|
+
|
70
|
+
handle = MailboxResponseHandle(address)
|
71
|
+
self._handles[address] = handle
|
72
|
+
|
73
|
+
return handle
|
74
|
+
|
75
|
+
def _new_address(self) -> str:
|
76
|
+
"""Returns an unused address for a request.
|
77
|
+
|
78
|
+
Assumes `_handles_lock` is held.
|
79
|
+
"""
|
80
|
+
|
81
|
+
def generate():
|
82
|
+
return "".join(
|
83
|
+
secrets.choice(string.ascii_lowercase + string.digits)
|
84
|
+
for i in range(12)
|
85
|
+
)
|
86
|
+
|
87
|
+
address = generate()
|
88
|
+
|
89
|
+
# Being extra cautious. This loop will almost never be entered.
|
90
|
+
while address in self._handles:
|
91
|
+
address = generate()
|
92
|
+
|
93
|
+
return address
|
94
|
+
|
95
|
+
def deliver(self, response: spb.ServerResponse) -> None:
|
96
|
+
"""Deliver a response from the service.
|
97
|
+
|
98
|
+
If the response address is invalid, this does nothing.
|
99
|
+
It is a no-op if the mailbox has been closed.
|
100
|
+
"""
|
101
|
+
address = response.request_id
|
102
|
+
if not address:
|
103
|
+
kind: str | None = response.WhichOneof("server_response_type")
|
104
|
+
if kind == "result_communicate":
|
105
|
+
result_type = response.result_communicate.WhichOneof("result_type")
|
106
|
+
kind = f"result_communicate.{result_type}"
|
107
|
+
|
108
|
+
_logger.error(f"Received response with no mailbox slot: {kind}")
|
109
|
+
return
|
110
|
+
|
111
|
+
with self._handles_lock:
|
112
|
+
# NOTE: If the mailbox is closed, this returns None because
|
113
|
+
# we clear the dict.
|
114
|
+
handle = self._handles.pop(address, None)
|
115
|
+
|
116
|
+
# It is not an error if there is no handle for the address:
|
117
|
+
# handles can be abandoned if the result is no longer needed.
|
118
|
+
if handle:
|
119
|
+
handle.deliver(response)
|
120
|
+
|
121
|
+
def close(self) -> None:
|
122
|
+
"""Indicate no further responses will be delivered.
|
123
|
+
|
124
|
+
Abandons all handles.
|
125
|
+
"""
|
126
|
+
with self._handles_lock:
|
127
|
+
self._closed = True
|
128
|
+
|
129
|
+
_logger.info(
|
130
|
+
f"Closing mailbox, abandoning {len(self._handles)} handles.",
|
131
|
+
)
|
132
|
+
|
133
|
+
for handle in self._handles.values():
|
134
|
+
handle.abandon()
|
135
|
+
self._handles.clear()
|
@@ -0,0 +1,127 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import abc
|
4
|
+
import sys
|
5
|
+
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
|
6
|
+
|
7
|
+
# Necessary to break an import loop.
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from wandb.sdk.interface import interface
|
10
|
+
|
11
|
+
if sys.version_info >= (3, 12):
|
12
|
+
from typing import override
|
13
|
+
else:
|
14
|
+
from typing_extensions import override
|
15
|
+
|
16
|
+
|
17
|
+
_T = TypeVar("_T")
|
18
|
+
_S = TypeVar("_S")
|
19
|
+
|
20
|
+
|
21
|
+
class HandleAbandonedError(Exception):
|
22
|
+
"""The handle has no response and has been abandoned."""
|
23
|
+
|
24
|
+
|
25
|
+
class MailboxHandle(abc.ABC, Generic[_T]):
|
26
|
+
"""A thread-safe handle that allows waiting for a response to a request."""
|
27
|
+
|
28
|
+
def map(self, fn: Callable[[_T], _S]) -> MailboxHandle[_S]:
|
29
|
+
"""Returns a transformed handle.
|
30
|
+
|
31
|
+
Methods on the returned handle call methods on this handle, but the
|
32
|
+
response type is derived using the given function.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
fn: A function to apply to this handle's result to get the new
|
36
|
+
handle's result. The function should be pure and fast.
|
37
|
+
"""
|
38
|
+
return _MailboxMappedHandle(self, fn)
|
39
|
+
|
40
|
+
@abc.abstractmethod
|
41
|
+
def abandon(self) -> None:
|
42
|
+
"""Abandon the handle, indicating it will not receive a response."""
|
43
|
+
|
44
|
+
@abc.abstractmethod
|
45
|
+
def cancel(self, iface: interface.InterfaceBase) -> None:
|
46
|
+
"""Cancel the handle, requesting any associated work to not complete.
|
47
|
+
|
48
|
+
This automatically abandons the handle, as a response is no longer
|
49
|
+
guaranteed.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
iface: The interface on which to publish the cancel request.
|
53
|
+
"""
|
54
|
+
|
55
|
+
@abc.abstractmethod
|
56
|
+
def check(self) -> _T | None:
|
57
|
+
"""Returns the response if it's ready."""
|
58
|
+
|
59
|
+
@abc.abstractmethod
|
60
|
+
def wait_or(self, *, timeout: float | None) -> _T:
|
61
|
+
"""Wait for a response or a timeout.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
timeout: A finite number of seconds or None to never time out.
|
65
|
+
If less than or equal to zero, times out immediately unless
|
66
|
+
the response is available.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
The response if it arrives before the timeout or has already arrived.
|
70
|
+
|
71
|
+
Raises:
|
72
|
+
TimeoutError: If the timeout is reached.
|
73
|
+
HandleAbandonedError: If the handle becomes abandoned.
|
74
|
+
"""
|
75
|
+
|
76
|
+
@abc.abstractmethod
|
77
|
+
async def wait_async(self, *, timeout: float | None) -> _T:
|
78
|
+
"""Wait for a response or timeout.
|
79
|
+
|
80
|
+
This must run in an `asyncio` event loop.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
timeout: A finite number of seconds or None to never time out.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
The response if it arrives before the timeout or has already arrived.
|
87
|
+
|
88
|
+
Raises:
|
89
|
+
TimeoutError: If the timeout is reached.
|
90
|
+
HandleAbandonedError: If the handle becomes abandoned.
|
91
|
+
"""
|
92
|
+
|
93
|
+
|
94
|
+
class _MailboxMappedHandle(Generic[_S], MailboxHandle[_S]):
|
95
|
+
"""A mailbox handle whose result is derived from another handle."""
|
96
|
+
|
97
|
+
def __init__(
|
98
|
+
self,
|
99
|
+
handle: MailboxHandle[_T],
|
100
|
+
fn: Callable[[_T], _S],
|
101
|
+
) -> None:
|
102
|
+
self._handle = handle
|
103
|
+
self._fn = fn
|
104
|
+
|
105
|
+
@override
|
106
|
+
def abandon(self) -> None:
|
107
|
+
self._handle.abandon()
|
108
|
+
|
109
|
+
@override
|
110
|
+
def cancel(self, iface: interface.InterfaceBase) -> None:
|
111
|
+
self._handle.cancel(iface)
|
112
|
+
|
113
|
+
@override
|
114
|
+
def check(self) -> _S | None:
|
115
|
+
if response := self._handle.check():
|
116
|
+
return self._fn(response)
|
117
|
+
else:
|
118
|
+
return None
|
119
|
+
|
120
|
+
@override
|
121
|
+
def wait_or(self, *, timeout: float | None) -> _S:
|
122
|
+
return self._fn(self._handle.wait_or(timeout=timeout))
|
123
|
+
|
124
|
+
@override
|
125
|
+
async def wait_async(self, *, timeout: float | None) -> _S:
|
126
|
+
response = await self._handle.wait_async(timeout=timeout)
|
127
|
+
return self._fn(response)
|
@@ -0,0 +1,167 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import math
|
5
|
+
import sys
|
6
|
+
import threading
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
from wandb.proto import wandb_server_pb2 as spb
|
10
|
+
|
11
|
+
from .mailbox_handle import HandleAbandonedError, MailboxHandle
|
12
|
+
|
13
|
+
# Necessary to break an import loop.
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from wandb.sdk.interface import interface
|
16
|
+
|
17
|
+
if sys.version_info >= (3, 12):
|
18
|
+
from typing import override
|
19
|
+
else:
|
20
|
+
from typing_extensions import override
|
21
|
+
|
22
|
+
|
23
|
+
class MailboxResponseHandle(MailboxHandle[spb.ServerResponse]):
|
24
|
+
"""A general handle for any ServerResponse."""
|
25
|
+
|
26
|
+
def __init__(self, address: str) -> None:
|
27
|
+
self._address = address
|
28
|
+
self._lock = threading.Lock()
|
29
|
+
self._event = threading.Event()
|
30
|
+
|
31
|
+
self._abandoned = False
|
32
|
+
self._response: spb.ServerResponse | None = None
|
33
|
+
|
34
|
+
self._asyncio_events: dict[asyncio.Event, _AsyncioEvent] = dict()
|
35
|
+
|
36
|
+
def deliver(self, response: spb.ServerResponse) -> None:
|
37
|
+
"""Deliver the response.
|
38
|
+
|
39
|
+
This may only be called once. It is an error to respond to the same
|
40
|
+
request more than once. It is a no-op if the handle has been abandoned.
|
41
|
+
"""
|
42
|
+
with self._lock:
|
43
|
+
if self._abandoned:
|
44
|
+
return
|
45
|
+
|
46
|
+
if self._response:
|
47
|
+
raise ValueError(
|
48
|
+
f"A response has already been delivered to {self._address}."
|
49
|
+
)
|
50
|
+
|
51
|
+
self._response = response
|
52
|
+
self._signal_done()
|
53
|
+
|
54
|
+
@override
|
55
|
+
def cancel(self, iface: interface.InterfaceBase) -> None:
|
56
|
+
iface.publish_cancel(self._address)
|
57
|
+
self.abandon()
|
58
|
+
|
59
|
+
@override
|
60
|
+
def abandon(self) -> None:
|
61
|
+
with self._lock:
|
62
|
+
self._abandoned = True
|
63
|
+
self._signal_done()
|
64
|
+
|
65
|
+
def _signal_done(self) -> None:
|
66
|
+
"""Indicate that the handle either got a response or became abandoned.
|
67
|
+
|
68
|
+
The lock must be held.
|
69
|
+
"""
|
70
|
+
# Unblock threads blocked on `wait_or`.
|
71
|
+
self._event.set()
|
72
|
+
|
73
|
+
# Unblock asyncio loops blocked on `wait_async`.
|
74
|
+
for asyncio_event in self._asyncio_events.values():
|
75
|
+
asyncio_event.set_threadsafe()
|
76
|
+
self._asyncio_events.clear()
|
77
|
+
|
78
|
+
@override
|
79
|
+
def check(self) -> spb.ServerResponse | None:
|
80
|
+
with self._lock:
|
81
|
+
return self._response
|
82
|
+
|
83
|
+
@override
|
84
|
+
def wait_or(self, *, timeout: float | None) -> spb.ServerResponse:
|
85
|
+
if timeout is not None and not math.isfinite(timeout):
|
86
|
+
raise ValueError("Timeout must be finite or None.")
|
87
|
+
|
88
|
+
if not self._event.wait(timeout=timeout):
|
89
|
+
raise TimeoutError(
|
90
|
+
f"Timed out waiting for response on {self._address}",
|
91
|
+
)
|
92
|
+
|
93
|
+
with self._lock:
|
94
|
+
if self._response:
|
95
|
+
return self._response
|
96
|
+
|
97
|
+
assert self._abandoned
|
98
|
+
raise HandleAbandonedError()
|
99
|
+
|
100
|
+
@override
|
101
|
+
async def wait_async(self, *, timeout: float | None) -> spb.ServerResponse:
|
102
|
+
if timeout is not None and not math.isfinite(timeout):
|
103
|
+
raise ValueError("Timeout must be finite or None.")
|
104
|
+
|
105
|
+
evt = asyncio.Event()
|
106
|
+
self._add_asyncio_event(asyncio.get_event_loop(), evt)
|
107
|
+
|
108
|
+
try:
|
109
|
+
await asyncio.wait_for(evt.wait(), timeout=timeout)
|
110
|
+
|
111
|
+
except (asyncio.TimeoutError, TimeoutError) as e:
|
112
|
+
with self._lock:
|
113
|
+
if self._response:
|
114
|
+
return self._response
|
115
|
+
elif self._abandoned:
|
116
|
+
raise HandleAbandonedError()
|
117
|
+
else:
|
118
|
+
raise TimeoutError(
|
119
|
+
f"Timed out waiting for response on {self._address}"
|
120
|
+
) from e
|
121
|
+
|
122
|
+
else:
|
123
|
+
with self._lock:
|
124
|
+
if self._response:
|
125
|
+
return self._response
|
126
|
+
|
127
|
+
assert self._abandoned
|
128
|
+
raise HandleAbandonedError()
|
129
|
+
|
130
|
+
finally:
|
131
|
+
self._forget_asyncio_event(evt)
|
132
|
+
|
133
|
+
def _add_asyncio_event(
|
134
|
+
self,
|
135
|
+
loop: asyncio.AbstractEventLoop,
|
136
|
+
event: asyncio.Event,
|
137
|
+
) -> None:
|
138
|
+
"""Add an event to signal when a response is received.
|
139
|
+
|
140
|
+
If a response already exists, this notifies the event loop immediately.
|
141
|
+
"""
|
142
|
+
asyncio_event = _AsyncioEvent(loop, event)
|
143
|
+
|
144
|
+
with self._lock:
|
145
|
+
if self._response or self._abandoned:
|
146
|
+
asyncio_event.set_threadsafe()
|
147
|
+
else:
|
148
|
+
self._asyncio_events[event] = asyncio_event
|
149
|
+
|
150
|
+
def _forget_asyncio_event(self, event: asyncio.Event) -> None:
|
151
|
+
"""Cancel signalling an event when a response is received."""
|
152
|
+
with self._lock:
|
153
|
+
self._asyncio_events.pop(event, None)
|
154
|
+
|
155
|
+
|
156
|
+
class _AsyncioEvent:
|
157
|
+
def __init__(
|
158
|
+
self,
|
159
|
+
loop: asyncio.AbstractEventLoop,
|
160
|
+
event: asyncio.Event,
|
161
|
+
):
|
162
|
+
self._loop = loop
|
163
|
+
self._event = event
|
164
|
+
|
165
|
+
def set_threadsafe(self) -> None:
|
166
|
+
"""Set the asyncio event in its own loop."""
|
167
|
+
self._loop.call_soon_threadsafe(self._event.set)
|
@@ -0,0 +1,135 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
from typing import Any, Callable, Coroutine, List, TypeVar, cast
|
5
|
+
|
6
|
+
from wandb.sdk.lib import asyncio_compat
|
7
|
+
|
8
|
+
from .mailbox_handle import MailboxHandle
|
9
|
+
|
10
|
+
_T = TypeVar("_T")
|
11
|
+
|
12
|
+
|
13
|
+
def wait_with_progress(
|
14
|
+
handle: MailboxHandle[_T],
|
15
|
+
*,
|
16
|
+
timeout: float | None,
|
17
|
+
progress_after: float,
|
18
|
+
display_progress: Callable[[], Coroutine[Any, Any, None]],
|
19
|
+
) -> _T:
|
20
|
+
"""Wait for a handle, possibly displaying progress to the user.
|
21
|
+
|
22
|
+
Equivalent to passing a single handle to `wait_all_with_progress`.
|
23
|
+
"""
|
24
|
+
return wait_all_with_progress(
|
25
|
+
[handle],
|
26
|
+
timeout=timeout,
|
27
|
+
progress_after=progress_after,
|
28
|
+
display_progress=display_progress,
|
29
|
+
)[0]
|
30
|
+
|
31
|
+
|
32
|
+
def wait_all_with_progress(
|
33
|
+
handle_list: list[MailboxHandle[_T]],
|
34
|
+
*,
|
35
|
+
timeout: float | None,
|
36
|
+
progress_after: float,
|
37
|
+
display_progress: Callable[[], Coroutine[Any, Any, None]],
|
38
|
+
) -> list[_T]:
|
39
|
+
"""Wait for multiple handles, possibly displaying progress to the user.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
handle_list: The handles to wait for.
|
43
|
+
timeout: A number of seconds after which to raise a TimeoutError,
|
44
|
+
or None if this should never timeout.
|
45
|
+
progress_after: A number of seconds after which to start the
|
46
|
+
display_progress callback. Starting the callback creates a thread
|
47
|
+
and starts an asyncio loop, so we want to avoid doing it if
|
48
|
+
the handle is resolved quickly.
|
49
|
+
display_progress: An asyncio function that displays progress to
|
50
|
+
the user. This function is executed on a new thread and cancelled
|
51
|
+
if the timeout is exceeded.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
A list where the Nth item is the Nth handle's result.
|
55
|
+
|
56
|
+
Raises:
|
57
|
+
TimeoutError: If the overall timeout expires.
|
58
|
+
HandleAbandonedError: If any handle becomes abandoned.
|
59
|
+
Exception: Any exception from the display function is propagated.
|
60
|
+
"""
|
61
|
+
if not handle_list:
|
62
|
+
return []
|
63
|
+
|
64
|
+
if timeout is not None and timeout <= progress_after:
|
65
|
+
return _wait_handles(handle_list, timeout=timeout)
|
66
|
+
|
67
|
+
start_time = time.monotonic()
|
68
|
+
|
69
|
+
try:
|
70
|
+
return _wait_handles(handle_list, timeout=progress_after)
|
71
|
+
except TimeoutError:
|
72
|
+
pass
|
73
|
+
|
74
|
+
async def progress_loop_with_timeout() -> list[_T]:
|
75
|
+
with asyncio_compat.cancel_on_exit(display_progress()):
|
76
|
+
if timeout is not None:
|
77
|
+
elapsed_time = time.monotonic() - start_time
|
78
|
+
remaining_timeout = timeout - elapsed_time
|
79
|
+
else:
|
80
|
+
remaining_timeout = None
|
81
|
+
|
82
|
+
return await _wait_handles_async(
|
83
|
+
handle_list,
|
84
|
+
timeout=remaining_timeout,
|
85
|
+
)
|
86
|
+
|
87
|
+
return asyncio_compat.run(progress_loop_with_timeout)
|
88
|
+
|
89
|
+
|
90
|
+
def _wait_handles(
|
91
|
+
handle_list: list[MailboxHandle[_T]],
|
92
|
+
*,
|
93
|
+
timeout: float,
|
94
|
+
) -> list[_T]:
|
95
|
+
"""Wait for multiple mailbox handles.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Each handle's result, in the same order as the given handles.
|
99
|
+
|
100
|
+
Raises:
|
101
|
+
TimeoutError: If the overall timeout expires.
|
102
|
+
HandleAbandonedError: If any handle becomes abandoned.
|
103
|
+
"""
|
104
|
+
results: list[_T] = []
|
105
|
+
|
106
|
+
start_time = time.monotonic()
|
107
|
+
for handle in handle_list:
|
108
|
+
elapsed_time = time.monotonic() - start_time
|
109
|
+
remaining_timeout = timeout - elapsed_time
|
110
|
+
results.append(handle.wait_or(timeout=remaining_timeout))
|
111
|
+
|
112
|
+
return results
|
113
|
+
|
114
|
+
|
115
|
+
async def _wait_handles_async(
|
116
|
+
handle_list: list[MailboxHandle[_T]],
|
117
|
+
*,
|
118
|
+
timeout: float | None,
|
119
|
+
) -> list[_T]:
|
120
|
+
"""Asynchronously wait for multiple mailbox handles.
|
121
|
+
|
122
|
+
Just like _wait_handles.
|
123
|
+
"""
|
124
|
+
results: list[_T | None] = [None for _ in handle_list]
|
125
|
+
|
126
|
+
async def wait_single(index: int) -> None:
|
127
|
+
handle = handle_list[index]
|
128
|
+
results[index] = await handle.wait_async(timeout=timeout)
|
129
|
+
|
130
|
+
async with asyncio_compat.open_task_group() as task_group:
|
131
|
+
for index in range(len(handle_list)):
|
132
|
+
task_group.start_soon(wait_single(index))
|
133
|
+
|
134
|
+
# NOTE: `list` is not subscriptable until Python 3.10, so we use List.
|
135
|
+
return cast(List[_T], results)
|
wandb/sdk/service/server_sock.py
CHANGED
@@ -44,7 +44,10 @@ class SockServerInterfaceReaderThread(threading.Thread):
|
|
44
44
|
_stopped: "Event"
|
45
45
|
|
46
46
|
def __init__(
|
47
|
-
self,
|
47
|
+
self,
|
48
|
+
clients: ClientDict,
|
49
|
+
iface: "InterfaceRelay",
|
50
|
+
stopped: "Event",
|
48
51
|
) -> None:
|
49
52
|
self._iface = iface
|
50
53
|
self._clients = clients
|
@@ -53,7 +56,6 @@ class SockServerInterfaceReaderThread(threading.Thread):
|
|
53
56
|
self._stopped = stopped
|
54
57
|
|
55
58
|
def run(self) -> None:
|
56
|
-
assert self._iface.relay_q
|
57
59
|
while not self._stopped.is_set():
|
58
60
|
try:
|
59
61
|
result = self._iface.relay_q.get(timeout=1)
|
@@ -70,6 +72,7 @@ class SockServerInterfaceReaderThread(threading.Thread):
|
|
70
72
|
sock_client = self._clients.get_client(sockid)
|
71
73
|
assert sock_client
|
72
74
|
sresp = spb.ServerResponse()
|
75
|
+
sresp.request_id = result.control.mailbox_slot
|
73
76
|
sresp.result_communicate.CopyFrom(result)
|
74
77
|
sock_client.send_server_response(sresp)
|
75
78
|
|
@@ -148,7 +151,10 @@ class SockServerReadThread(threading.Thread):
|
|
148
151
|
inform_attach_response.settings.CopyFrom(
|
149
152
|
self._mux._streams[stream_id]._settings._proto,
|
150
153
|
)
|
151
|
-
response = spb.ServerResponse(
|
154
|
+
response = spb.ServerResponse(
|
155
|
+
request_id=sreq.request_id,
|
156
|
+
inform_attach_response=inform_attach_response,
|
157
|
+
)
|
152
158
|
self._sock_client.send_server_response(response)
|
153
159
|
iface = self._mux.get_stream(stream_id).interface
|
154
160
|
|