learning-loop-node 0.23.0__tar.gz → 0.24.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.
Files changed (76) hide show
  1. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/PKG-INFO +13 -1
  2. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/README.md +12 -0
  3. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/outbox.py +4 -2
  4. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/entrypoint.py +7 -1
  5. learning_loop_node-0.24.0/learning_loop_node/trainer/batch_size.py +126 -0
  6. learning_loop_node-0.24.0/learning_loop_node/trainer/cuda.py +279 -0
  7. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node.egg-info/PKG-INFO +13 -1
  8. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/pyproject.toml +1 -1
  9. learning_loop_node-0.23.0/learning_loop_node/trainer/batch_size.py +0 -81
  10. learning_loop_node-0.23.0/learning_loop_node/trainer/cuda.py +0 -137
  11. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/__init__.py +0 -0
  12. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/annotation/__init__.py +0 -0
  13. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
  14. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/annotation/annotator_node.py +0 -0
  15. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/__init__.py +0 -0
  16. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/annotation_data.py +0 -0
  17. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/annotations.py +0 -0
  18. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/detections.py +0 -0
  19. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/general.py +0 -0
  20. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/image_metadata.py +0 -0
  21. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/socket_response.py +0 -0
  22. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_classes/training.py +0 -0
  23. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/data_exchanger.py +0 -0
  24. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/__init__.py +0 -0
  25. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/categories.py +0 -0
  26. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/detector_logic.py +0 -0
  27. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/detector_node.py +0 -0
  28. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/exceptions.py +0 -0
  29. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/geometry.py +0 -0
  30. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  31. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
  32. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
  33. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/postprocess.py +0 -0
  34. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/__init__.py +0 -0
  35. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/about.py +0 -0
  36. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  37. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/detect.py +0 -0
  38. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
  39. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  40. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
  41. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/detector/rest/upload.py +0 -0
  42. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/enums/__init__.py +0 -0
  43. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/enums/annotator.py +0 -0
  44. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/enums/detector.py +0 -0
  45. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/enums/general.py +0 -0
  46. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/enums/trainer.py +0 -0
  47. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/globals.py +0 -0
  48. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/__init__.py +0 -0
  49. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/background_tasks.py +0 -0
  50. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/environment_reader.py +0 -0
  51. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  52. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/log_conf.py +0 -0
  53. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/misc.py +0 -0
  54. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/helpers/run.py +0 -0
  55. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/loop_communication.py +0 -0
  56. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/node.py +0 -0
  57. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/py.typed +0 -0
  58. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/rest.py +0 -0
  59. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/__init__.py +0 -0
  60. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/downloader.py +0 -0
  61. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/exceptions.py +0 -0
  62. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/executor.py +0 -0
  63. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/io_helpers.py +0 -0
  64. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/metrics.py +0 -0
  65. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/rest/__init__.py +0 -0
  66. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
  67. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/subprocess.py +0 -0
  68. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/test_executor.py +0 -0
  69. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/trainer_logic.py +0 -0
  70. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/trainer_logic_generic.py +0 -0
  71. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node/trainer/trainer_node.py +0 -0
  72. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node.egg-info/SOURCES.txt +0 -0
  73. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node.egg-info/dependency_links.txt +0 -0
  74. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node.egg-info/requires.txt +0 -0
  75. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/learning_loop_node.egg-info/top_level.txt +0 -0
  76. {learning_loop_node-0.23.0 → learning_loop_node-0.24.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning_loop_node
3
- Version: 0.23.0
3
+ Version: 0.24.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
@@ -60,8 +60,10 @@ You can configure connection to our Learning Loop by specifying the following en
60
60
  | LOOP_PROJECT | PROJECT | Project ID | Detector | - |
61
61
  | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.3 |
62
62
  | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.6 |
63
+ | MAX_OUTBOX_SIZE | - | max. number of non-priority images queued for upload | Detector (opt.) | 1000 |
63
64
  | EXCLUSIVE_MODEL_BUILD | - | Reject detections during update to save VRAM (set to 1) | Detector (opt.) | 0 |
64
65
  | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) | 10 |
66
+ | VRAM_LIMIT_GB | - | GPU memory (GB) a training may use; the batch size is probed against it (`--vram-limit-gb`, trainers built with `node_parser(vram_limit=True)`) | Trainer (opt.) | 0 (whole card) |
65
67
  | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
66
68
  | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
67
69
  | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
@@ -73,6 +75,16 @@ Where a name has an alias, either spelling works. If both are set to **different
73
75
  prefixed name wins and a warning names the value used — the variable is never treated as unset,
74
76
  which would otherwise let `LOOP_HOST` fall back to its default of `learning-loop.ai`.
75
77
 
78
+ #### Debug logging
79
+
80
+ Every node type (Detector, Trainer, Annotator, Converter) has a REST endpoint to switch its own log level between `INFO` and `DEBUG` at runtime. Send the plain text `on` or `off` with a PUT request:
81
+
82
+ - Turn debug logging on: `curl -X PUT -d "on" http://localhost/debug_logging`
83
+ - Turn debug logging off: `curl -X PUT -d "off" http://localhost/debug_logging`
84
+
85
+ The response repeats the new state. Any other body returns status code 400.
86
+ Only the node's own logger is affected; third-party loggers such as `httpx` keep their level. The setting is not persistent, a restart brings the log level back to `INFO`.
87
+
76
88
  #### Testing
77
89
 
78
90
  We use github actions for CI. Tests can also be executed locally by running
@@ -27,8 +27,10 @@ You can configure connection to our Learning Loop by specifying the following en
27
27
  | LOOP_PROJECT | PROJECT | Project ID | Detector | - |
28
28
  | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.3 |
29
29
  | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.6 |
30
+ | MAX_OUTBOX_SIZE | - | max. number of non-priority images queued for upload | Detector (opt.) | 1000 |
30
31
  | EXCLUSIVE_MODEL_BUILD | - | Reject detections during update to save VRAM (set to 1) | Detector (opt.) | 0 |
31
32
  | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) | 10 |
33
+ | VRAM_LIMIT_GB | - | GPU memory (GB) a training may use; the batch size is probed against it (`--vram-limit-gb`, trainers built with `node_parser(vram_limit=True)`) | Trainer (opt.) | 0 (whole card) |
32
34
  | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
33
35
  | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
34
36
  | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
@@ -40,6 +42,16 @@ Where a name has an alias, either spelling works. If both are set to **different
40
42
  prefixed name wins and a warning names the value used — the variable is never treated as unset,
41
43
  which would otherwise let `LOOP_HOST` fall back to its default of `learning-loop.ai`.
42
44
 
45
+ #### Debug logging
46
+
47
+ Every node type (Detector, Trainer, Annotator, Converter) has a REST endpoint to switch its own log level between `INFO` and `DEBUG` at runtime. Send the plain text `on` or `off` with a PUT request:
48
+
49
+ - Turn debug logging on: `curl -X PUT -d "on" http://localhost/debug_logging`
50
+ - Turn debug logging off: `curl -X PUT -d "off" http://localhost/debug_logging`
51
+
52
+ The response repeats the new state. Any other body returns status code 400.
53
+ Only the node's own logger is affected; third-party loggers such as `httpx` keep their level. The setting is not persistent, a restart brings the log level back to `INFO`.
54
+
43
55
  #### Testing
44
56
 
45
57
  We use github actions for CI. Tests can also be executed locally by running
@@ -37,7 +37,7 @@ class Outbox():
37
37
  It handles upload failures by splitting the upload into two smaller batches until the problematic image is identified - and removed.
38
38
  Any image can be saved to the normal or the priority queue.
39
39
  Images in the priority queue are uploaded first.
40
- The total queue length is limited to 1000 images.
40
+ The normal queue is limited to MAX_OUTBOX_SIZE images (default 1000); the oldest are dropped first.
41
41
  """
42
42
 
43
43
  def __init__(self) -> None:
@@ -59,7 +59,9 @@ class Outbox():
59
59
  self.log.info('Outbox initialized with target_uri: %s', self.target_uri)
60
60
 
61
61
  self.BATCH_SIZE = 20
62
- self.MAX_UPLOAD_LENGTH = 1000 # only affects the `upload_folders` list
62
+ self.MAX_UPLOAD_LENGTH = int(os.environ.get('MAX_OUTBOX_SIZE', '1000')) # only limits `upload_folders`
63
+ if self.MAX_UPLOAD_LENGTH <= 0:
64
+ raise ValueError(f'MAX_OUTBOX_SIZE must be positive, got {self.MAX_UPLOAD_LENGTH}')
63
65
  self.UPLOAD_INTERVAL_S = 5
64
66
  self.UPLOAD_TIMEOUT_S = 30
65
67
 
@@ -12,21 +12,27 @@ from argparse import Action, Namespace
12
12
  import configargparse
13
13
  import uvicorn
14
14
 
15
+ from ..trainer.batch_size import VRAM_LIMIT_GB_FLAG, VRAM_LIMIT_GB_HELP
16
+
15
17
  logger = logging.getLogger(__name__)
16
18
 
17
19
 
18
- def node_parser(*, description: str, legacy_env_prefix: str = '') -> configargparse.ArgumentParser:
20
+ def node_parser(*, description: str, legacy_env_prefix: str = '',
21
+ vram_limit: bool = False) -> configargparse.ArgumentParser:
19
22
  """Build the parser for a node, pre-loaded with the settings every node has.
20
23
 
21
24
  :param legacy_env_prefix: A prefix an earlier version of this node required, e.g.
22
25
  ``'MY_DETECTOR_'``. Both spellings keep working, with a warning: the prefix on the
23
26
  current name (``MY_DETECTOR_NODE_HOST``) and the prefix on the flag it was originally
24
27
  applied to (``MY_DETECTOR_HOST``). Leave empty for a node that never used one.
28
+ :param vram_limit: Add :data:`~learning_loop_node.trainer.batch_size.VRAM_LIMIT_GB_FLAG`.
25
29
  """
26
30
  parser = _NodeArgumentParser(description=description, legacy_env_prefix=legacy_env_prefix)
27
31
  parser.add_argument('--host', default='0.0.0.0', env_var='NODE_HOST',
28
32
  help='Host interface to bind to')
29
33
  parser.add_argument('--port', type=int, default=80, env_var='NODE_PORT', help='Port to bind to')
34
+ if vram_limit:
35
+ parser.add_argument(VRAM_LIMIT_GB_FLAG, type=float, default=0, help=VRAM_LIMIT_GB_HELP)
30
36
  return parser
31
37
 
32
38
 
@@ -0,0 +1,126 @@
1
+ """Choosing a batch size by probing, rather than configuring one.
2
+
3
+ The trainer supplies a ``fits`` predicate that runs a representative step at doubling sizes.
4
+ Only powers of two are visited, so equal hardware yields an equal recipe. Nothing here imports a
5
+ deep-learning framework.
6
+
7
+ Adapted from PyTorch Lightning's ``BatchSizeFinder`` (power-scaling mode).
8
+ Copyright The Lightning AI team. Licensed under the Apache License, Version 2.0.
9
+ https://github.com/Lightning-AI/pytorch-lightning
10
+ """
11
+
12
+ import logging
13
+ from collections.abc import Callable, Mapping
14
+ from typing import Any
15
+
16
+ from .exceptions import InsufficientMemoryError
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ REQUESTED_BATCH_SIZE = 'max_batch_size'
21
+ """The hyperparameter every node reads its `measure_batch_size` argument out of.
22
+
23
+ 0 or absent means the card decides. It is an input only: a trainer reports the size it settled on
24
+ under a different key, conventionally ``batch_size``, and never writes it back here.
25
+ """
26
+
27
+ VRAM_LIMIT_GB_FLAG = '--vram-limit-gb'
28
+ """The flag a trainer takes its GPU budget from; ``VRAM_LIMIT_GB`` follows from the name."""
29
+
30
+ VRAM_LIMIT_GB_HELP = ('Gigabytes of GPU memory a training may use. The batch size is probed against this limit '
31
+ 'instead of the whole card, so a lower limit yields a smaller batch size rather than an '
32
+ 'out-of-memory error. Use it to share a GPU or to keep headroom against fragmentation. '
33
+ "The limit is relative to the card's total memory, not to what is currently free. "
34
+ '0 (default) means no limit.')
35
+
36
+ MAX_BATCH_SIZE = 1024
37
+ """Where a search stops when its caller sets no bound of its own."""
38
+
39
+ NO_GPU_BATCH_SIZE = 8
40
+ """Batch size used when there is no GPU to probe."""
41
+
42
+ MIN_TRAIN_STEPS_PER_EPOCH = 8
43
+ """Fewest optimizer steps an epoch must have; :func:`dataset_limit` is derived from it."""
44
+
45
+
46
+ def find_batch_size(fits: Callable[[int], bool], *, limit: int, minimum: int = 1,
47
+ candidate: int = 0) -> int:
48
+ """Return the largest batch size that fits, never exceeding ``limit``.
49
+
50
+ Powers of two, plus ``minimum`` and ``candidate``, the two sizes outside that set this will
51
+ return, and only because somebody named them and they then measured.
52
+
53
+ :param fits: Runs a representative probe; ``False`` on out-of-memory.
54
+ :param limit: Upper bound; the doubling stops at the largest power of two within it.
55
+ :param minimum: Smallest size to try, as named; the doubling continues from the next power of
56
+ two above it. Raise it above one for a step that cannot run on a single sample at all:
57
+ BatchNorm over a 1x1 feature map, a validation pass that halves the batch.
58
+ :param candidate: An exact size, tried once the doubling has reached its ceiling, so a size
59
+ that was asked for is used as asked for rather than rounded down. Ignored unless it lies
60
+ between that ceiling and ``limit``; a bound nobody named — one derived from the dataset,
61
+ say — must not be passed here.
62
+ :raises InsufficientMemoryError: If not even ``minimum`` fits.
63
+ """
64
+ minimum = max(1, minimum)
65
+ bound = max(limit, minimum)
66
+ ceiling = max(smaller_pot(bound), minimum)
67
+
68
+ if not fits(minimum):
69
+ raise InsufficientMemoryError(f'batch size {minimum} does not fit in memory')
70
+
71
+ size = minimum
72
+ while size < ceiling and fits(smaller_pot(size) * 2):
73
+ size = smaller_pot(size) * 2
74
+
75
+ if size == ceiling and ceiling < candidate <= bound and fits(candidate):
76
+ size = candidate
77
+
78
+ return size
79
+
80
+
81
+ def requested_batch_size(hyperparameters: Mapping[str, Any]) -> int:
82
+ """The bound a training asked for, read out of the hyperparameters the loop sent.
83
+
84
+ An unfilled field arrives as absent, ``None`` or ``''``, and all three mean no bound.
85
+
86
+ :raises ValueError: If the value is there but is not a number.
87
+ """
88
+ return int(hyperparameters.get(REQUESTED_BATCH_SIZE, 0) or 0)
89
+
90
+
91
+ def dataset_limit(sample_count: int) -> int:
92
+ """The batch size ceiling that still leaves ``MIN_TRAIN_STEPS_PER_EPOCH`` steps per epoch."""
93
+ if sample_count < 1:
94
+ raise ValueError(f'sample_count must be >= 1, got {sample_count}')
95
+ return max(1, sample_count // MIN_TRAIN_STEPS_PER_EPOCH)
96
+
97
+
98
+ def no_gpu_batch_size(limit: int, probe: str, minimum: int = 1) -> int:
99
+ """The batch size to fall back on when there is no GPU to probe; never below ``minimum``."""
100
+ batch_size = max(min(smaller_pot(limit), NO_GPU_BATCH_SIZE), minimum)
101
+ logger.warning('%s: CUDA is unavailable; using batch size %d without probing', probe, batch_size)
102
+ return batch_size
103
+
104
+
105
+ def smaller_pot(n: int) -> int:
106
+ """The largest power of two that is <= ``n``."""
107
+ if n < 1:
108
+ raise ValueError(f'n must be >= 1, got {n}')
109
+ return 1 << (n.bit_length() - 1)
110
+
111
+
112
+ def batch_count(sample_count: int, batch_size: int) -> int:
113
+ """How many batches a loader yields, the last one possibly short."""
114
+ return -(-sample_count // batch_size)
115
+
116
+
117
+ def is_out_of_memory(exception: BaseException) -> bool:
118
+ """Whether the exception signals exhausted memory, on the GPU or the host.
119
+
120
+ cuDNN and cuBLAS workspace failures raise a plain ``RuntimeError``, so the message has to be
121
+ matched too.
122
+ """
123
+ if isinstance(exception, MemoryError):
124
+ return True
125
+ message = str(exception).lower()
126
+ return any(text in message for text in ('out of memory', 'alloc_failed', 'cuda error: unknown error'))
@@ -0,0 +1,279 @@
1
+ """GPU memory budgeting and batch-size probing for trainers that train in-process.
2
+
3
+ ``--vram-limit-gb`` -- gigabytes of the card this training may use -- becomes both the budget a
4
+ probe measures against and the cap that holds the process to it, on whichever GPU the calling
5
+ process is using. The cap is a share of the card's *total* memory, not of what is free.
6
+
7
+ This is the one module in the library that imports torch, which the package does not declare, so
8
+ only a trainer may import it. The search itself is in :mod:`~learning_loop_node.trainer.batch_size`.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import gc
13
+ import logging
14
+ from abc import ABC, abstractmethod
15
+ from argparse import ArgumentParser
16
+ from collections.abc import Callable
17
+
18
+ import torch
19
+
20
+ from .batch_size import (
21
+ MAX_BATCH_SIZE,
22
+ REQUESTED_BATCH_SIZE,
23
+ VRAM_LIMIT_GB_FLAG,
24
+ VRAM_LIMIT_GB_HELP,
25
+ dataset_limit,
26
+ find_batch_size,
27
+ is_out_of_memory,
28
+ no_gpu_batch_size,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ SAFETY_MARGIN = 0.05
34
+ """Share of the budget held back while probing, against allocator fragmentation later on."""
35
+
36
+ DEFAULT_STEPS_PER_TRIAL = 4
37
+ """Training steps a batch size must survive before it counts as fitting.
38
+
39
+ Some trainings reach their peak only after a few steps: an optimizer that allocates its state on
40
+ the first one, gradients that accumulate, a scaler that skips the first updates.
41
+ """
42
+
43
+ _budget_gb: float = 0
44
+ """What :func:`limit_cuda_memory` capped this process to; 0 means the whole card."""
45
+
46
+
47
+ class ProbeStep(ABC):
48
+ """A training cycle built for one batch-size probe and released once the probe is done.
49
+
50
+ :func:`measure_batch_size` builds it through a factory, after the safety margin is reserved,
51
+ and holds the only reference to it, so nothing built for the probe stays on the card for the
52
+ training that follows. A trial at one batch size is ``steps_per_trial`` calls of
53
+ :meth:`train_step` followed by one of :meth:`val_step`; a subclass implements both, the other
54
+ two are optional.
55
+ """
56
+
57
+ @abstractmethod
58
+ def train_step(self, batch_size: int) -> str | None:
59
+ """Run one training step at ``batch_size``; may return a detail to append to the log line."""
60
+
61
+ @abstractmethod
62
+ def val_step(self, batch_size: int) -> str | None:
63
+ """Run the validation a training at ``batch_size`` runs between epochs.
64
+
65
+ Validate at the batch size the training validates with, which need not be ``batch_size``
66
+ (YOLOv5 halves it). A training that does not validate returns ``None`` without running
67
+ anything.
68
+ """
69
+
70
+ def on_out_of_memory(self) -> None: # noqa: B027
71
+ """Drop what a trial that ran out of memory left behind (an optimizer's gradients, say)."""
72
+
73
+ def release(self) -> None: # noqa: B027
74
+ """Undo what building the step changed outside it, such as moving the real model off the card."""
75
+
76
+
77
+ def measure_batch_size(build_step: Callable[[], ProbeStep], *, max_batch_size: int = 0,
78
+ sample_count: int | None = None, probe: str = 'batch-size probe',
79
+ minimum: int = 1, steps_per_trial: int = DEFAULT_STEPS_PER_TRIAL) -> int:
80
+ """Settle a training's batch size against what it asked for and what the card allows.
81
+
82
+ Every training enters here, whatever shape its hyperparameters have; :func:`probe_batch_size` is
83
+ for a probe with no requested size to honour, such as a detection pass bounded only by how many
84
+ images there are. The step is built only once there is a card to probe.
85
+
86
+ :param build_step: Builds the step the trials run; called once, after the margin is reserved.
87
+ :param max_batch_size: What the training asked for, as carried in the
88
+ :data:`~.batch_size.REQUESTED_BATCH_SIZE` hyperparameter: the largest batch it may use,
89
+ measured rather than trusted. A size that fits is used as asked for, whether or not it is a
90
+ power of two; one that does not becomes the largest power of two below it that does. 0 means
91
+ the card decides alone. A request below ``minimum`` is raised to it, with a warning.
92
+ :param sample_count: Samples in the training split, when the caller knows it. The search is
93
+ then bounded by :func:`~.batch_size.dataset_limit`; that bound is never used as the exact
94
+ candidate.
95
+ :param probe: Names this probe in the log, so a node running several stays readable.
96
+ :param minimum: Smallest size to try, taking precedence over ``max_batch_size``; see
97
+ :func:`~.batch_size.find_batch_size`.
98
+ :param steps_per_trial: Training steps a size must survive, each trial then ending in one
99
+ validation step.
100
+ :raises InsufficientMemoryError: If not even ``minimum`` fits.
101
+ :raises ValueError: If the training asked for a negative batch size, or ``steps_per_trial`` is
102
+ below one.
103
+ """
104
+ if max_batch_size < 0:
105
+ raise ValueError(f'{REQUESTED_BATCH_SIZE} must be >= 0, got {max_batch_size}')
106
+ if steps_per_trial < 1:
107
+ raise ValueError(f'steps_per_trial must be >= 1, got {steps_per_trial}')
108
+ if 0 < max_batch_size < minimum:
109
+ logger.warning('%s: requested %s=%d is below the trainer minimum of %d; using %d', probe,
110
+ REQUESTED_BATCH_SIZE, max_batch_size, minimum, minimum)
111
+
112
+ limit = max_batch_size
113
+ if sample_count is not None:
114
+ limit = min(limit or MAX_BATCH_SIZE, dataset_limit(sample_count))
115
+ logger.info('%s: %d training samples allow at most %d per batch', probe, sample_count,
116
+ dataset_limit(sample_count))
117
+
118
+ return _probe(build_step, probe=probe, limit=limit, steps_per_trial=steps_per_trial,
119
+ candidate=max_batch_size, minimum=minimum)
120
+
121
+
122
+ def probe_batch_size(run_batch: Callable[[int], str | None], *, probe: str = 'batch-size probe',
123
+ limit: int = 0) -> int:
124
+ """Find the largest batch that fits for a pass with no requested size, such as a detection pass.
125
+
126
+ :param run_batch: Runs the batch; may return a detail to append to the log line.
127
+ :param probe: Names this probe in the log, so a node running several stays readable.
128
+ :param limit: Caps the search; 0 means
129
+ :data:`~learning_loop_node.trainer.batch_size.MAX_BATCH_SIZE`.
130
+ :raises InsufficientMemoryError: If not even a batch of one fits.
131
+ """
132
+ return _probe(lambda: _PassStep(run_batch), probe=probe, limit=limit, steps_per_trial=1)
133
+
134
+
135
+ class _PassStep(ProbeStep):
136
+ """A plain callable as a step; there is nothing of its own to build, validate or release."""
137
+
138
+ def __init__(self, run_batch: Callable[[int], str | None]) -> None:
139
+ self._run_batch = run_batch
140
+
141
+ def train_step(self, batch_size: int) -> str | None:
142
+ return self._run_batch(batch_size)
143
+
144
+ def val_step(self, batch_size: int) -> None:
145
+ return None
146
+
147
+
148
+ def _probe(build_step: Callable[[], ProbeStep], *, probe: str, limit: int, steps_per_trial: int,
149
+ candidate: int = 0, minimum: int = 1) -> int:
150
+ """Everything of a probe but the step: the GPU check, the margin, the search, the release.
151
+
152
+ The margin is a share of the budget :func:`limit_cuda_memory` set in this process.
153
+ """
154
+ bound = max(limit or MAX_BATCH_SIZE, minimum)
155
+
156
+ if not torch.cuda.is_available():
157
+ return no_gpu_batch_size(bound, probe, minimum)
158
+
159
+ margin = reserve_margin(probe=probe)
160
+ try:
161
+ chosen = _search(build_step, probe=probe, bound=bound, steps_per_trial=steps_per_trial,
162
+ minimum=minimum, candidate=candidate)
163
+ finally:
164
+ del margin
165
+ free_cuda_memory()
166
+ logger.info('%s: selected batch size %d (upper bound %d)', probe, chosen, bound)
167
+ return chosen
168
+
169
+
170
+ def _search(build_step: Callable[[], ProbeStep], *, probe: str, bound: int, steps_per_trial: int,
171
+ minimum: int, candidate: int) -> int:
172
+ """Build the step, search with it and release it; once this returns, nothing references it."""
173
+ step = build_step()
174
+ try:
175
+ fits = measured_fits(_trial(step, steps_per_trial), probe=probe, on_out_of_memory=step.on_out_of_memory)
176
+ return find_batch_size(fits, limit=bound, minimum=minimum, candidate=candidate)
177
+ finally:
178
+ step.release()
179
+
180
+
181
+ def _trial(step: ProbeStep, steps_per_trial: int) -> Callable[[int], str | None]:
182
+ """One trial of ``step``: its training steps, then its validation."""
183
+ def run_trial(batch_size: int) -> str | None:
184
+ train_detail = None
185
+ for _ in range(steps_per_trial):
186
+ train_detail = step.train_step(batch_size)
187
+ val_detail = step.val_step(batch_size)
188
+ return '; '.join(detail for detail in (train_detail, val_detail) if detail) or None
189
+
190
+ return run_trial
191
+
192
+
193
+ def measured_fits(run_batch: Callable[[int], str | None], *, probe: str,
194
+ on_out_of_memory: Callable[[], None] | None = None) -> Callable[[int], bool]:
195
+ """Wrap ``run_batch`` into the ``fits`` predicate ``find_batch_size`` searches with.
196
+
197
+ An out-of-memory failure is the answer "does not fit"; anything else is re-raised. Both
198
+ arrive as the same exception types, so they are told apart by
199
+ :func:`~learning_loop_node.trainer.batch_size.is_out_of_memory`, not by ``except``.
200
+
201
+ :param run_batch: Runs the batch; may return a detail to append to the log line.
202
+ :param on_out_of_memory: Runs after a trial ran out of memory, to drop what it left behind
203
+ (an optimizer's gradients, say).
204
+ """
205
+ def fits(batch_size: int) -> bool:
206
+ free_cuda_memory()
207
+ try:
208
+ torch.cuda.reset_peak_memory_stats()
209
+ detail = run_batch(batch_size)
210
+ torch.cuda.synchronize()
211
+ logger.info('%s: %d fits (peak %.2f GB, margin included)%s', probe, batch_size,
212
+ torch.cuda.max_memory_allocated() / 1024**3, f'; {detail}' if detail else '')
213
+ return True
214
+ except (torch.cuda.OutOfMemoryError, RuntimeError, MemoryError) as exc:
215
+ if not isinstance(exc, torch.cuda.OutOfMemoryError) and not is_out_of_memory(exc):
216
+ raise
217
+ logger.info('%s: %d does not fit (%s)', probe, batch_size, type(exc).__name__)
218
+ if on_out_of_memory is not None:
219
+ on_out_of_memory()
220
+ return False
221
+
222
+ return fits
223
+
224
+
225
+ def reserve_margin(*, probe: str) -> torch.Tensor:
226
+ """Claim :data:`SAFETY_MARGIN` of the budget, so a trial competes against a smaller card.
227
+
228
+ Keep the returned tensor alive for as long as the probe runs: releasing it hands the margin
229
+ back, and the chosen size is no longer the size that was measured.
230
+ """
231
+ margin_bytes = int(usable_memory_bytes() * SAFETY_MARGIN)
232
+ logger.info('%s: keeping %.0f MB free as a safety margin', probe, margin_bytes / 1024**2)
233
+ return torch.empty(margin_bytes, dtype=torch.uint8, device='cuda')
234
+
235
+
236
+ def usable_memory_bytes() -> int:
237
+ """How much GPU memory this process may allocate, honouring :func:`limit_cuda_memory`."""
238
+ if _budget_gb > 0:
239
+ return int(_budget_gb * 1024**3)
240
+ return torch.cuda.get_device_properties(None).total_memory
241
+
242
+
243
+ def add_vram_limit_argument(parser: ArgumentParser) -> None:
244
+ """Give a spawned training script the same GPU budget flag its node has.
245
+
246
+ The spawned process still has to call :func:`limit_cuda_memory` with it.
247
+ """
248
+ parser.add_argument(VRAM_LIMIT_GB_FLAG, type=float, default=0, help=VRAM_LIMIT_GB_HELP)
249
+
250
+
251
+ def limit_cuda_memory(vram_limit_gb: float) -> None:
252
+ """Cap how much of the GPU this process may allocate, to ``vram_limit_gb`` gigabytes.
253
+
254
+ Call this once per process that touches the GPU, a spawned training process included: the
255
+ cap does not survive the spawn. A probe in this process then measures against the same budget.
256
+
257
+ :param vram_limit_gb: 0 or less means no cap.
258
+ """
259
+ global _budget_gb # pylint: disable=global-statement
260
+ if vram_limit_gb <= 0 or not torch.cuda.is_available():
261
+ return
262
+
263
+ total_bytes = torch.cuda.get_device_properties(None).total_memory
264
+ fraction = vram_limit_gb * 1024**3 / total_bytes
265
+ total_gb = total_bytes / 1024**3
266
+
267
+ if fraction >= 1.0:
268
+ logger.warning('VRAM limit of %.1f GB exceeds the card capacity of %.1f GB; not limiting',
269
+ vram_limit_gb, total_gb)
270
+ return
271
+
272
+ torch.cuda.set_per_process_memory_fraction(fraction, None)
273
+ _budget_gb = vram_limit_gb
274
+ logger.info('Limiting VRAM usage to %.1f GB of %.1f GB (%.0f%%)', vram_limit_gb, total_gb, fraction * 100)
275
+
276
+
277
+ def free_cuda_memory() -> None:
278
+ gc.collect()
279
+ torch.cuda.empty_cache()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning_loop_node
3
- Version: 0.23.0
3
+ Version: 0.24.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
@@ -60,8 +60,10 @@ You can configure connection to our Learning Loop by specifying the following en
60
60
  | LOOP_PROJECT | PROJECT | Project ID | Detector | - |
61
61
  | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.3 |
62
62
  | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.6 |
63
+ | MAX_OUTBOX_SIZE | - | max. number of non-priority images queued for upload | Detector (opt.) | 1000 |
63
64
  | EXCLUSIVE_MODEL_BUILD | - | Reject detections during update to save VRAM (set to 1) | Detector (opt.) | 0 |
64
65
  | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) | 10 |
66
+ | VRAM_LIMIT_GB | - | GPU memory (GB) a training may use; the batch size is probed against it (`--vram-limit-gb`, trainers built with `node_parser(vram_limit=True)`) | Trainer (opt.) | 0 (whole card) |
65
67
  | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
66
68
  | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
67
69
  | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
@@ -73,6 +75,16 @@ Where a name has an alias, either spelling works. If both are set to **different
73
75
  prefixed name wins and a warning names the value used — the variable is never treated as unset,
74
76
  which would otherwise let `LOOP_HOST` fall back to its default of `learning-loop.ai`.
75
77
 
78
+ #### Debug logging
79
+
80
+ Every node type (Detector, Trainer, Annotator, Converter) has a REST endpoint to switch its own log level between `INFO` and `DEBUG` at runtime. Send the plain text `on` or `off` with a PUT request:
81
+
82
+ - Turn debug logging on: `curl -X PUT -d "on" http://localhost/debug_logging`
83
+ - Turn debug logging off: `curl -X PUT -d "off" http://localhost/debug_logging`
84
+
85
+ The response repeats the new state. Any other body returns status code 400.
86
+ Only the node's own logger is affected; third-party loggers such as `httpx` keep their level. The setting is not persistent, a restart brings the log level back to `INFO`.
87
+
76
88
  #### Testing
77
89
 
78
90
  We use github actions for CI. Tests can also be executed locally by running
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "learning_loop_node"
3
- version = "0.23.0"
3
+ version = "0.24.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" }
@@ -1,81 +0,0 @@
1
- """Choosing a batch size by probing, rather than configuring one.
2
-
3
- The trainer supplies a ``fits`` predicate that runs a representative step at doubling sizes.
4
- Only powers of two are visited, so equal hardware yields an equal recipe. Nothing here imports a
5
- deep-learning framework.
6
-
7
- Adapted from PyTorch Lightning's ``BatchSizeFinder`` (power-scaling mode).
8
- Copyright The Lightning AI team. Licensed under the Apache License, Version 2.0.
9
- https://github.com/Lightning-AI/pytorch-lightning
10
- """
11
-
12
- import logging
13
- from collections.abc import Callable
14
-
15
- from .exceptions import InsufficientMemoryError
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
- MAX_BATCH_SIZE = 1024
20
- """Where a search stops when its caller sets no bound of its own."""
21
-
22
- NO_GPU_BATCH_SIZE = 8
23
- """Batch size used when there is no GPU to probe."""
24
-
25
- MIN_TRAIN_STEPS_PER_EPOCH = 8
26
- """Fewest optimizer steps an epoch must have; :func:`dataset_limit` is derived from it."""
27
-
28
-
29
- def find_batch_size(fits: Callable[[int], bool], *, limit: int) -> int:
30
- """Return the largest power-of-two batch size that fits, never exceeding ``limit``.
31
-
32
- :param fits: Runs a representative probe; ``False`` on out-of-memory.
33
- :raises InsufficientMemoryError: If not even a batch size of 1 fits.
34
- """
35
- limit = smaller_pot(limit)
36
- if not fits(1):
37
- raise InsufficientMemoryError('batch size 1 does not fit in memory')
38
-
39
- size = 1
40
- while size < limit and fits(size * 2):
41
- size *= 2
42
-
43
- return size
44
-
45
-
46
- def dataset_limit(sample_count: int) -> int:
47
- """The batch size ceiling that still leaves ``MIN_TRAIN_STEPS_PER_EPOCH`` steps per epoch."""
48
- if sample_count < 1:
49
- raise ValueError(f'sample_count must be >= 1, got {sample_count}')
50
- return max(1, sample_count // MIN_TRAIN_STEPS_PER_EPOCH)
51
-
52
-
53
- def no_gpu_batch_size(limit: int, probe: str) -> int:
54
- """The batch size to fall back on when there is no GPU to probe."""
55
- batch_size = min(smaller_pot(limit), NO_GPU_BATCH_SIZE)
56
- logger.warning('%s: CUDA is unavailable; using batch size %d without probing', probe, batch_size)
57
- return batch_size
58
-
59
-
60
- def smaller_pot(n: int) -> int:
61
- """The largest power of two that is <= ``n``."""
62
- if n < 1:
63
- raise ValueError(f'n must be >= 1, got {n}')
64
- return 1 << (n.bit_length() - 1)
65
-
66
-
67
- def batch_count(sample_count: int, batch_size: int) -> int:
68
- """How many batches a loader yields, the last one possibly short."""
69
- return -(-sample_count // batch_size)
70
-
71
-
72
- def is_out_of_memory(exception: BaseException) -> bool:
73
- """Whether the exception signals exhausted memory, on the GPU or the host.
74
-
75
- cuDNN and cuBLAS workspace failures raise a plain ``RuntimeError``, so the message has to be
76
- matched too.
77
- """
78
- if isinstance(exception, MemoryError):
79
- return True
80
- message = str(exception).lower()
81
- return any(text in message for text in ('out of memory', 'alloc_failed', 'cuda error: unknown error'))
@@ -1,137 +0,0 @@
1
- """GPU memory budgeting and batch-size probing for trainers that train in-process.
2
-
3
- ``--vram-limit-gb`` -- gigabytes of the card this training may use -- becomes both the budget a
4
- probe measures against and the cap that holds the process to it, on whichever GPU the calling
5
- process is using. The cap is a share of the card's *total* memory, not of what is free.
6
-
7
- This is the one module in the library that imports torch, which the package does not declare, so
8
- only a trainer may import it. The search itself is in :mod:`~learning_loop_node.trainer.batch_size`.
9
- """
10
- from __future__ import annotations
11
-
12
- import gc
13
- import logging
14
- from collections.abc import Callable
15
-
16
- import torch
17
-
18
- from .batch_size import MAX_BATCH_SIZE, find_batch_size, is_out_of_memory, no_gpu_batch_size, smaller_pot
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
- SAFETY_MARGIN = 0.05
23
- """Share of the budget held back while probing, against allocator fragmentation later on."""
24
-
25
-
26
- def probe_batch_size(run_batch: Callable[[int], str | None], *, probe: str = 'batch-size probe',
27
- limit: int = 0, vram_limit_gb: float = 0) -> int:
28
- """Find the largest power-of-two batch size ``run_batch`` fits into.
29
-
30
- For a probe whose measurement is one call. A probe that has to build a throwaway model first
31
- composes :func:`reserve_margin`, :func:`measured_fits` and ``find_batch_size`` itself.
32
-
33
- :param run_batch: Runs the batch; may return a detail to append to the log line.
34
- :param probe: Names this probe in the log, so a node running several stays readable.
35
- :param limit: Caps the search, rounded down to a power of two; 0 means
36
- :data:`~learning_loop_node.trainer.batch_size.MAX_BATCH_SIZE`.
37
- :param vram_limit_gb: The budget the safety margin is a share of; 0 means the whole card.
38
- :raises InsufficientMemoryError: If not even a batch size of 1 fits.
39
- """
40
- limit = smaller_pot(limit or MAX_BATCH_SIZE)
41
-
42
- if not torch.cuda.is_available():
43
- return no_gpu_batch_size(limit, probe)
44
-
45
- margin = reserve_margin(vram_limit_gb, probe=probe)
46
- try:
47
- chosen = find_batch_size(measured_fits(run_batch, probe=probe), limit=limit)
48
- finally:
49
- del margin
50
- free_cuda_memory()
51
- logger.info('%s: selected batch size %d (upper bound %d)', probe, chosen, limit)
52
- return chosen
53
-
54
-
55
- def measured_fits(run_batch: Callable[[int], str | None], *, probe: str,
56
- on_out_of_memory: Callable[[], None] | None = None) -> Callable[[int], bool]:
57
- """Wrap ``run_batch`` into the ``fits`` predicate ``find_batch_size`` searches with.
58
-
59
- An out-of-memory failure is the answer "does not fit"; anything else is re-raised. Both
60
- arrive as the same exception types, so they are told apart by
61
- :func:`~learning_loop_node.trainer.batch_size.is_out_of_memory`, not by ``except``.
62
-
63
- :param run_batch: Runs the batch; may return a detail to append to the log line.
64
- :param on_out_of_memory: Runs after a trial ran out of memory, to drop what it left behind
65
- (an optimizer's gradients, say).
66
- """
67
- def fits(batch_size: int) -> bool:
68
- free_cuda_memory()
69
- try:
70
- torch.cuda.reset_peak_memory_stats()
71
- detail = run_batch(batch_size)
72
- torch.cuda.synchronize()
73
- logger.info('%s: %d fits (peak %.2f GB, margin included)%s', probe, batch_size,
74
- torch.cuda.max_memory_allocated() / 1024**3, f'; {detail}' if detail else '')
75
- return True
76
- except (torch.cuda.OutOfMemoryError, RuntimeError, MemoryError) as exc:
77
- if not isinstance(exc, torch.cuda.OutOfMemoryError) and not is_out_of_memory(exc):
78
- raise
79
- logger.info('%s: %d does not fit (%s)', probe, batch_size, type(exc).__name__)
80
- if on_out_of_memory is not None:
81
- on_out_of_memory()
82
- return False
83
-
84
- return fits
85
-
86
-
87
- def reserve_margin(vram_limit_gb: float, *, probe: str) -> torch.Tensor:
88
- """Claim :data:`SAFETY_MARGIN` of the budget, so a trial competes against a smaller card.
89
-
90
- Keep the returned tensor alive for as long as the probe runs: releasing it hands the margin
91
- back, and the chosen size is no longer the size that was measured.
92
-
93
- :param vram_limit_gb: The budget the margin is a share of; 0 means the whole card.
94
- """
95
- margin_bytes = int(usable_memory_bytes(vram_limit_gb) * SAFETY_MARGIN)
96
- logger.info('%s: keeping %.0f MB free as a safety margin', probe, margin_bytes / 1024**2)
97
- return torch.empty(margin_bytes, dtype=torch.uint8, device='cuda')
98
-
99
-
100
- def usable_memory_bytes(vram_limit_gb: float) -> int:
101
- """How much GPU memory this process may allocate, honouring :func:`limit_cuda_memory`.
102
-
103
- :param vram_limit_gb: 0 or less means the whole card.
104
- """
105
- total_bytes = torch.cuda.get_device_properties(None).total_memory
106
- if vram_limit_gb <= 0:
107
- return total_bytes
108
- return min(total_bytes, int(vram_limit_gb * 1024**3))
109
-
110
-
111
- def limit_cuda_memory(vram_limit_gb: float) -> None:
112
- """Cap how much of the GPU this process may allocate, to ``vram_limit_gb`` gigabytes.
113
-
114
- Call this once per process that touches the GPU, a spawned training process included: the
115
- cap does not survive the spawn.
116
-
117
- :param vram_limit_gb: 0 or less means no cap.
118
- """
119
- if vram_limit_gb <= 0 or not torch.cuda.is_available():
120
- return
121
-
122
- total_bytes = torch.cuda.get_device_properties(None).total_memory
123
- fraction = vram_limit_gb * 1024**3 / total_bytes
124
- total_gb = total_bytes / 1024**3
125
-
126
- if fraction >= 1.0:
127
- logger.warning('VRAM limit of %.1f GB exceeds the card capacity of %.1f GB; not limiting',
128
- vram_limit_gb, total_gb)
129
- return
130
-
131
- torch.cuda.set_per_process_memory_fraction(fraction, None)
132
- logger.info('Limiting VRAM usage to %.1f GB of %.1f GB (%.0f%%)', vram_limit_gb, total_gb, fraction * 100)
133
-
134
-
135
- def free_cuda_memory() -> None:
136
- gc.collect()
137
- torch.cuda.empty_cache()