reactor-runtime 3.5.0__tar.gz → 3.6.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.
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/PKG-INFO +5 -4
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/README.md +4 -3
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/pyproject.toml +1 -1
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/pyproject.toml.orig +1 -1
- reactor_runtime-3.6.0/src/reactor_runtime/distributed/__init__.py +34 -0
- reactor_runtime-3.6.0/src/reactor_runtime/distributed/errors.py +43 -0
- reactor_runtime-3.6.0/src/reactor_runtime/distributed/ipc.py +227 -0
- reactor_runtime-3.6.0/src/reactor_runtime/distributed/protocol.py +66 -0
- reactor_runtime-3.6.0/src/reactor_runtime/distributed/rank.py +246 -0
- reactor_runtime-3.6.0/src/reactor_runtime/distributed/runner.py +387 -0
- reactor_runtime-3.6.0/src/reactor_runtime/distributed/worker.py +74 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/recording/chunk_encoder.py +34 -11
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/recording/recorder.py +54 -14
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/LICENSE +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/NOTICE +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/codes.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/fields.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/model.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/naming.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/service.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/session.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/transport.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/typespec.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/core/values.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/event_stream.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/http/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/http/events.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/http/routes.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/http/server.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/http/spec.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/app/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/app/input_state.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/app/outcome.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/app/reactor_app.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/client.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/events/decorators.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/events/errors.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/internal/aliases.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/internal/bridge.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/model/contract.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/model/schema.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/log.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/manifest.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/message_gateway.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/metrics/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/metrics/command.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/metrics/model.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/metrics/registry.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/metrics/session.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/paths.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/protocol/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/protocol/base.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/protocol/common.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/protocol/v0/codec.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/protocol/v1/codec.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/py.typed +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/runner/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/runner/connection_manager.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/runner/offer_epochs.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/runner/runner.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/runner/state_machine.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/runner/upload_resolution.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/serve.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/service.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/acceptor.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/router.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/acceptor.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/config.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/connection.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/metrics.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/peer.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/router.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/stats.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/transport/webrtc/version.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_runtime/upload_store.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/common_pb2.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/common_pb2.pyi +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/control_pb2.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/control_pb2.pyi +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/data_pb2.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/data_pb2.pyi +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/model_pb2.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/model_pb2.pyi +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/platform_pb2.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/platform_pb2.pyi +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/track_pb2.py +0 -0
- {reactor_runtime-3.5.0 → reactor_runtime-3.6.0}/src/reactor_wire/v1/track_pb2.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactor-runtime
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: A Python framework for building real-time, interactive video models
|
|
5
5
|
Author: Reactor
|
|
6
6
|
Author-email: Reactor <team@reactor.inc>
|
|
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
**Build real-time AI models in Python.**
|
|
32
32
|
|
|
33
|
-
[📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/
|
|
33
|
+
[📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/quickstart) · [🌐 Reactor](https://reactor.inc)
|
|
34
34
|
|
|
35
35
|
</div>
|
|
36
36
|
|
|
@@ -129,7 +129,7 @@ cd my-model
|
|
|
129
129
|
reactor run
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
`reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
|
|
132
|
+
`reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc/sdk-reference/using-the-sdk), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
|
|
133
133
|
|
|
134
134
|
Log from the same import, passing context as keyword arguments:
|
|
135
135
|
|
|
@@ -155,7 +155,8 @@ Not on macOS, or pinning a release in CI? See [Install the CLI](https://docs.rea
|
|
|
155
155
|
|
|
156
156
|
## Learn more
|
|
157
157
|
|
|
158
|
-
- [Quickstart](https://docs.reactor.inc/deploy/development/
|
|
158
|
+
- [Quickstart](https://docs.reactor.inc/deploy/development/quickstart): from zero to a model deployed on Reactor's GPUs
|
|
159
|
+
- [Runtime overview](https://docs.reactor.inc/deploy/development/overview): what the runtime handles, and the outline of a model
|
|
159
160
|
- [Model anatomy](https://docs.reactor.inc/deploy/development/reactor-app/model-anatomy): every member of a `ReactorApp`, line by line
|
|
160
161
|
- [The step loop](https://docs.reactor.inc/deploy/development/reactor-app/step-loop): `process_input()`, `generate()`, `process_output()`, and the frame rate
|
|
161
162
|
- [Starter example](./examples/starter/README.md): the model `reactor init` scaffolds: one class, `generate()` alone, the smallest complete `ReactorApp`
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Build real-time AI models in Python.**
|
|
6
6
|
|
|
7
|
-
[📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/
|
|
7
|
+
[📖 Documentation](https://docs.reactor.inc/deploy/overview) · [🚀 Quickstart](https://docs.reactor.inc/deploy/development/quickstart) · [🌐 Reactor](https://reactor.inc)
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
@@ -103,7 +103,7 @@ cd my-model
|
|
|
103
103
|
reactor run
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
`reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
|
|
106
|
+
`reactor run` builds a container with the runtime inside and serves WebRTC signaling on port 8080. Point a browser at it with the [JS SDK](https://docs.reactor.inc/sdk-reference/using-the-sdk), or connect from the [Reactor Sandbox](https://reactor-sandbox.vercel.app/) and watch frames stream immediately.
|
|
107
107
|
|
|
108
108
|
Log from the same import, passing context as keyword arguments:
|
|
109
109
|
|
|
@@ -129,7 +129,8 @@ Not on macOS, or pinning a release in CI? See [Install the CLI](https://docs.rea
|
|
|
129
129
|
|
|
130
130
|
## Learn more
|
|
131
131
|
|
|
132
|
-
- [Quickstart](https://docs.reactor.inc/deploy/development/
|
|
132
|
+
- [Quickstart](https://docs.reactor.inc/deploy/development/quickstart): from zero to a model deployed on Reactor's GPUs
|
|
133
|
+
- [Runtime overview](https://docs.reactor.inc/deploy/development/overview): what the runtime handles, and the outline of a model
|
|
133
134
|
- [Model anatomy](https://docs.reactor.inc/deploy/development/reactor-app/model-anatomy): every member of a `ReactorApp`, line by line
|
|
134
135
|
- [The step loop](https://docs.reactor.inc/deploy/development/reactor-app/step-loop): `process_input()`, `generate()`, `process_output()`, and the frame rate
|
|
135
136
|
- [Starter example](./examples/starter/README.md): the model `reactor init` scaffolds: one class, `generate()` alone, the smallest complete `ReactorApp`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Run a model in its own process, or in one process per GPU, behind one object.
|
|
2
|
+
|
|
3
|
+
A worker is a class with ``load()``, ``generate()``, and ``reset()``. A
|
|
4
|
+
:class:`DistributedRunner` starts N of them, one per GPU, and lets the caller
|
|
5
|
+
drive all of them with one call. The caller writes no process, queue,
|
|
6
|
+
shared-memory, or NCCL code::
|
|
7
|
+
|
|
8
|
+
runner = DistributedRunner(MyModel, load_kwargs={"config_path": path})
|
|
9
|
+
runner.start()
|
|
10
|
+
result = runner.generate(MyInput(...))
|
|
11
|
+
runner.reset()
|
|
12
|
+
runner.shutdown()
|
|
13
|
+
|
|
14
|
+
This package is standalone. Nothing else in the runtime depends on it, and it
|
|
15
|
+
reads no manifest and no environment variable of its own.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from reactor_runtime.distributed.errors import (
|
|
19
|
+
RankDesync,
|
|
20
|
+
SharedSlotAllocationFailed,
|
|
21
|
+
WorkerCrashed,
|
|
22
|
+
WorkerTimeout,
|
|
23
|
+
)
|
|
24
|
+
from reactor_runtime.distributed.runner import DistributedRunner
|
|
25
|
+
from reactor_runtime.distributed.worker import DistributedWorker
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"DistributedRunner",
|
|
29
|
+
"DistributedWorker",
|
|
30
|
+
"RankDesync",
|
|
31
|
+
"SharedSlotAllocationFailed",
|
|
32
|
+
"WorkerCrashed",
|
|
33
|
+
"WorkerTimeout",
|
|
34
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Failures a :class:`~reactor_runtime.distributed.DistributedRunner` raises to its caller.
|
|
2
|
+
|
|
3
|
+
A worker's own exception is never wrapped: when every rank raises the same
|
|
4
|
+
error, the caller receives rank 0's instance unchanged. The types here cover
|
|
5
|
+
what the runner itself detects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkerCrashed(RuntimeError): # noqa: N818 (named for the event a caller catches)
|
|
12
|
+
"""A worker process died without answering.
|
|
13
|
+
|
|
14
|
+
The message names the rank and its exit code. The group is unusable after
|
|
15
|
+
this; ``shutdown()`` ends it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkerTimeout(TimeoutError): # noqa: N818 (named for the event a caller catches)
|
|
20
|
+
"""A call did not get an answer from every rank within its timeout.
|
|
21
|
+
|
|
22
|
+
The processes are alive but one of them is stuck, usually inside a
|
|
23
|
+
collective a peer abandoned. The group is unusable after this.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RankDesync(RuntimeError): # noqa: N818 (named for the state a caller catches)
|
|
28
|
+
"""Some ranks raised and others did not, or ranks raised different types of error.
|
|
29
|
+
|
|
30
|
+
The message names the first failing rank and carries its exception as
|
|
31
|
+
``__cause__``. Rank 0's result, if any, came out of a collective a failing
|
|
32
|
+
rank was meant to contribute to, so it is not returned. The group refuses
|
|
33
|
+
further calls.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SharedSlotAllocationFailed(MemoryError): # noqa: N818 (named for the event a caller catches)
|
|
38
|
+
"""A shared-memory block could not be created or grown.
|
|
39
|
+
|
|
40
|
+
The message states the size that was asked for and, where the host exposes
|
|
41
|
+
it, the size of ``/dev/shm``. Docker's default is 64 MB; ``--shm-size`` on
|
|
42
|
+
``docker run`` and a ``medium: Memory`` ``emptyDir`` on a pod raise it.
|
|
43
|
+
"""
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Carry an author's object across a process boundary without copying it twice.
|
|
2
|
+
|
|
3
|
+
:func:`pack` pickles an object with protocol 5. Every flat buffer the object
|
|
4
|
+
holds, a contiguous numpy array above all, is handed to the writer's
|
|
5
|
+
:class:`SharedSlot` instead of being written into the pickle. The pickle
|
|
6
|
+
stays small and travels on a queue; the bytes sit in shared memory. The
|
|
7
|
+
header names the block, so the reader attaches to whatever it is told and
|
|
8
|
+
the block can grow without a protocol of its own.
|
|
9
|
+
|
|
10
|
+
Every block a slot creates is named from the slot's prefix and a generation
|
|
11
|
+
number. A process that knows the prefix can unlink what a writer left behind
|
|
12
|
+
when the writer was terminated before it could close its slot.
|
|
13
|
+
|
|
14
|
+
A torch tensor and a non-contiguous array do not expose a flat buffer and
|
|
15
|
+
pickle inline. That still works, only slower, and is reported once per
|
|
16
|
+
process when the header passes about one megabyte.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import contextlib
|
|
22
|
+
import pickle
|
|
23
|
+
import secrets
|
|
24
|
+
import shutil
|
|
25
|
+
from multiprocessing import shared_memory
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from reactor_runtime.distributed.errors import SharedSlotAllocationFailed
|
|
30
|
+
from reactor_runtime.log import get_logger
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
_INITIAL_BYTES = 1 << 20
|
|
35
|
+
_INLINE_WARN_BYTES = 1 << 20
|
|
36
|
+
_SHM_DIR = Path("/dev/shm")
|
|
37
|
+
# Growth at least doubles a block, so no slot reaches this many generations.
|
|
38
|
+
_MAX_GENERATIONS = 64
|
|
39
|
+
|
|
40
|
+
_inline_reported = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def new_prefix() -> str:
|
|
44
|
+
"""Return a block-name prefix for one slot. Short enough for macOS's 31-character limit."""
|
|
45
|
+
return f"rr{secrets.token_hex(4)}_"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SharedSlot:
|
|
49
|
+
"""One shared-memory block a writer fills, one call at a time.
|
|
50
|
+
|
|
51
|
+
A call starts with :meth:`begin`, appends buffers with :meth:`write`, and
|
|
52
|
+
hands the returned spans to the reader inside the header. When a call's
|
|
53
|
+
buffers do not fit, the block is replaced by a larger one and the bytes
|
|
54
|
+
already written move with it. Steady state is one block at the writer's
|
|
55
|
+
high-water mark.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
prefix: Names every block this slot creates, as ``<prefix><generation>``.
|
|
59
|
+
A process that holds the prefix can reclaim the blocks with
|
|
60
|
+
:func:`unlink_blocks` if the writer dies without :meth:`close`.
|
|
61
|
+
A fresh one from :func:`new_prefix` when omitted.
|
|
62
|
+
initial_bytes: The size of the first block.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, *, prefix: str | None = None, initial_bytes: int = _INITIAL_BYTES) -> None:
|
|
66
|
+
self._prefix = prefix or new_prefix()
|
|
67
|
+
self._generation = 0
|
|
68
|
+
self._shm = _allocate(f"{self._prefix}0", initial_bytes)
|
|
69
|
+
self._cursor = 0
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def name(self) -> str:
|
|
73
|
+
"""The name a reader attaches to. Changes when the block grows."""
|
|
74
|
+
return self._shm.name
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def capacity(self) -> int:
|
|
78
|
+
"""Bytes the current block holds."""
|
|
79
|
+
return self._shm.size
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def _buf(self) -> memoryview:
|
|
83
|
+
return _buffer_of(self._shm)
|
|
84
|
+
|
|
85
|
+
def begin(self) -> None:
|
|
86
|
+
"""Start a new call; the next write lands at offset zero."""
|
|
87
|
+
self._cursor = 0
|
|
88
|
+
|
|
89
|
+
def write(self, buffer: Any) -> tuple[int, int]:
|
|
90
|
+
"""Append one flat buffer and return its ``(offset, length)`` span."""
|
|
91
|
+
if isinstance(buffer, pickle.PickleBuffer):
|
|
92
|
+
raw = buffer.raw()
|
|
93
|
+
else:
|
|
94
|
+
raw = memoryview(buffer).cast("B")
|
|
95
|
+
length = raw.nbytes
|
|
96
|
+
end = self._cursor + length
|
|
97
|
+
if end > self.capacity:
|
|
98
|
+
self._grow(end)
|
|
99
|
+
self._buf[self._cursor : end] = raw
|
|
100
|
+
span = (self._cursor, length)
|
|
101
|
+
self._cursor = end
|
|
102
|
+
return span
|
|
103
|
+
|
|
104
|
+
def close(self) -> None:
|
|
105
|
+
"""Detach and unlink the block."""
|
|
106
|
+
with contextlib.suppress(Exception):
|
|
107
|
+
self._shm.close()
|
|
108
|
+
with contextlib.suppress(Exception):
|
|
109
|
+
self._shm.unlink()
|
|
110
|
+
|
|
111
|
+
def _grow(self, needed: int) -> None:
|
|
112
|
+
self._generation += 1
|
|
113
|
+
replacement = _allocate(f"{self._prefix}{self._generation}", max(needed, 2 * self.capacity))
|
|
114
|
+
_buffer_of(replacement)[: self._cursor] = self._buf[: self._cursor]
|
|
115
|
+
logger.info("shared block grown", from_bytes=self.capacity, to_bytes=replacement.size)
|
|
116
|
+
self.close()
|
|
117
|
+
self._shm = replacement
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SlotReader:
|
|
121
|
+
"""The reading side: attaches to the block a header names and keeps the mapping."""
|
|
122
|
+
|
|
123
|
+
def __init__(self) -> None:
|
|
124
|
+
self._shm: shared_memory.SharedMemory | None = None
|
|
125
|
+
|
|
126
|
+
def attach(self, name: str) -> None:
|
|
127
|
+
"""Map the block called *name*, replacing the previous mapping if it differs."""
|
|
128
|
+
if self._shm is not None and self._shm.name == name:
|
|
129
|
+
return
|
|
130
|
+
self.close()
|
|
131
|
+
self._shm = shared_memory.SharedMemory(name=name)
|
|
132
|
+
|
|
133
|
+
def copy(self, offset: int, length: int) -> bytearray:
|
|
134
|
+
"""Copy one span out. A copy, so the writer may reuse the block at once."""
|
|
135
|
+
if self._shm is None:
|
|
136
|
+
raise RuntimeError("attach() has not run")
|
|
137
|
+
return bytearray(_buffer_of(self._shm)[offset : offset + length])
|
|
138
|
+
|
|
139
|
+
def close(self) -> None:
|
|
140
|
+
"""Drop the mapping. The writer owns the block and unlinks it."""
|
|
141
|
+
if self._shm is not None:
|
|
142
|
+
with contextlib.suppress(Exception):
|
|
143
|
+
self._shm.close()
|
|
144
|
+
self._shm = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def pack(obj: Any, slot: SharedSlot) -> bytes:
|
|
148
|
+
"""Pickle *obj* with its flat buffers written into *slot*.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A header for the queue: the pickle bytes, the block's name, and the
|
|
152
|
+
span of each out-of-band buffer, pickled together.
|
|
153
|
+
"""
|
|
154
|
+
slot.begin()
|
|
155
|
+
spans: list[tuple[int, int]] = []
|
|
156
|
+
|
|
157
|
+
def out_of_band(buffer: pickle.PickleBuffer) -> None:
|
|
158
|
+
spans.append(slot.write(buffer))
|
|
159
|
+
|
|
160
|
+
blob = pickle.dumps(obj, protocol=5, buffer_callback=out_of_band)
|
|
161
|
+
if len(blob) > _INLINE_WARN_BYTES:
|
|
162
|
+
_report_inline(len(blob))
|
|
163
|
+
return pickle.dumps((blob, slot.name, spans), protocol=5)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def unpack(header: bytes, reader: SlotReader) -> Any:
|
|
167
|
+
"""Rebuild the object a :func:`pack` header describes."""
|
|
168
|
+
blob, name, spans = pickle.loads(header)
|
|
169
|
+
reader.attach(name)
|
|
170
|
+
return pickle.loads(blob, buffers=[reader.copy(offset, length) for offset, length in spans])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def unlink_blocks(prefix: str) -> None:
|
|
174
|
+
"""Unlink every block a :class:`SharedSlot` with *prefix* left behind.
|
|
175
|
+
|
|
176
|
+
For a writer that ended without closing its slot, such as a terminated or
|
|
177
|
+
killed process. Every generation the slot could have reached is tried, so
|
|
178
|
+
a block that grew after its last header was read is reclaimed too. Call it
|
|
179
|
+
only once the writer is dead: a live writer's block would go with the rest.
|
|
180
|
+
"""
|
|
181
|
+
for generation in range(_MAX_GENERATIONS):
|
|
182
|
+
try:
|
|
183
|
+
shm = shared_memory.SharedMemory(name=f"{prefix}{generation}")
|
|
184
|
+
except FileNotFoundError:
|
|
185
|
+
continue
|
|
186
|
+
with contextlib.suppress(Exception):
|
|
187
|
+
shm.close()
|
|
188
|
+
with contextlib.suppress(Exception):
|
|
189
|
+
shm.unlink()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _allocate(name: str, size: int) -> shared_memory.SharedMemory:
|
|
193
|
+
if _SHM_DIR.exists():
|
|
194
|
+
usage = shutil.disk_usage(_SHM_DIR)
|
|
195
|
+
if size > usage.free:
|
|
196
|
+
raise SharedSlotAllocationFailed(
|
|
197
|
+
f"could not grow the shared buffer to {size / 2**20:.1f} MB. /dev/shm has "
|
|
198
|
+
f"{usage.total / 2**20:.1f} MB total and {usage.free / 2**20:.1f} MB free. "
|
|
199
|
+
"Run with a larger --shm-size, or size /dev/shm in the deployment."
|
|
200
|
+
)
|
|
201
|
+
try:
|
|
202
|
+
return shared_memory.SharedMemory(name=name, create=True, size=size)
|
|
203
|
+
except OSError as exc:
|
|
204
|
+
raise SharedSlotAllocationFailed(
|
|
205
|
+
f"could not allocate a shared buffer of {size / 2**20:.1f} MB: {exc}. "
|
|
206
|
+
"Run with a larger --shm-size, or size /dev/shm in the deployment."
|
|
207
|
+
) from exc
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _buffer_of(shm: shared_memory.SharedMemory) -> memoryview:
|
|
211
|
+
buf = shm.buf
|
|
212
|
+
if buf is None:
|
|
213
|
+
raise RuntimeError(f"shared block {shm.name} is closed")
|
|
214
|
+
return buf
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _report_inline(size: int) -> None:
|
|
218
|
+
global _inline_reported
|
|
219
|
+
if _inline_reported:
|
|
220
|
+
return
|
|
221
|
+
_inline_reported = True
|
|
222
|
+
logger.warning(
|
|
223
|
+
"a call's header is large; some payload pickled inline instead of through shared memory. "
|
|
224
|
+
"A torch tensor or a non-contiguous array does that: pass np.ascontiguousarray(x) or "
|
|
225
|
+
"np.asarray(tensor.cpu())",
|
|
226
|
+
header_bytes=size,
|
|
227
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The messages between a runner and its ranks.
|
|
2
|
+
|
|
3
|
+
Requests go from the runner to every rank, in the same order on every rank's
|
|
4
|
+
queue. Every rank answers every request. A large payload never rides on a
|
|
5
|
+
queue: :class:`Generate` and :class:`Answer` carry a header from
|
|
6
|
+
:func:`~reactor_runtime.distributed.ipc.pack`, and the bytes sit in a shared
|
|
7
|
+
block the header names.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Generate:
|
|
17
|
+
"""Run one ``generate()`` on every rank with the input the header describes."""
|
|
18
|
+
|
|
19
|
+
header: bytes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Reset:
|
|
24
|
+
"""Call ``reset()`` on every rank."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Shutdown:
|
|
29
|
+
"""Leave the request loop, run the exit barrier, and end the process."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Rendezvous:
|
|
34
|
+
"""Rank 0 listens for the process group on this port, which it bound itself."""
|
|
35
|
+
|
|
36
|
+
port: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Join:
|
|
41
|
+
"""Join the process group on the port rank 0 reported. Sent to every other rank."""
|
|
42
|
+
|
|
43
|
+
port: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class Loaded:
|
|
48
|
+
"""A rank finished ``load()`` and is ready for requests."""
|
|
49
|
+
|
|
50
|
+
rank: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Answer:
|
|
55
|
+
"""One rank's reply to a :class:`Generate` or :class:`Reset`.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
rank: The answering rank.
|
|
59
|
+
header: Rank 0's packed result on a successful ``Generate``. Other
|
|
60
|
+
ranks and every ``Reset`` send ``None``.
|
|
61
|
+
error: What the worker raised, or ``None`` on success.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
rank: int
|
|
65
|
+
header: bytes | None = None
|
|
66
|
+
error: Exception | None = None
|