sequence-ai 0.44.2__tar.gz → 0.45.0__tar.gz
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.
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/PKG-INFO +3 -2
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/README.md +2 -1
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/pyproject.toml +1 -1
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/named.py +11 -4
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/direct.py +10 -337
- sequence_ai-0.45.0/sequence_ai/_core/direct_episode.py +181 -0
- sequence_ai-0.45.0/sequence_ai/_core/direct_observe.py +220 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/window.py +38 -7
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/worker_transport.py +60 -18
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/.gitignore +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/LICENSE +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/eval.md +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/observability.md +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/__main__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_account.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_deploy_gateway.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_eval_gateway.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_gateway.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_http.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_observe_gateway.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_partial.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/adapter.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/adapter_check.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/adapter_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/audit_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/base.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_cancel.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_registry.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_rm.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_source.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/capacity_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/cli_json.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/cli_parser.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/clock.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/code_bundle.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/collate.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/columns.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/data_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deploy.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deploy_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deployments_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deps_cost.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/discover.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/errors.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/eval.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/eval_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/eval_engine.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/freeze.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/image.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/kinds.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/listing.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/loading.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/observe.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/ops_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/params.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/policy.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/check.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/errors.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/export.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/motion.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/mujoco.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/sapien.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/scaffold.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/presets/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/presets/robot_arm.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/project_deps.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/registry.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/replay.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/run_logs.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/safety.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/secret.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/selection.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/source.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/storage_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/template_fetch.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/tf.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/units.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/upgrade_cli.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/usage_hist.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/volume.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/volume_upload.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/weights.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq_launch.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/act_size.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/capacity.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/catalogue.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/diagnostics.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/encode.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/error_codes.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/frames.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/logsource.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/mock_gate.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/money.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/msgpack_io.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/names.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/obs_schedule.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/observe_stream.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/own_rows.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/pinned_tls.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/public_tls.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/reporting.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/rid.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/transport.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/warn.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/frames.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/observation.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/player.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/policy.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/run_loop.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/types.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/content.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/conversation.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/infer.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/types.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/decide/__init__.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/decide/infer.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/decide/types.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/errors.py +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/py.typed +0 -0
- {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sequence-ai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.45.0
|
|
4
4
|
Summary: The inference and benchmark platform for physical AI: deploy a robot policy, run it on your robot, score it.
|
|
5
5
|
Project-URL: Homepage, https://generalsequences.com
|
|
6
6
|
Project-URL: Documentation, https://generalsequences.com/docs/
|
|
@@ -991,7 +991,8 @@ adapter's `__init__` keywords and a benchmark's `@seq.setup` keywords the same w
|
|
|
991
991
|
(`observations=seq.Observations({"cam": seq.Stream(at=[-15, 0])})`) has its worker keep the frames its acts
|
|
992
992
|
read: hand it every step's observation (`run()` and a `Player` do), only the steps a window reads go up, and each act
|
|
993
993
|
is handed them with its own. When the copy holding an episode stops (out of memory, a crash), the robot goes on in a new episode with a
|
|
994
|
-
warning
|
|
994
|
+
warning - its steps counted from 0 again, none of the old one's observations handed to it - and `run()` and a `Player`
|
|
995
|
+
count it in `episode_resets`; on a worker of one copy the act sent again finds
|
|
995
996
|
it still starting - a loop of `act()` calls waits that out, while `run()` and a `Player` raise it, saying so, and
|
|
996
997
|
`run()` called again goes on in the new episode. A policy that only needs the last N
|
|
997
998
|
frames needs no state: declare `observation=seq.ObservationContract(history=N)` and the robot sends
|
|
@@ -945,7 +945,8 @@ adapter's `__init__` keywords and a benchmark's `@seq.setup` keywords the same w
|
|
|
945
945
|
(`observations=seq.Observations({"cam": seq.Stream(at=[-15, 0])})`) has its worker keep the frames its acts
|
|
946
946
|
read: hand it every step's observation (`run()` and a `Player` do), only the steps a window reads go up, and each act
|
|
947
947
|
is handed them with its own. When the copy holding an episode stops (out of memory, a crash), the robot goes on in a new episode with a
|
|
948
|
-
warning
|
|
948
|
+
warning - its steps counted from 0 again, none of the old one's observations handed to it - and `run()` and a `Player`
|
|
949
|
+
count it in `episode_resets`; on a worker of one copy the act sent again finds
|
|
949
950
|
it still starting - a loop of `act()` calls waits that out, while `run()` and a `Player` raise it, saying so, and
|
|
950
951
|
`run()` called again goes on in the new episode. A policy that only needs the last N
|
|
951
952
|
frames needs no state: declare `observation=seq.ObservationContract(history=N)` and the robot sends
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sequence-ai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.45.0"
|
|
8
8
|
description = "The inference and benchmark platform for physical AI: deploy a robot policy, run it on your robot, score it."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -10,7 +10,8 @@ each name is a method of the class (its own or inherited) taking what the worker
|
|
|
10
10
|
ones its constructor - or the factory `make` names - takes → the class keeps every method as written, and its
|
|
11
11
|
registration names them (INFER, OBSERVE, RESET, ON_EPISODE) beside how the worker builds it (PolicyConstructor): the
|
|
12
12
|
worker calls the constructor at load, config files read and "{weights}" filled where it runs (sequence_base.construct),
|
|
13
|
-
resets the class at an episode's first act, hands its observe method each observation
|
|
13
|
+
resets the class at an episode's first act, hands its observe method each observation - and its step in the episode,
|
|
14
|
+
from 0, by name, when the method takes `step` - then asks its infer method.
|
|
14
15
|
|
|
15
16
|
WHY the platform calls the user's methods rather than the user writing @seq.load / @seq.infer around them: the goal
|
|
16
17
|
is that a class that runs locally deploys as it is - "just add a decoration and now you can upload it" - and nothing
|
|
@@ -106,7 +107,8 @@ def _method(cls: type, arg: str, name: Any) -> str:
|
|
|
106
107
|
|
|
107
108
|
def _takes(cls: type, arg: str, name: str | None, positional: int) -> None:
|
|
108
109
|
"""That the method `name` takes `positional` arguments besides self and no more required ones: the worker hands
|
|
109
|
-
observe one observation, infer the act's inputs (or nothing
|
|
110
|
+
observe one observation - and step=, its step in the episode, when it takes one - infer the act's inputs (or nothing
|
|
111
|
+
when the class observes), reset and end nothing."""
|
|
110
112
|
if name is None:
|
|
111
113
|
return
|
|
112
114
|
fn = getattr(cls, name)
|
|
@@ -117,11 +119,16 @@ def _takes(cls: type, arg: str, name: str | None, positional: int) -> None:
|
|
|
117
119
|
params = list(sig.parameters.values())
|
|
118
120
|
if params and inspect.isfunction(inspect.getattr_static(cls, name)):
|
|
119
121
|
params = params[1:]
|
|
122
|
+
|
|
123
|
+
by_name = {"step": 0} if arg == "observe" and any(
|
|
124
|
+
p.kind is inspect.Parameter.VAR_KEYWORD or (p.name == "step" and p.kind in (
|
|
125
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)) for p in params) else {}
|
|
120
126
|
try:
|
|
121
|
-
inspect.Signature(params).bind(*([None] * positional))
|
|
127
|
+
inspect.Signature(params).bind(*([None] * positional), **by_name)
|
|
122
128
|
except TypeError:
|
|
123
129
|
hands = {0: "nothing", 1: "one argument"}[positional]
|
|
124
|
-
why = {"observe": "each observation
|
|
130
|
+
why = {"observe": "each observation, and step= - its step in the episode, from 0 - when it takes one",
|
|
131
|
+
"infer": "the act's inputs" if positional else
|
|
125
132
|
"nothing - its observations went to observe=", "reset": "nothing", "end": "nothing",
|
|
126
133
|
"sample": "nothing - it returns the episode's act inputs"}[arg]
|
|
127
134
|
raise PolicyDefinitionError(f"{cls.__name__}.{name}{sig} ({arg}=): the worker hands it {hands} ({why})") \
|
|
@@ -25,7 +25,8 @@ still does `self._t.post("/v1/act", payload)`, and this class routes that one pa
|
|
|
25
25
|
worker while `/v1/models` and `/v1/act/ready` stay on the gateway.
|
|
26
26
|
|
|
27
27
|
The data plane is worker_transport.py's (one kept-alive client to one worker's `/act`), the pin
|
|
28
|
-
pinned_tls.py's (the worker's certificate checked by the fingerprint the gateway pinned).
|
|
28
|
+
pinned_tls.py's (the worker's certificate checked by the fingerprint the gateway pinned). A robot's observations between
|
|
29
|
+
acts are direct_observe.py's, its episodes direct_episode.py's: DirectConnection mixes both in.
|
|
29
30
|
|
|
30
31
|
Stdlib `ssl` + `hashlib` only for the pin (no `cryptography`, no `certifi`: a worker checked by the
|
|
31
32
|
CAs instead is public_tls.py's); httpx-only, Python 3.9-clean (no runtime `X | Y`, no `match`); and none
|
|
@@ -48,15 +49,13 @@ from typing import Any, Callable, NamedTuple, Optional
|
|
|
48
49
|
|
|
49
50
|
from ..errors import AuthError, Conflict, InternalError, InvalidRequest, SequencesError, Unavailable
|
|
50
51
|
from . import act_size
|
|
51
|
-
from .names import segment
|
|
52
52
|
from .public_tls import unmask_name
|
|
53
53
|
from .transport import DEFAULT_TIMEOUT_S, Transport
|
|
54
54
|
from .warn import warn
|
|
55
|
-
from . import obs_schedule as _obs_schedule
|
|
56
55
|
from . import observe_stream as _observe
|
|
57
|
-
from .
|
|
58
|
-
from .
|
|
59
|
-
|
|
56
|
+
from .worker_transport import _STILL_OPENING, _WorkerGone, _WorkerTransport, _frames_on, _wait_set
|
|
57
|
+
from .direct_episode import _EPISODE_LOST, _STATE_GONE, _Episodes
|
|
58
|
+
from .direct_observe import _ObservationStream
|
|
60
59
|
|
|
61
60
|
|
|
62
61
|
|
|
@@ -107,21 +106,6 @@ def _busy_backoff(attempt: int, retry_after_s: Optional[float]) -> float:
|
|
|
107
106
|
|
|
108
107
|
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
_COPY_STOPPED = frozenset({"gpu_out_of_memory", "host_out_of_memory", "crashed", "hung"})
|
|
115
|
-
_EPISODE_LOST = "episode_lost"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
_BATCH_FAILED = "batch_failed"
|
|
121
|
-
_STATE_GONE = _COPY_STOPPED | {_BATCH_FAILED}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
109
|
_SAY_BUSY_AFTER_S = 5.0
|
|
126
110
|
|
|
127
111
|
|
|
@@ -366,7 +350,7 @@ def _keep_alive(ref: "weakref.ref[DirectConnection]", stop: threading.Event) ->
|
|
|
366
350
|
del conn
|
|
367
351
|
|
|
368
352
|
|
|
369
|
-
class DirectConnection:
|
|
353
|
+
class DirectConnection(_ObservationStream, _Episodes):
|
|
370
354
|
"""
|
|
371
355
|
Control/data-plane split behind the `Transport` surface `Policy` already drives.
|
|
372
356
|
|
|
@@ -531,6 +515,7 @@ class DirectConnection:
|
|
|
531
515
|
feedback = payload.pop("_feedback", None)
|
|
532
516
|
|
|
533
517
|
own = self._own_act(asked, step_at, payload.get("plan")) if asked is not None else None
|
|
518
|
+
resets = self.episode_resets
|
|
534
519
|
resent = False
|
|
535
520
|
lost = 0
|
|
536
521
|
busy_since: Optional[float] = None
|
|
@@ -541,6 +526,9 @@ class DirectConnection:
|
|
|
541
526
|
if asked is not None:
|
|
542
527
|
payload["episode_id"] = asked
|
|
543
528
|
self._apply_episode(payload)
|
|
529
|
+
if own is not None and self.episode_resets != resets:
|
|
530
|
+
|
|
531
|
+
own, resets = self._own_anew(asked, own), self.episode_resets
|
|
544
532
|
if own is not None:
|
|
545
533
|
self._observed_act(payload, worker, own, timeout_s)
|
|
546
534
|
if not by_hand:
|
|
@@ -660,183 +648,6 @@ class DirectConnection:
|
|
|
660
648
|
warn(f"waiting for a free copy of the policy on its worker: {exc}")
|
|
661
649
|
time.sleep(wait)
|
|
662
650
|
|
|
663
|
-
def observe(self, inputs: Any, episode_id: str, step: Optional[int] = None) -> int:
|
|
664
|
-
"""One observation of the caller's episode `episode_id`, handed over as the robot makes it: its number in the
|
|
665
|
-
episode, at once. Packed here, on the robot's own thread (what msgpack cannot carry is refused as an act's inputs
|
|
666
|
-
are), then posted on a connection of its own to a worker that takes them (observe_stream), held for the
|
|
667
|
-
episode's next act while the worker has not said whether it does, and sent nowhere when it does not. The next
|
|
668
|
-
act of the episode is the next number. A policy that declares what it reads keeps it by step (_observe_window);
|
|
669
|
-
`step`: the step it was made at (a Player's rows played), else the next. Mid-episode, its worker gone and the
|
|
670
|
-
one in its place starting (or the robot in line for one), the observation is kept for the episode's next act,
|
|
671
|
-
which waits for that worker as an act does and sends it: never waited for here."""
|
|
672
|
-
self._assert_open()
|
|
673
|
-
try:
|
|
674
|
-
self._ensure_connected()
|
|
675
|
-
reachable = True
|
|
676
|
-
except Unavailable as exc:
|
|
677
|
-
if self.contract is None or not (exc.warming or exc.queued):
|
|
678
|
-
raise
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
reachable = False
|
|
683
|
-
if _obs_schedule.reads_past(self._observations):
|
|
684
|
-
return self._observe_window(inputs, episode_id, step, reachable)
|
|
685
|
-
try:
|
|
686
|
-
packed = _observe.packed(inputs)
|
|
687
|
-
except ValueError as exc:
|
|
688
|
-
raise InvalidRequest(f"observe(): this observation cannot be sent - {exc}") from None
|
|
689
|
-
with self._lock:
|
|
690
|
-
n = self._numbers.get(episode_id, 0)
|
|
691
|
-
self._numbers[episode_id] = n + 1
|
|
692
|
-
worker = self._worker if reachable else None
|
|
693
|
-
wid = self._worker_episode if episode_id == self._caller_episode and self._worker_episode else episode_id
|
|
694
|
-
takes = None if worker is None else worker.takes_observations
|
|
695
|
-
stream = self._stream_for(worker) if takes else None
|
|
696
|
-
held = self._held.pop(episode_id, []) if takes is not None else []
|
|
697
|
-
if takes is None:
|
|
698
|
-
self._held.setdefault(episode_id, []).append((n, _observe.frame(n, packed)))
|
|
699
|
-
if stream is not None:
|
|
700
|
-
for s, f in [*held, (n, _observe.frame(n, packed))]:
|
|
701
|
-
stream.put(wid, s, f)
|
|
702
|
-
self._say_refusals(stream)
|
|
703
|
-
elif takes is False:
|
|
704
|
-
self._say_unread()
|
|
705
|
-
return n
|
|
706
|
-
|
|
707
|
-
def expect_ask(self, episode_id: str, step: Optional[int]) -> None:
|
|
708
|
-
"""The step a Player expects to ask at next in `episode_id` (its playback left until its lead, or the planning
|
|
709
|
-
worker's time) - None: none, its run's last rows in hand: its window's frames go up as that act reads them, not
|
|
710
|
-
by a guess from the last spacing, and none go up ahead when no act is coming."""
|
|
711
|
-
with self._lock:
|
|
712
|
-
win = self._windows.get(episode_id)
|
|
713
|
-
if win is not None:
|
|
714
|
-
win.expect(step)
|
|
715
|
-
|
|
716
|
-
def reads_past(self) -> bool:
|
|
717
|
-
"""Whether the policy reads steps before an act's own (its Observations, as connect answered them): a Player then
|
|
718
|
-
hands it every tick's observation (observe), each numbered by its step."""
|
|
719
|
-
return _obs_schedule.reads_past(self._observations)
|
|
720
|
-
|
|
721
|
-
def _observe_window(self, inputs: Any, episode_id: str, step: Optional[int], reachable: bool = True) -> int:
|
|
722
|
-
"""observe() for a policy that declares what it reads, steps before an act's own among it: numbered by step and
|
|
723
|
-
kept (its Window) - what the next act is expected to read at that step posted now, to a worker that takes them;
|
|
724
|
-
the rest goes up just before the act that reads it. Not `reachable` (its worker restarting): all of it goes up
|
|
725
|
-
just before the act. Returns its step."""
|
|
726
|
-
with self._lock:
|
|
727
|
-
worker = self._worker if reachable else None
|
|
728
|
-
takes = None if worker is None else worker.takes_observations
|
|
729
|
-
win = self._window_for(episode_id, None)
|
|
730
|
-
s = win.step(step)
|
|
731
|
-
uploads = win.observe(s, inputs)
|
|
732
|
-
if takes is None:
|
|
733
|
-
win.lost()
|
|
734
|
-
wid = self._worker_episode if episode_id == self._caller_episode and self._worker_episode else episode_id
|
|
735
|
-
stream = self._stream_for(worker) if takes else None
|
|
736
|
-
if stream is not None:
|
|
737
|
-
for st, f in self._packed_steps(uploads):
|
|
738
|
-
stream.put(wid, st, f)
|
|
739
|
-
self._say_refusals(stream)
|
|
740
|
-
elif takes is False:
|
|
741
|
-
self._say_unread()
|
|
742
|
-
return s
|
|
743
|
-
|
|
744
|
-
def _window_for(self, episode_id: str, plan: Any) -> "_Window":
|
|
745
|
-
"""The episode's Window, made at its first observation or act (called under self._lock): its schedule taken to
|
|
746
|
-
steps by the robot's rate - its act's plan.hz (a Player's), else connect(control_hz=) - and the next act expected
|
|
747
|
-
as far on as the policy's open_loop_horizon until there are two."""
|
|
748
|
-
win = self._windows.get(episode_id)
|
|
749
|
-
if win is None:
|
|
750
|
-
rate = plan.get("hz") if isinstance(plan, dict) else None
|
|
751
|
-
try:
|
|
752
|
-
sched = _obs_schedule.of(self._observations, rate=rate or self.control_hz)
|
|
753
|
-
except ValueError as exc:
|
|
754
|
-
raise InvalidRequest(str(exc)) from None
|
|
755
|
-
gap = (self._actions or {}).get("open_loop_horizon") if isinstance(self._actions, dict) else None
|
|
756
|
-
win = self._windows[episode_id] = _Window(sched, gap=gap or 1)
|
|
757
|
-
return win
|
|
758
|
-
|
|
759
|
-
@staticmethod
|
|
760
|
-
def _packed_steps(uploads: list) -> list:
|
|
761
|
-
"""[(step, frame)] of a Window's uploads, packed on the caller's thread: what msgpack cannot carry refused here."""
|
|
762
|
-
try:
|
|
763
|
-
return [(s, _observe.frame(s, _observe.packed(obs))) for s, obs in uploads]
|
|
764
|
-
except ValueError as exc:
|
|
765
|
-
raise InvalidRequest(f"observe(): an observation cannot be sent - {exc}") from None
|
|
766
|
-
|
|
767
|
-
def _stream_for(self, worker: "_WorkerTransport") -> "_observe.ObserveStream":
|
|
768
|
-
"""The stream posting to `worker` - made the first time it is asked for, and anew for another worker, posting
|
|
769
|
-
again what the one before kept since each episode's last served act; another worker holds none of a window's
|
|
770
|
-
frames, which go again as their acts read them. Called under self._lock."""
|
|
771
|
-
if self._stream is None or self._stream_to is not worker:
|
|
772
|
-
carried = {}
|
|
773
|
-
if self._stream is not None:
|
|
774
|
-
carried = self._stream.carry()
|
|
775
|
-
self._stream.close()
|
|
776
|
-
for win in self._windows.values():
|
|
777
|
-
win.lost()
|
|
778
|
-
self._stream, self._stream_to = _observe.ObserveStream(worker.post_observe, carried), worker
|
|
779
|
-
return self._stream
|
|
780
|
-
|
|
781
|
-
def _own_act(self, episode_id: str, step_at: Optional[int] = None, plan: Any = None) -> Optional[tuple]:
|
|
782
|
-
"""For an act of an episode the robot hands observations to (observe), or of a policy that reads steps before an
|
|
783
|
-
act's own: (its number - `step_at`, a Player's rows played, else the next - the frames held for it, its Window
|
|
784
|
-
or None); None for any other act."""
|
|
785
|
-
self._ensure_connected()
|
|
786
|
-
with self._lock:
|
|
787
|
-
if _obs_schedule.reads_past(self._observations):
|
|
788
|
-
if self._observations.get("unit") == "seconds" and not (isinstance(plan, dict) and plan.get("hz")):
|
|
789
|
-
|
|
790
|
-
raise InvalidRequest("this policy counts its window in seconds, which its worker takes to steps by "
|
|
791
|
-
"your robot's rate: send each act with steps_left= (its plan carries the rate "
|
|
792
|
-
"connect(control_hz=) says), or let run() or a Player send them")
|
|
793
|
-
win = self._window_for(episode_id, plan)
|
|
794
|
-
return win.step(step_at), [], win
|
|
795
|
-
if episode_id not in self._numbers:
|
|
796
|
-
return None
|
|
797
|
-
n = self._numbers[episode_id]
|
|
798
|
-
self._numbers[episode_id] = n + 1
|
|
799
|
-
return n, self._held.pop(episode_id, []), None
|
|
800
|
-
|
|
801
|
-
def _observed_act(self, payload: dict, worker: "_WorkerTransport", own: tuple, timeout_s: Optional[float]) -> None:
|
|
802
|
-
"""The act's body, each try, in place: its number (`seq`) when its worker takes the stream - the frames held for
|
|
803
|
-
it posted first - and nothing of the stream when it does not: its policy reads the act's own alone. A worker
|
|
804
|
-
that has not answered yet (the first act, or one after a reconnect, whose stream posts what the one before kept)
|
|
805
|
-
is waited for as the act would wait for it anyway (await_answer)."""
|
|
806
|
-
n, held, win = own
|
|
807
|
-
if worker.takes_observations is None:
|
|
808
|
-
worker.await_answer(timeout_s)
|
|
809
|
-
if worker.takes_observations:
|
|
810
|
-
with self._lock:
|
|
811
|
-
stream = self._stream_for(worker)
|
|
812
|
-
uploads = win.act(n) if win is not None else []
|
|
813
|
-
for s, f in [*self._packed_steps(uploads), *held]:
|
|
814
|
-
stream.put(payload["episode_id"], s, f)
|
|
815
|
-
held.clear()
|
|
816
|
-
payload["seq"] = n
|
|
817
|
-
self._say_refusals(stream)
|
|
818
|
-
return
|
|
819
|
-
payload.pop("seq", None)
|
|
820
|
-
if held or win is not None:
|
|
821
|
-
held.clear()
|
|
822
|
-
self._say_unread()
|
|
823
|
-
|
|
824
|
-
def _say_unread(self) -> None:
|
|
825
|
-
"""Said once a connection: observations handed to observe() went nowhere, as its worker does not take them."""
|
|
826
|
-
with self._lock:
|
|
827
|
-
said, self._said_unread = self._said_unread, True
|
|
828
|
-
if not said:
|
|
829
|
-
warn("observe() hands this policy nothing: its worker reads each act's own observation alone. A policy is "
|
|
830
|
-
"handed the ones between acts when it declares observations=seq.Observations(every=1) (naming an "
|
|
831
|
-
"observe= method does) and its worker takes them - one deployed before workers did, once `seq deploy` builds it "
|
|
832
|
-
"again")
|
|
833
|
-
|
|
834
|
-
def _say_refusals(self, stream: "_observe.ObserveStream") -> None:
|
|
835
|
-
"""What the worker refused to keep of an episode's observations (more than it keeps between two acts), said once
|
|
836
|
-
an episode on the robot's own thread: those were left out, and its acts read the ones it kept."""
|
|
837
|
-
for why in stream.refusals():
|
|
838
|
-
warn(f"observations left out: the worker refused them ({why}); the episode's acts read the ones it kept")
|
|
839
|
-
|
|
840
651
|
def handshake(self) -> Optional[dict]:
|
|
841
652
|
"""Connect now if not yet connected — the same handshake the first act makes — and return the contract
|
|
842
653
|
the deployment declared ({"action", "observation", "embodiment"}). Lets Policy check an observation
|
|
@@ -1416,144 +1227,6 @@ A robot in line had no seat: it is told it is no longer in line. `why` is the pl
|
|
|
1416
1227
|
else:
|
|
1417
1228
|
self._lease_deadline = None
|
|
1418
1229
|
|
|
1419
|
-
def end_episode(self, episode_id: str) -> None:
|
|
1420
|
-
"""The robot's episode is over: the worker frees its state now (a stateful policy's @seq.on_episode runs
|
|
1421
|
-
and its replica returns to the pool) instead of after its TTL with no act. Never raises. When the platform asked the
|
|
1422
|
-
robot to move (its worker is leaving), this boundary is where it does: the next episode starts on a worker
|
|
1423
|
-
already up - at its deployment's cap, where the worker is replaced in place, on the new one once it is up - and
|
|
1424
|
-
nothing is cut. The worker it leaves is told the episode ended while the move's connect goes, not before it;
|
|
1425
|
-
kept on that worker after all, its next episode starts once the worker has been told, as with no move.
|
|
1426
|
-
"""
|
|
1427
|
-
with self._lock:
|
|
1428
|
-
worker = None if self._closed else self._worker
|
|
1429
|
-
wid = self._worker_episode if episode_id == self._caller_episode else episode_id
|
|
1430
|
-
self._episode_open = False
|
|
1431
|
-
self._settle_params()
|
|
1432
|
-
|
|
1433
|
-
self._numbers.pop(episode_id, None)
|
|
1434
|
-
self._held.pop(episode_id, None)
|
|
1435
|
-
self._windows.pop(episode_id, None)
|
|
1436
|
-
if self._stream is not None and wid:
|
|
1437
|
-
self._stream.end(wid)
|
|
1438
|
-
moving = worker is not None and self._rotate is not None and _ready(self._rotate)
|
|
1439
|
-
told = None
|
|
1440
|
-
if worker is not None and wid:
|
|
1441
|
-
if moving:
|
|
1442
|
-
told = worker.end_episode_behind(wid)
|
|
1443
|
-
else:
|
|
1444
|
-
worker.post_episode_end(wid)
|
|
1445
|
-
try:
|
|
1446
|
-
with self._lock:
|
|
1447
|
-
if self._rotate is None or self._closed or self._worker is None:
|
|
1448
|
-
return
|
|
1449
|
-
if not _ready(self._rotate):
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
return
|
|
1455
|
-
|
|
1456
|
-
self._caller_episode = self._worker_episode = None
|
|
1457
|
-
try:
|
|
1458
|
-
self._connect(renew=False, move=True)
|
|
1459
|
-
except Unavailable:
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
if self._worker is not None:
|
|
1467
|
-
self._worker.gone = True
|
|
1468
|
-
except Exception:
|
|
1469
|
-
pass
|
|
1470
|
-
finally:
|
|
1471
|
-
if told is not None:
|
|
1472
|
-
with self._lock:
|
|
1473
|
-
stays = self._worker is worker and not worker.gone
|
|
1474
|
-
if stays:
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
told.join(_EPISODE_END_S + _KEEP_PING_TIMEOUT_S)
|
|
1478
|
-
|
|
1479
|
-
def label_episode(self, episode_id: str, *, success: bool, notes: str = "") -> None:
|
|
1480
|
-
"""How the robot's episode went, kept with the deployment's captured field data (`seq policy capture`):
|
|
1481
|
-
written for the id the workers saw it under — and every id it carried after a reconnect moved it. Raises on
|
|
1482
|
-
failure.
|
|
1483
|
-
"""
|
|
1484
|
-
with self._lock:
|
|
1485
|
-
ids = [episode_id, *self._aliases.get(episode_id, [])]
|
|
1486
|
-
for wid in ids:
|
|
1487
|
-
|
|
1488
|
-
self._gateway.post(f"/v1/deployments/{segment(self._model)}/episodes/{segment(wid)}/label",
|
|
1489
|
-
{"success": bool(success), "notes": notes})
|
|
1490
|
-
|
|
1491
|
-
def _take_params(self, answered: dict) -> None:
|
|
1492
|
-
"""What the platform answered the session runs with: at once, or — while an episode runs and they differ —
|
|
1493
|
-
at its boundary, so one episode never runs on two sets of parameters."""
|
|
1494
|
-
if self._episode_open and (answered or None) != self._params:
|
|
1495
|
-
self._pending_params = answered
|
|
1496
|
-
else:
|
|
1497
|
-
self._params, self._pending_params = answered or None, None
|
|
1498
|
-
|
|
1499
|
-
def _settle_params(self) -> None:
|
|
1500
|
-
"""An episode boundary: parameters a renewal answered during the episode take effect now."""
|
|
1501
|
-
if self._pending_params is not None:
|
|
1502
|
-
self._params, self._pending_params = self._pending_params or None, None
|
|
1503
|
-
|
|
1504
|
-
def _apply_episode(self, payload: dict) -> None:
|
|
1505
|
-
"""Remap the caller's episode id to the current worker-side id (in place)."""
|
|
1506
|
-
with self._lock:
|
|
1507
|
-
caller = payload.get("episode_id")
|
|
1508
|
-
if caller is None:
|
|
1509
|
-
return
|
|
1510
|
-
if caller != self._caller_episode:
|
|
1511
|
-
|
|
1512
|
-
self._caller_episode = caller
|
|
1513
|
-
self._worker_episode = caller
|
|
1514
|
-
self._settle_params()
|
|
1515
|
-
self._episode_open = True
|
|
1516
|
-
payload["episode_id"] = self._worker_episode
|
|
1517
|
-
|
|
1518
|
-
def _rotate_episode(self, notice: Optional[dict] = None, *, stopped: Optional[str] = None) -> Optional[str]:
|
|
1519
|
-
"""New worker → the episode restarts there; surface it, do not continue silently - with why, when the platform
|
|
1520
|
-
had said the robot was to move (`notice`, its `rotate`). `stopped`: the copy of the policy that held it stopped
|
|
1521
|
-
(its cause, _COPY_STOPPED), or ended it for a batch that raised (_BATCH_FAILED), on the same worker. Returns the
|
|
1522
|
-
worker-side id it leaves, None on a stateless path."""
|
|
1523
|
-
if self._caller_episode is None:
|
|
1524
|
-
return None
|
|
1525
|
-
left = self._worker_episode
|
|
1526
|
-
self._worker_episode = uuid.uuid4().hex
|
|
1527
|
-
self._aliases.setdefault(self._caller_episode, []).append(self._worker_episode)
|
|
1528
|
-
self.episode_resets += 1
|
|
1529
|
-
if self._stream is not None and left:
|
|
1530
|
-
|
|
1531
|
-
self._stream.move(left, self._worker_episode)
|
|
1532
|
-
if self._caller_episode in self._windows:
|
|
1533
|
-
self._windows[self._caller_episode].lost()
|
|
1534
|
-
if stopped == _BATCH_FAILED:
|
|
1535
|
-
warn("episode was reset: a batch it was in failed, so what the policy kept of it may be half changed and was "
|
|
1536
|
-
"given up; the robot goes on in a new episode")
|
|
1537
|
-
return left
|
|
1538
|
-
if stopped is not None:
|
|
1539
|
-
warn(f"episode was reset: the copy of the policy that held it stopped ({stopped}); its next act starts a new "
|
|
1540
|
-
"episode, and what the policy kept of this one did not carry over")
|
|
1541
|
-
return left
|
|
1542
|
-
why = f" ({_moving_words(notice, moved=True)})" if notice else ""
|
|
1543
|
-
warn("episode was reset after a reconnect to a new worker" + why + "; what the policy kept of this episode "
|
|
1544
|
-
"on the previous worker did not carry over")
|
|
1545
|
-
return left
|
|
1546
|
-
|
|
1547
|
-
def _lose_episode(self, worker: "_WorkerTransport", cause: Optional[str]) -> None:
|
|
1548
|
-
"""The copy that held the caller's episode stopped, or ended it for a batch that raised (_STATE_GONE): a new
|
|
1549
|
-
worker-side episode, said with the cause (_rotate_episode), and the one it leaves ended on the worker - its lost
|
|
1550
|
-
mark cleared, nothing else to free - so the act goes again under the new id. Best effort: an end that does not
|
|
1551
|
-
arrive leaves the mark to run out."""
|
|
1552
|
-
with self._lock:
|
|
1553
|
-
left = self._rotate_episode(stopped=cause or "unknown cause")
|
|
1554
|
-
if left:
|
|
1555
|
-
worker.post_episode_end(left)
|
|
1556
|
-
|
|
1557
1230
|
def _assert_open(self) -> None:
|
|
1558
1231
|
if self._closed:
|
|
1559
1232
|
raise SequencesError("this connection is closed; open a new one with connect()")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A DirectConnection's episodes, mixed into it (_Episodes): the caller's episode id mapped to the one its worker sees
|
|
3
|
+
(_apply_episode) - a new one when a reconnect moved the robot or the copy that held it stopped (_rotate_episode,
|
|
4
|
+
_lose_episode) - its end told to the worker, where a move the platform asked for happens (end_episode), how it went kept
|
|
5
|
+
with the captured field data (label_episode), and the session's parameters taking effect at its boundaries. The
|
|
6
|
+
connection, its worker and its lock are DirectConnection's (direct.py), whose move-notice words these read.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from ..errors import Unavailable
|
|
15
|
+
from .names import segment
|
|
16
|
+
from .warn import warn
|
|
17
|
+
from .worker_transport import _EPISODE_END_S, _KEEP_PING_TIMEOUT_S, _WorkerTransport
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_COPY_STOPPED = frozenset({"gpu_out_of_memory", "host_out_of_memory", "crashed", "hung"})
|
|
27
|
+
_EPISODE_LOST = "episode_lost"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_BATCH_FAILED = "batch_failed"
|
|
33
|
+
_STATE_GONE = _COPY_STOPPED | {_BATCH_FAILED}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Episodes:
|
|
37
|
+
"""DirectConnection's episodes (the module's docstring)."""
|
|
38
|
+
|
|
39
|
+
def end_episode(self, episode_id: str) -> None:
|
|
40
|
+
"""The robot's episode is over: the worker frees its state now (a stateful policy's @seq.on_episode runs
|
|
41
|
+
and its replica returns to the pool) instead of after its TTL with no act. Never raises. When the platform asked the
|
|
42
|
+
robot to move (its worker is leaving), this boundary is where it does: the next episode starts on a worker
|
|
43
|
+
already up - at its deployment's cap, where the worker is replaced in place, on the new one once it is up - and
|
|
44
|
+
nothing is cut. The worker it leaves is told the episode ended while the move's connect goes, not before it;
|
|
45
|
+
kept on that worker after all, its next episode starts once the worker has been told, as with no move.
|
|
46
|
+
"""
|
|
47
|
+
from .direct import _ready
|
|
48
|
+
with self._lock:
|
|
49
|
+
worker = None if self._closed else self._worker
|
|
50
|
+
wid = self._worker_episode if episode_id == self._caller_episode else episode_id
|
|
51
|
+
self._episode_open = False
|
|
52
|
+
self._settle_params()
|
|
53
|
+
|
|
54
|
+
self._numbers.pop(episode_id, None)
|
|
55
|
+
self._held.pop(episode_id, None)
|
|
56
|
+
self._windows.pop(episode_id, None)
|
|
57
|
+
if self._stream is not None and wid:
|
|
58
|
+
self._stream.end(wid)
|
|
59
|
+
moving = worker is not None and self._rotate is not None and _ready(self._rotate)
|
|
60
|
+
told = None
|
|
61
|
+
if worker is not None and wid:
|
|
62
|
+
if moving:
|
|
63
|
+
told = worker.end_episode_behind(wid)
|
|
64
|
+
else:
|
|
65
|
+
worker.post_episode_end(wid)
|
|
66
|
+
try:
|
|
67
|
+
with self._lock:
|
|
68
|
+
if self._rotate is None or self._closed or self._worker is None:
|
|
69
|
+
return
|
|
70
|
+
if not _ready(self._rotate):
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
self._caller_episode = self._worker_episode = None
|
|
78
|
+
try:
|
|
79
|
+
self._connect(renew=False, move=True)
|
|
80
|
+
except Unavailable:
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if self._worker is not None:
|
|
88
|
+
self._worker.gone = True
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
finally:
|
|
92
|
+
if told is not None:
|
|
93
|
+
with self._lock:
|
|
94
|
+
stays = self._worker is worker and not worker.gone
|
|
95
|
+
if stays:
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
told.join(_EPISODE_END_S + _KEEP_PING_TIMEOUT_S)
|
|
99
|
+
|
|
100
|
+
def label_episode(self, episode_id: str, *, success: bool, notes: str = "") -> None:
|
|
101
|
+
"""How the robot's episode went, kept with the deployment's captured field data (`seq policy capture`):
|
|
102
|
+
written for the id the workers saw it under — and every id it carried after a reconnect moved it. Raises on
|
|
103
|
+
failure.
|
|
104
|
+
"""
|
|
105
|
+
with self._lock:
|
|
106
|
+
ids = [episode_id, *self._aliases.get(episode_id, [])]
|
|
107
|
+
for wid in ids:
|
|
108
|
+
|
|
109
|
+
self._gateway.post(f"/v1/deployments/{segment(self._model)}/episodes/{segment(wid)}/label",
|
|
110
|
+
{"success": bool(success), "notes": notes})
|
|
111
|
+
|
|
112
|
+
def _take_params(self, answered: dict) -> None:
|
|
113
|
+
"""What the platform answered the session runs with: at once, or — while an episode runs and they differ —
|
|
114
|
+
at its boundary, so one episode never runs on two sets of parameters."""
|
|
115
|
+
if self._episode_open and (answered or None) != self._params:
|
|
116
|
+
self._pending_params = answered
|
|
117
|
+
else:
|
|
118
|
+
self._params, self._pending_params = answered or None, None
|
|
119
|
+
|
|
120
|
+
def _settle_params(self) -> None:
|
|
121
|
+
"""An episode boundary: parameters a renewal answered during the episode take effect now."""
|
|
122
|
+
if self._pending_params is not None:
|
|
123
|
+
self._params, self._pending_params = self._pending_params or None, None
|
|
124
|
+
|
|
125
|
+
def _apply_episode(self, payload: dict) -> None:
|
|
126
|
+
"""Remap the caller's episode id to the current worker-side id (in place)."""
|
|
127
|
+
with self._lock:
|
|
128
|
+
caller = payload.get("episode_id")
|
|
129
|
+
if caller is None:
|
|
130
|
+
return
|
|
131
|
+
if caller != self._caller_episode:
|
|
132
|
+
|
|
133
|
+
self._caller_episode = caller
|
|
134
|
+
self._worker_episode = caller
|
|
135
|
+
self._settle_params()
|
|
136
|
+
self._episode_open = True
|
|
137
|
+
payload["episode_id"] = self._worker_episode
|
|
138
|
+
|
|
139
|
+
def _rotate_episode(self, notice: Optional[dict] = None, *, stopped: Optional[str] = None) -> Optional[str]:
|
|
140
|
+
"""New worker → the episode restarts there; surface it, do not continue silently - with why, when the platform
|
|
141
|
+
had said the robot was to move (`notice`, its `rotate`). `stopped`: the copy of the policy that held it stopped
|
|
142
|
+
(its cause, _COPY_STOPPED), or ended it for a batch that raised (_BATCH_FAILED), on the same worker. The new
|
|
143
|
+
episode is handed none of the old one's observations and counts its steps from 0 - the act on its way its first
|
|
144
|
+
(Window.restart, post) - as the policy's Episode does; a run's counts go on. Returns the worker-side id it leaves,
|
|
145
|
+
None on a stateless path."""
|
|
146
|
+
if self._caller_episode is None:
|
|
147
|
+
return None
|
|
148
|
+
left = self._worker_episode
|
|
149
|
+
self._worker_episode = uuid.uuid4().hex
|
|
150
|
+
self._aliases.setdefault(self._caller_episode, []).append(self._worker_episode)
|
|
151
|
+
self.episode_resets += 1
|
|
152
|
+
if self._stream is not None and left:
|
|
153
|
+
self._stream.end(left)
|
|
154
|
+
win = self._windows.get(self._caller_episode)
|
|
155
|
+
if win is not None:
|
|
156
|
+
win.restart(win.asked if win.asked > win.acted else win.unreached)
|
|
157
|
+
elif self._caller_episode in self._numbers:
|
|
158
|
+
self._number_anew(self._caller_episode)
|
|
159
|
+
if stopped == _BATCH_FAILED:
|
|
160
|
+
warn("episode was reset: a batch it was in failed, so what the policy kept of it may be half changed and was "
|
|
161
|
+
"given up; the robot goes on in a new episode")
|
|
162
|
+
return left
|
|
163
|
+
if stopped is not None:
|
|
164
|
+
warn(f"episode was reset: the copy of the policy that held it stopped ({stopped}); its next act starts a new "
|
|
165
|
+
"episode, and what the policy kept of this one did not carry over")
|
|
166
|
+
return left
|
|
167
|
+
from .direct import _moving_words
|
|
168
|
+
why = f" ({_moving_words(notice, moved=True)})" if notice else ""
|
|
169
|
+
warn("episode was reset after a reconnect to a new worker" + why + "; what the policy kept of this episode "
|
|
170
|
+
"on the previous worker did not carry over")
|
|
171
|
+
return left
|
|
172
|
+
|
|
173
|
+
def _lose_episode(self, worker: "_WorkerTransport", cause: Optional[str]) -> None:
|
|
174
|
+
"""The copy that held the caller's episode stopped, or ended it for a batch that raised (_STATE_GONE): a new
|
|
175
|
+
worker-side episode, said with the cause (_rotate_episode), and the one it leaves ended on the worker - its lost
|
|
176
|
+
mark cleared, nothing else to free - so the act goes again under the new id. Best effort: an end that does not
|
|
177
|
+
arrive leaves the mark to run out."""
|
|
178
|
+
with self._lock:
|
|
179
|
+
left = self._rotate_episode(stopped=cause or "unknown cause")
|
|
180
|
+
if left:
|
|
181
|
+
worker.post_episode_end(left)
|