learning-loop-node 0.24.0__tar.gz → 0.25.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.
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/PKG-INFO +16 -1
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/README.md +15 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/trainer.py +2 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/misc.py +1 -1
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/exceptions.py +12 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/trainer_logic_generic.py +35 -5
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/trainer_node.py +9 -2
- learning_loop_node-0.25.0/learning_loop_node/trainer/training_slot.py +207 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/PKG-INFO +16 -1
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/SOURCES.txt +1 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/pyproject.toml +1 -1
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/annotation/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/annotation/annotator_node.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/annotation_data.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/annotations.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/detections.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/general.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/image_metadata.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/socket_response.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/training.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_exchanger.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/categories.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/detector_logic.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/detector_node.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/exceptions.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/geometry.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/outbox.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/postprocess.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/about.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/detect.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/upload.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/annotator.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/detector.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/general.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/globals.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/background_tasks.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/entrypoint.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/environment_reader.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/log_conf.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/run.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/loop_communication.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/node.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/py.typed +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/rest.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/batch_size.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/cuda.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/downloader.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/executor.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/io_helpers.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/metrics.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/rest/__init__.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/subprocess.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/test_executor.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/trainer_logic.py +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/dependency_links.txt +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/requires.txt +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/top_level.txt +0 -0
- {learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
|
|
5
5
|
Author-email: Zauberzeug GmbH <info@zauberzeug.com>
|
|
6
6
|
License: MIT
|
|
@@ -67,6 +67,7 @@ You can configure connection to our Learning Loop by specifying the following en
|
|
|
67
67
|
| RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
|
|
68
68
|
| KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
|
|
69
69
|
| TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
|
|
70
|
+
| TRAINING_SLOT_LOCK | - | Lock file shared by the trainers on one GPU (see Trainer Node) | Trainer (opt.) | - (disabled) |
|
|
70
71
|
| USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) | 0 |
|
|
71
72
|
|
|
72
73
|
Note that organization and project IDs are always lower case and may differ from the names in the Learning Loop which can have uppercase letters.
|
|
@@ -250,6 +251,20 @@ Trainers fetch the images and anntoations from the Learning Loop to train new mo
|
|
|
250
251
|
|
|
251
252
|
- if the command line tool "jpeginfo" is installed, the downloader will drop corrupted images automatically
|
|
252
253
|
|
|
254
|
+
### Several trainers on one GPU
|
|
255
|
+
|
|
256
|
+
Trainers that share a GPU (say, one per model architecture on the same machine) coordinate through
|
|
257
|
+
a lock file: mount one host directory into each of them and point `TRAINING_SLOT_LOCK` at a file
|
|
258
|
+
inside it, e.g. `/slot/training.lock`. A trainer takes the lock when it accepts a training and holds
|
|
259
|
+
it until the training is cleaned up, including detection and upload. While a sibling holds it, the
|
|
260
|
+
others report the state `blocked` instead of `idle`, so the Learning Loop neither offers them for a
|
|
261
|
+
training nor dispatches queued jobs to them; a trainer that is sent `begin_training` regardless
|
|
262
|
+
refuses it with `{"success": false, "error_msg": ...}` in the acknowledgement and sets nothing up. A
|
|
263
|
+
training resumed after a restart cannot be refused — it waits for the lock instead, reporting
|
|
264
|
+
`waiting_for_slot` meanwhile. The lock is an `flock`, so the kernel releases it whenever the holding
|
|
265
|
+
process ends, however it ends — there is nothing to clean up after a crash. Mount the directory,
|
|
266
|
+
not the file, and never replace the file: the lock belongs to its inode.
|
|
267
|
+
|
|
253
268
|
## Converter Node
|
|
254
269
|
|
|
255
270
|
A Conveter Node converts models from one format into another.
|
|
@@ -34,6 +34,7 @@ You can configure connection to our Learning Loop by specifying the following en
|
|
|
34
34
|
| RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
|
|
35
35
|
| KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
|
|
36
36
|
| TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
|
|
37
|
+
| TRAINING_SLOT_LOCK | - | Lock file shared by the trainers on one GPU (see Trainer Node) | Trainer (opt.) | - (disabled) |
|
|
37
38
|
| USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) | 0 |
|
|
38
39
|
|
|
39
40
|
Note that organization and project IDs are always lower case and may differ from the names in the Learning Loop which can have uppercase letters.
|
|
@@ -217,6 +218,20 @@ Trainers fetch the images and anntoations from the Learning Loop to train new mo
|
|
|
217
218
|
|
|
218
219
|
- if the command line tool "jpeginfo" is installed, the downloader will drop corrupted images automatically
|
|
219
220
|
|
|
221
|
+
### Several trainers on one GPU
|
|
222
|
+
|
|
223
|
+
Trainers that share a GPU (say, one per model architecture on the same machine) coordinate through
|
|
224
|
+
a lock file: mount one host directory into each of them and point `TRAINING_SLOT_LOCK` at a file
|
|
225
|
+
inside it, e.g. `/slot/training.lock`. A trainer takes the lock when it accepts a training and holds
|
|
226
|
+
it until the training is cleaned up, including detection and upload. While a sibling holds it, the
|
|
227
|
+
others report the state `blocked` instead of `idle`, so the Learning Loop neither offers them for a
|
|
228
|
+
training nor dispatches queued jobs to them; a trainer that is sent `begin_training` regardless
|
|
229
|
+
refuses it with `{"success": false, "error_msg": ...}` in the acknowledgement and sets nothing up. A
|
|
230
|
+
training resumed after a restart cannot be refused — it waits for the lock instead, reporting
|
|
231
|
+
`waiting_for_slot` meanwhile. The lock is an `flock`, so the kernel releases it whenever the holding
|
|
232
|
+
process ends, however it ends — there is nothing to clean up after a crash. Mount the directory,
|
|
233
|
+
not the file, and never replace the file: the lock belongs to its inode.
|
|
234
|
+
|
|
220
235
|
## Converter Node
|
|
221
236
|
|
|
222
237
|
A Conveter Node converts models from one format into another.
|
|
@@ -4,7 +4,9 @@ from enum import Enum
|
|
|
4
4
|
|
|
5
5
|
class TrainerState(str, Enum):
|
|
6
6
|
Idle = 'idle'
|
|
7
|
+
Blocked = 'blocked' # idle, but unable to take a training
|
|
7
8
|
Initialized = 'initialized'
|
|
9
|
+
WaitingForSlot = 'waiting_for_slot'
|
|
8
10
|
Preparing = 'preparing'
|
|
9
11
|
DataDownloading = 'data_downloading'
|
|
10
12
|
DataDownloaded = 'data_downloaded'
|
|
@@ -150,7 +150,7 @@ def ensure_socket_response(func):
|
|
|
150
150
|
if isinstance(value, bool):
|
|
151
151
|
return asdict(SocketResponse.from_bool(value))
|
|
152
152
|
if isinstance(value, SocketResponse):
|
|
153
|
-
return value
|
|
153
|
+
return asdict(value)
|
|
154
154
|
if (args[0] in ['connect', 'disconnect', 'connect_error']):
|
|
155
155
|
return value
|
|
156
156
|
if value is None:
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/exceptions.py
RENAMED
|
@@ -5,6 +5,18 @@ class CriticalError(Exception):
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class TrainingRefusedError(Exception):
|
|
9
|
+
"""Raised when a trainer cannot take on a training it was asked to begin; nothing was set up."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TrainingSlotTakenError(TrainingRefusedError):
|
|
13
|
+
"""Raised when a sibling node holds the training slot."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, holder: str) -> None:
|
|
16
|
+
super().__init__(f'the training slot is held by {holder}')
|
|
17
|
+
self.holder = holder
|
|
18
|
+
|
|
19
|
+
|
|
8
20
|
class InsufficientMemoryError(RuntimeError):
|
|
9
21
|
"""Raised when not even the smallest unit of work fits in memory."""
|
|
10
22
|
|
|
@@ -26,8 +26,9 @@ from ..helpers.misc import (
|
|
|
26
26
|
is_valid_uuid4,
|
|
27
27
|
)
|
|
28
28
|
from .downloader import TrainingsDownloader
|
|
29
|
-
from .exceptions import CriticalError, NodeNeedsRestartError
|
|
29
|
+
from .exceptions import CriticalError, NodeNeedsRestartError, TrainingRefusedError
|
|
30
30
|
from .io_helpers import ActiveTrainingIO, EnvironmentVars, LastTrainingIO
|
|
31
|
+
from .training_slot import TrainingSlot, training_slot_from_env
|
|
31
32
|
|
|
32
33
|
if TYPE_CHECKING:
|
|
33
34
|
from .trainer_node import TrainerNode
|
|
@@ -37,7 +38,7 @@ logger = logging.getLogger('learning_loop_node.trainer_logic_generic')
|
|
|
37
38
|
|
|
38
39
|
class TrainerLogicGeneric(ABC):
|
|
39
40
|
|
|
40
|
-
def __init__(self, model_format: str) -> None:
|
|
41
|
+
def __init__(self, model_format: str, training_slot: TrainingSlot | None = None) -> None:
|
|
41
42
|
|
|
42
43
|
# NOTE: model_format is used in the file path for the model on the server:
|
|
43
44
|
# It acts as a key for list of files (cf. _get_latest_model_files)
|
|
@@ -54,6 +55,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
54
55
|
self._training: Optional[Training] = None
|
|
55
56
|
self._active_training_io: Optional[ActiveTrainingIO] = None
|
|
56
57
|
self._environment_vars = EnvironmentVars()
|
|
58
|
+
self.training_slot = training_slot or training_slot_from_env()
|
|
57
59
|
|
|
58
60
|
# ---------------------------------------- PROPERTIES TO AVOID CHECKING FOR NONE ----------------------------------------
|
|
59
61
|
|
|
@@ -98,9 +100,17 @@ class TrainerLogicGeneric(ABC):
|
|
|
98
100
|
|
|
99
101
|
@property
|
|
100
102
|
def state(self) -> str:
|
|
101
|
-
"""Returns the current state of the training.
|
|
103
|
+
"""Returns the current state of the training.
|
|
104
|
+
|
|
105
|
+
Without a training the state is `idle`, or `blocked` while a sibling node holds the training slot.
|
|
106
|
+
A training resumed after a restart reports `waiting_for_slot` until it gets the slot: it has a
|
|
107
|
+
training, so it is never `blocked`.
|
|
102
108
|
"""
|
|
103
|
-
if
|
|
109
|
+
if not self.training_active:
|
|
110
|
+
return TrainerState.Blocked.value if self.training_slot.taken_by_sibling else TrainerState.Idle.value
|
|
111
|
+
if not self.training_slot.held:
|
|
112
|
+
return TrainerState.WaitingForSlot.value
|
|
113
|
+
if self.training.training_state is None:
|
|
104
114
|
return TrainerState.Idle.value
|
|
105
115
|
return self.training.training_state
|
|
106
116
|
|
|
@@ -213,8 +223,21 @@ class TrainerLogicGeneric(ABC):
|
|
|
213
223
|
|
|
214
224
|
async def begin_training(self, organization: str, project: str, training_config: Dict) -> None:
|
|
215
225
|
"""Called on `begin_training` event from the Learning Loop.
|
|
226
|
+
|
|
227
|
+
The training slot is taken before anything is set up, so a refused training leaves the
|
|
228
|
+
previous training folders in place.
|
|
229
|
+
|
|
230
|
+
:raise TrainingRefusedError: if a training is active or a sibling node holds the training slot.
|
|
216
231
|
"""
|
|
217
|
-
self.
|
|
232
|
+
if self.training_active:
|
|
233
|
+
raise TrainingRefusedError(f'training {self.training.id} is still active')
|
|
234
|
+
context = Context(organization=organization, project=project)
|
|
235
|
+
self.training_slot.claim(self._slot_claim(context, training_config.get('training_number')))
|
|
236
|
+
try:
|
|
237
|
+
self._init_new_training(context, training_config)
|
|
238
|
+
except Exception:
|
|
239
|
+
self.training_slot.release()
|
|
240
|
+
raise
|
|
218
241
|
self._begin_training_task()
|
|
219
242
|
|
|
220
243
|
def _begin_training_task(self) -> None:
|
|
@@ -242,6 +265,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
242
265
|
"""
|
|
243
266
|
self.errors.reset_all()
|
|
244
267
|
try:
|
|
268
|
+
await self.training_slot.wait_for(self._slot_claim(self.training.context, self.training.training_number))
|
|
245
269
|
await self._training_loop()
|
|
246
270
|
except asyncio.CancelledError:
|
|
247
271
|
if not self.shutdown_event.is_set():
|
|
@@ -254,6 +278,12 @@ class TrainerLogicGeneric(ABC):
|
|
|
254
278
|
logger.info('CancelledError in _run - shutting down')
|
|
255
279
|
except Exception:
|
|
256
280
|
logger.exception('(Ignored) exception in trainer_logic._run:')
|
|
281
|
+
finally:
|
|
282
|
+
self._training = None
|
|
283
|
+
self.training_slot.release()
|
|
284
|
+
|
|
285
|
+
def _slot_claim(self, context: Context, training_number: Any) -> str:
|
|
286
|
+
return f'node {self.node.uuid}, training #{training_number} of {context.organization}/{context.project}'
|
|
257
287
|
|
|
258
288
|
# ---------------------------------------- TRAINING STATES ----------------------------------------
|
|
259
289
|
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/trainer_node.py
RENAMED
|
@@ -7,7 +7,10 @@ from typing import Dict, Optional
|
|
|
7
7
|
from fastapi.encoders import jsonable_encoder
|
|
8
8
|
from socketio import AsyncClient, exceptions
|
|
9
9
|
|
|
10
|
+
from ..data_classes import SocketResponse
|
|
11
|
+
from ..enums import TrainerState
|
|
10
12
|
from ..node import Node
|
|
13
|
+
from .exceptions import TrainingRefusedError
|
|
11
14
|
from .io_helpers import LastTrainingIO
|
|
12
15
|
from .rest import backdoor_controls
|
|
13
16
|
from .trainer_logic_generic import TrainerLogicGeneric
|
|
@@ -64,7 +67,11 @@ class TrainerNode(Node):
|
|
|
64
67
|
@sio_client.event
|
|
65
68
|
async def begin_training(organization: str, project: str, details: Dict):
|
|
66
69
|
self.log.info('received begin_training from server')
|
|
67
|
-
|
|
70
|
+
try:
|
|
71
|
+
await self.trainer_logic.begin_training(organization, project, details)
|
|
72
|
+
except TrainingRefusedError as e:
|
|
73
|
+
self.log.warning('refused begin_training: %s', e)
|
|
74
|
+
return SocketResponse(success=False, error_msg=str(e), payload='refused')
|
|
68
75
|
return True
|
|
69
76
|
|
|
70
77
|
@sio_client.event
|
|
@@ -102,7 +109,7 @@ class TrainerNode(Node):
|
|
|
102
109
|
if not self._idle_timeout:
|
|
103
110
|
return
|
|
104
111
|
|
|
105
|
-
if self.trainer_logic.state
|
|
112
|
+
if self.trainer_logic.state in (TrainerState.Idle, TrainerState.Blocked):
|
|
106
113
|
if self._first_idle_time is None:
|
|
107
114
|
self._first_idle_time = time.time()
|
|
108
115
|
idle_time = time.time() - self._first_idle_time
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""The right to use the GPU, shared between the trainer nodes on one machine.
|
|
2
|
+
|
|
3
|
+
A node holds the slot from the moment it accepts a training until the training is cleared.
|
|
4
|
+
"""
|
|
5
|
+
import asyncio
|
|
6
|
+
import fcntl
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import weakref
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .exceptions import TrainingSlotTakenError
|
|
15
|
+
|
|
16
|
+
ENV_VAR = 'TRAINING_SLOT_LOCK'
|
|
17
|
+
_CLAIM_ATTEMPTS = 3
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger('learning_loop_node.training_slot')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TrainingSlot(ABC):
|
|
23
|
+
"""The slot as a trainer uses it; an adapter implements `_acquire`, `_sibling_holder`, `release` and `held`."""
|
|
24
|
+
|
|
25
|
+
def claim(self, training: str) -> None:
|
|
26
|
+
"""Take the slot for `training` without waiting; a no-op if this slot already holds it.
|
|
27
|
+
|
|
28
|
+
:raise TrainingSlotTakenError: if a sibling node holds the slot.
|
|
29
|
+
"""
|
|
30
|
+
holder = self._describe(training)
|
|
31
|
+
for _ in range(_CLAIM_ATTEMPTS - 1):
|
|
32
|
+
try:
|
|
33
|
+
self._acquire(holder)
|
|
34
|
+
return
|
|
35
|
+
except TrainingSlotTakenError:
|
|
36
|
+
if self._sibling_holder() is not None:
|
|
37
|
+
raise
|
|
38
|
+
self._acquire(holder)
|
|
39
|
+
|
|
40
|
+
async def wait_for(self, training: str) -> None:
|
|
41
|
+
"""Take the slot for `training`, waiting as long as a sibling node holds it."""
|
|
42
|
+
try:
|
|
43
|
+
self.claim(training)
|
|
44
|
+
return
|
|
45
|
+
except TrainingSlotTakenError as e:
|
|
46
|
+
logger.info('%s - waiting', e)
|
|
47
|
+
while True:
|
|
48
|
+
await asyncio.sleep(1)
|
|
49
|
+
try:
|
|
50
|
+
self.claim(training)
|
|
51
|
+
except TrainingSlotTakenError:
|
|
52
|
+
continue
|
|
53
|
+
logger.info('acquired training slot')
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def release(self) -> None:
|
|
58
|
+
"""Give the slot back; a no-op if this slot does not hold it."""
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def held(self) -> bool:
|
|
63
|
+
"""Whether this slot object holds the slot."""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def taken_by_sibling(self) -> bool:
|
|
67
|
+
return not self.held and self._sibling_holder() is not None
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def _acquire(self, holder: str) -> None:
|
|
71
|
+
"""Take the slot without waiting; a no-op if this slot already holds it.
|
|
72
|
+
|
|
73
|
+
:raise TrainingSlotTakenError: if the slot is held elsewhere, including by a momentary probe.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def _sibling_holder(self) -> str | None:
|
|
78
|
+
"""Who holds the slot, None if it is free or cannot be inspected; only asked while this slot does not hold it."""
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _describe(training: str) -> str:
|
|
82
|
+
return f'pid {os.getpid()} on {socket.gethostname()}, {training}'
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AlwaysFreeSlot(TrainingSlot):
|
|
86
|
+
"""The slot of a node that has the GPU to itself."""
|
|
87
|
+
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self._held = False
|
|
90
|
+
|
|
91
|
+
def release(self) -> None:
|
|
92
|
+
self._held = False
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def held(self) -> bool:
|
|
96
|
+
return self._held
|
|
97
|
+
|
|
98
|
+
def _acquire(self, holder: str) -> None:
|
|
99
|
+
self._held = True
|
|
100
|
+
|
|
101
|
+
def _sibling_holder(self) -> str | None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FileTrainingSlot(TrainingSlot):
|
|
106
|
+
"""A slot backed by an exclusive `flock` on a file every sibling node mounts.
|
|
107
|
+
|
|
108
|
+
The kernel drops the lock with the process, so a node that dies in any way frees the slot.
|
|
109
|
+
The lock belongs to the inode: the file is only ever truncated and rewritten in place, never
|
|
110
|
+
replaced, and the mounted path is its directory.
|
|
111
|
+
|
|
112
|
+
The file's content names the holder and is diagnostic only — `_sibling_holder` tells a free slot
|
|
113
|
+
from a taken one by trying the lock, and that probe holds the lock for an instant, so a
|
|
114
|
+
claim that loses against a probe tries again. The file is made writable for everyone,
|
|
115
|
+
since sibling containers may run as different users.
|
|
116
|
+
|
|
117
|
+
A forked child shares the parent's lock and would keep it alive after the parent releases it,
|
|
118
|
+
so the child closes its copy right after the fork; the parent's lock is unaffected.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
_instances: 'weakref.WeakSet[FileTrainingSlot]' = weakref.WeakSet()
|
|
122
|
+
|
|
123
|
+
def __init__(self, path: str | Path) -> None:
|
|
124
|
+
self.path = Path(path)
|
|
125
|
+
self._fd: int | None = None
|
|
126
|
+
FileTrainingSlot._instances.add(self)
|
|
127
|
+
|
|
128
|
+
def _acquire(self, holder: str) -> None:
|
|
129
|
+
if self._fd is not None:
|
|
130
|
+
return
|
|
131
|
+
fd = self._open()
|
|
132
|
+
try:
|
|
133
|
+
self._lock(fd)
|
|
134
|
+
os.ftruncate(fd, 0)
|
|
135
|
+
os.write(fd, holder.encode())
|
|
136
|
+
except BaseException:
|
|
137
|
+
os.close(fd)
|
|
138
|
+
raise
|
|
139
|
+
self._fd = fd
|
|
140
|
+
|
|
141
|
+
def release(self) -> None:
|
|
142
|
+
if self._fd is None:
|
|
143
|
+
return
|
|
144
|
+
try:
|
|
145
|
+
os.ftruncate(self._fd, 0)
|
|
146
|
+
except OSError:
|
|
147
|
+
logger.warning('cannot clear the holder from training slot %s', self.path, exc_info=True)
|
|
148
|
+
finally:
|
|
149
|
+
os.close(self._fd)
|
|
150
|
+
self._fd = None
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def held(self) -> bool:
|
|
154
|
+
return self._fd is not None
|
|
155
|
+
|
|
156
|
+
def _sibling_holder(self) -> str | None:
|
|
157
|
+
try:
|
|
158
|
+
fd = os.open(self.path, os.O_RDONLY)
|
|
159
|
+
except FileNotFoundError:
|
|
160
|
+
return None
|
|
161
|
+
except OSError:
|
|
162
|
+
logger.warning('cannot inspect training slot %s - treating it as free', self.path, exc_info=True)
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
self._lock(fd)
|
|
166
|
+
except TrainingSlotTakenError as e:
|
|
167
|
+
return e.holder
|
|
168
|
+
finally:
|
|
169
|
+
os.close(fd)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def _drop_inherited_locks(cls) -> None:
|
|
174
|
+
for slot in list(cls._instances):
|
|
175
|
+
if slot._fd is not None:
|
|
176
|
+
os.close(slot._fd)
|
|
177
|
+
slot._fd = None
|
|
178
|
+
|
|
179
|
+
def _open(self) -> int:
|
|
180
|
+
fd = os.open(self.path, os.O_RDWR | os.O_CREAT, 0o666)
|
|
181
|
+
try:
|
|
182
|
+
os.fchmod(fd, 0o666)
|
|
183
|
+
except PermissionError:
|
|
184
|
+
pass
|
|
185
|
+
return fd
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _lock(fd: int) -> None:
|
|
189
|
+
try:
|
|
190
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
191
|
+
except BlockingIOError as e:
|
|
192
|
+
raise TrainingSlotTakenError(os.read(fd, 4096).decode(errors='replace') or 'unknown') from e
|
|
193
|
+
|
|
194
|
+
os.register_at_fork(after_in_child=FileTrainingSlot._drop_inherited_locks) # pylint: disable=protected-access
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def training_slot_from_env() -> TrainingSlot:
|
|
198
|
+
"""The slot `TRAINING_SLOT_LOCK` names, or a free one if the variable is unset.
|
|
199
|
+
|
|
200
|
+
:raise FileNotFoundError: if the lock file's directory does not exist.
|
|
201
|
+
"""
|
|
202
|
+
path = os.environ.get(ENV_VAR)
|
|
203
|
+
if not path:
|
|
204
|
+
return AlwaysFreeSlot()
|
|
205
|
+
if not Path(path).parent.is_dir():
|
|
206
|
+
raise FileNotFoundError(f'{ENV_VAR}={path}: the directory does not exist; mount it from the host')
|
|
207
|
+
return FileTrainingSlot(path)
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
|
|
5
5
|
Author-email: Zauberzeug GmbH <info@zauberzeug.com>
|
|
6
6
|
License: MIT
|
|
@@ -67,6 +67,7 @@ You can configure connection to our Learning Loop by specifying the following en
|
|
|
67
67
|
| RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
|
|
68
68
|
| KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
|
|
69
69
|
| TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
|
|
70
|
+
| TRAINING_SLOT_LOCK | - | Lock file shared by the trainers on one GPU (see Trainer Node) | Trainer (opt.) | - (disabled) |
|
|
70
71
|
| USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) | 0 |
|
|
71
72
|
|
|
72
73
|
Note that organization and project IDs are always lower case and may differ from the names in the Learning Loop which can have uppercase letters.
|
|
@@ -250,6 +251,20 @@ Trainers fetch the images and anntoations from the Learning Loop to train new mo
|
|
|
250
251
|
|
|
251
252
|
- if the command line tool "jpeginfo" is installed, the downloader will drop corrupted images automatically
|
|
252
253
|
|
|
254
|
+
### Several trainers on one GPU
|
|
255
|
+
|
|
256
|
+
Trainers that share a GPU (say, one per model architecture on the same machine) coordinate through
|
|
257
|
+
a lock file: mount one host directory into each of them and point `TRAINING_SLOT_LOCK` at a file
|
|
258
|
+
inside it, e.g. `/slot/training.lock`. A trainer takes the lock when it accepts a training and holds
|
|
259
|
+
it until the training is cleaned up, including detection and upload. While a sibling holds it, the
|
|
260
|
+
others report the state `blocked` instead of `idle`, so the Learning Loop neither offers them for a
|
|
261
|
+
training nor dispatches queued jobs to them; a trainer that is sent `begin_training` regardless
|
|
262
|
+
refuses it with `{"success": false, "error_msg": ...}` in the acknowledgement and sets nothing up. A
|
|
263
|
+
training resumed after a restart cannot be refused — it waits for the lock instead, reporting
|
|
264
|
+
`waiting_for_slot` meanwhile. The lock is an `flock`, so the kernel releases it whenever the holding
|
|
265
|
+
process ends, however it ends — there is nothing to clean up after a crash. Mount the directory,
|
|
266
|
+
not the file, and never replace the file: the lock belongs to its inode.
|
|
267
|
+
|
|
253
268
|
## Converter Node
|
|
254
269
|
|
|
255
270
|
A Conveter Node converts models from one format into another.
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/SOURCES.txt
RENAMED
|
@@ -68,5 +68,6 @@ learning_loop_node/trainer/test_executor.py
|
|
|
68
68
|
learning_loop_node/trainer/trainer_logic.py
|
|
69
69
|
learning_loop_node/trainer/trainer_logic_generic.py
|
|
70
70
|
learning_loop_node/trainer/trainer_node.py
|
|
71
|
+
learning_loop_node/trainer/training_slot.py
|
|
71
72
|
learning_loop_node/trainer/rest/__init__.py
|
|
72
73
|
learning_loop_node/trainer/rest/backdoor_controls.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "learning_loop_node"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.0"
|
|
4
4
|
description = "Python Library for Nodes which connect to the Zauberzeug Learning Loop"
|
|
5
5
|
authors = [{ name = "Zauberzeug GmbH", email = "info@zauberzeug.com" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/general.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_classes/training.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/data_exchanger.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/categories.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/detector_node.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/exceptions.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/geometry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/outbox.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/postprocess.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/about.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/detect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/detector/rest/upload.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/annotator.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/enums/detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/entrypoint.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/helpers/log_conf.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/loop_communication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/batch_size.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/downloader.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/executor.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/io_helpers.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/metrics.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/rest/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/subprocess.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/test_executor.py
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node/trainer/trainer_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/requires.txt
RENAMED
|
File without changes
|
{learning_loop_node-0.24.0 → learning_loop_node-0.25.0}/learning_loop_node.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|