nnInteractive 2.3.0__tar.gz → 2.3.2__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 (90) hide show
  1. {nninteractive-2.3.0 → nninteractive-2.3.2}/PKG-INFO +1 -1
  2. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/remote/remote_session.py +52 -5
  3. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/remote/serialization.py +15 -6
  4. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/server/app.py +61 -30
  5. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive.egg-info/PKG-INFO +1 -1
  6. {nninteractive-2.3.0 → nninteractive-2.3.2}/pyproject.toml +1 -1
  7. {nninteractive-2.3.0 → nninteractive-2.3.2}/LICENSE +0 -0
  8. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/__init__.py +0 -0
  9. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/__init__.py +0 -0
  10. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/cvpr2025_challenge_baseline/__init__.py +0 -0
  11. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/cvpr2025_challenge_baseline/predict.py +0 -0
  12. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/inference_session.py +0 -0
  13. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/remote/__init__.py +0 -0
  14. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/remote/_protocol.py +0 -0
  15. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/server/__init__.py +0 -0
  16. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/inference/server/main.py +0 -0
  17. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/interaction/__init__.py +0 -0
  18. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/interaction/point.py +0 -0
  19. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/setup.py +0 -0
  20. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/metadata.py +0 -0
  21. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/reader.py +0 -0
  22. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/run.py +0 -0
  23. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/__init__.py +0 -0
  24. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/__init__.py +0 -0
  25. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/automatic_mask_generator.py +0 -0
  26. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/benchmark.py +0 -0
  27. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/build_sam.py +0 -0
  28. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/__init__.py +0 -0
  29. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/__init__.py +0 -0
  30. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/hieradet.py +0 -0
  31. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/image_encoder.py +0 -0
  32. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/utils.py +0 -0
  33. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_attention.py +0 -0
  34. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_encoder.py +0 -0
  35. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/position_encoding.py +0 -0
  36. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/__init__.py +0 -0
  37. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/mask_decoder.py +0 -0
  38. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/prompt_encoder.py +0 -0
  39. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/transformer.py +0 -0
  40. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_base.py +0 -0
  41. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_utils.py +0 -0
  42. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/sam2_image_predictor.py +0 -0
  43. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor.py +0 -0
  44. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor_legacy.py +0 -0
  45. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/__init__.py +0 -0
  46. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/amg.py +0 -0
  47. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/misc.py +0 -0
  48. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/transforms.py +0 -0
  49. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/setup.py +0 -0
  50. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/__init__.py +0 -0
  51. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/__init__.py +0 -0
  52. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/sam2_datasets.py +0 -0
  53. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/transforms.py +0 -0
  54. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/utils.py +0 -0
  55. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_dataset.py +0 -0
  56. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_raw_dataset.py +0 -0
  57. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_sampler.py +0 -0
  58. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_segment_loader.py +0 -0
  59. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/loss_fns.py +0 -0
  60. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/model/__init__.py +0 -0
  61. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/model/sam2.py +0 -0
  62. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/optimizer.py +0 -0
  63. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/scripts/sav_frame_extraction_submitit.py +0 -0
  64. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/train.py +0 -0
  65. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/trainer.py +0 -0
  66. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/__init__.py +0 -0
  67. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/checkpoint_utils.py +0 -0
  68. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/data_utils.py +0 -0
  69. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/distributed.py +0 -0
  70. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/logger.py +0 -0
  71. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/train_utils.py +0 -0
  72. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/supervoxel.py +0 -0
  73. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/trainer/__init__.py +0 -0
  74. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/trainer/nnInteractiveTrainer.py +0 -0
  75. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/__init__.py +0 -0
  76. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/bboxes.py +0 -0
  77. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/checkpoint_cleansing.py +0 -0
  78. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/crop.py +0 -0
  79. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/erosion_dilation.py +0 -0
  80. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/inference_helpers.py +0 -0
  81. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/os_shennanigans.py +0 -0
  82. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive/utils/rounding.py +0 -0
  83. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive.egg-info/SOURCES.txt +0 -0
  84. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive.egg-info/dependency_links.txt +0 -0
  85. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive.egg-info/entry_points.txt +0 -0
  86. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive.egg-info/requires.txt +0 -0
  87. {nninteractive-2.3.0 → nninteractive-2.3.2}/nnInteractive.egg-info/top_level.txt +0 -0
  88. {nninteractive-2.3.0 → nninteractive-2.3.2}/readme.md +0 -0
  89. {nninteractive-2.3.0 → nninteractive-2.3.2}/setup.cfg +0 -0
  90. {nninteractive-2.3.0 → nninteractive-2.3.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nnInteractive
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Inference code for nnInteractive
5
5
  Author: Helmholtz Imaging Applied Computer Vision Lab
6
6
  Author-email: Fabian Isensee <f.isensee@dkfz-heidelberg.de>
@@ -42,6 +42,16 @@ from nnInteractive.inference.remote._protocol import (
42
42
  from nnInteractive.inference.remote.serialization import pack_array, unpack_array
43
43
 
44
44
 
45
+ def _compression_threads() -> int:
46
+ """blosc2 thread count for client-side upload compression.
47
+
48
+ Full logical CPU count: blosc2 scales measurably onto SMT siblings, so use them all to
49
+ minimize upload latency. Per-call only (passed to pack_array → compress2), so it never
50
+ mutates blosc2's global nthreads.
51
+ """
52
+ return max(1, os.cpu_count() or 1)
53
+
54
+
45
55
  class SessionExpiredError(RuntimeError):
46
56
  """Raised when the server reports the client's lease no longer exists.
47
57
 
@@ -122,6 +132,13 @@ class nnInteractiveRemoteInferenceSession:
122
132
  the server before the client gives up. Default 60s matches observed
123
133
  prediction times (100ms..~10s) with headroom for slow links. On
124
134
  expiry: ``httpx.ReadTimeout``.
135
+ set_image_read_timeout:
136
+ Read timeout (seconds) used *only* for ``set_image``. After the volume
137
+ is uploaded, the server decompresses and preprocesses the full image
138
+ before responding, which can take far longer than a prediction on a
139
+ large volume. ``set_image`` therefore gets its own generous read
140
+ timeout instead of the much tighter ``read_timeout`` used for
141
+ predictions. On expiry: ``httpx.ReadTimeout``.
125
142
  write_timeout:
126
143
  Seconds to finish uploading the request body. ``set_image`` uploads
127
144
  the full 4D volume so this is the longest-running upload. On expiry:
@@ -136,6 +153,7 @@ class nnInteractiveRemoteInferenceSession:
136
153
  api_key: Optional[str] = None,
137
154
  connect_timeout: float = 10.0,
138
155
  read_timeout: float = 60.0,
156
+ set_image_read_timeout: float = 600.0,
139
157
  write_timeout: float = 120.0,
140
158
  pool_timeout: float = 10.0,
141
159
  ):
@@ -156,6 +174,15 @@ class nnInteractiveRemoteInferenceSession:
156
174
  ),
157
175
  headers=headers,
158
176
  )
177
+ # Per-request timeout override for set_image: same connect/write/pool as
178
+ # the client default, but a much longer read budget for server-side
179
+ # decompression + preprocessing of the full volume.
180
+ self._set_image_timeout = httpx.Timeout(
181
+ connect=connect_timeout,
182
+ read=set_image_read_timeout,
183
+ write=write_timeout,
184
+ pool=pool_timeout,
185
+ )
159
186
  self._lease_token: Optional[str] = None
160
187
 
161
188
  # Claim a session on the server. The lease token is then attached to
@@ -232,12 +259,21 @@ class nnInteractiveRemoteInferenceSession:
232
259
  resp.raise_for_status()
233
260
  return resp
234
261
 
235
- def _post_binary(self, path: str, meta: dict, array_bytes: bytes) -> httpx.Response:
262
+ def _post_binary(
263
+ self,
264
+ path: str,
265
+ meta: dict,
266
+ array_bytes: bytes,
267
+ timeout: Union[httpx.Timeout, float, None] = None,
268
+ ) -> httpx.Response:
236
269
  headers = {
237
270
  META_HEADER: json.dumps(_to_jsonable(meta), separators=(",", ":")),
238
271
  "Content-Type": CONTENT_TYPE_OCTET_STREAM,
239
272
  }
240
- resp = self._http.post(path, content=array_bytes, headers=headers)
273
+ # httpx treats timeout=None as "no override" only when the arg is
274
+ # omitted; pass it through explicitly only when a caller supplied one.
275
+ kwargs = {} if timeout is None else {"timeout": timeout}
276
+ resp = self._http.post(path, content=array_bytes, headers=headers, **kwargs)
241
277
  _raise_for_lease_errors(resp)
242
278
  resp.raise_for_status()
243
279
  return resp
@@ -290,7 +326,12 @@ class nnInteractiveRemoteInferenceSession:
290
326
  def set_image(self, image: np.ndarray, image_properties: Optional[dict] = None) -> None:
291
327
  assert image.ndim == 4, f"expected a 4d image as input, got {image.ndim}d. Shape {image.shape}"
292
328
  meta = {"image_properties": image_properties or {}}
293
- resp = self._post_binary(PATH_SET_IMAGE, meta, pack_array(image))
329
+ resp = self._post_binary(
330
+ PATH_SET_IMAGE,
331
+ meta,
332
+ pack_array(image, nthreads=_compression_threads()),
333
+ timeout=self._set_image_timeout,
334
+ )
294
335
  info = resp.json()
295
336
  self.original_image_shape = tuple(info["original_image_shape"])
296
337
 
@@ -402,7 +443,9 @@ class nnInteractiveRemoteInferenceSession:
402
443
  "interaction_bbox": ([list(b) for b in interaction_bbox] if interaction_bbox is not None else None),
403
444
  }
404
445
  # Interactions (scribble/lasso masks) compress best with NOFILTER; skip auto-selection.
405
- resp = self._post_binary(path, meta, pack_array(mask_image, filters=[blosc2.Filter.NOFILTER]))
446
+ resp = self._post_binary(
447
+ path, meta, pack_array(mask_image, filters=[blosc2.Filter.NOFILTER], nthreads=_compression_threads())
448
+ )
406
449
  self._apply_prediction_response(resp)
407
450
 
408
451
  def add_initial_seg_interaction(
@@ -428,7 +471,11 @@ class nnInteractiveRemoteInferenceSession:
428
471
  "override_capability_checks": bool(override_capability_checks),
429
472
  }
430
473
  # Segmentations compress best with NOFILTER; skip auto-selection.
431
- resp = self._post_binary(PATH_ADD_INITIAL_SEG, meta, pack_array(initial_seg, filters=[blosc2.Filter.NOFILTER]))
474
+ resp = self._post_binary(
475
+ PATH_ADD_INITIAL_SEG,
476
+ meta,
477
+ pack_array(initial_seg, filters=[blosc2.Filter.NOFILTER], nthreads=_compression_threads()),
478
+ )
432
479
  self._apply_prediction_response(resp)
433
480
 
434
481
  # ------------------------------------------------------------------ #
@@ -51,18 +51,21 @@ _ID_CODEC = {v: k for k, v in _CODEC_ID.items()}
51
51
  _SELECT_FILTER_CROP_FRACTION = 0.25
52
52
 
53
53
 
54
- def _compress_all(raw: memoryview, total: int, codec: blosc2.Codec, clevel: int, filters: list) -> int:
54
+ def _compress_all(
55
+ raw: memoryview, total: int, codec: blosc2.Codec, clevel: int, filters: list, nthreads: Optional[int]
56
+ ) -> int:
55
57
  """Compressed byte length of ``raw`` under ``filters``, chunked exactly as pack_array does."""
58
+ extra = {} if nthreads is None else {"nthreads": nthreads}
56
59
  size = 0
57
60
  nchunks = (total + _CHUNK_SIZE - 1) // _CHUNK_SIZE
58
61
  for i in range(nchunks):
59
62
  start = i * _CHUNK_SIZE
60
63
  end = min(start + _CHUNK_SIZE, total)
61
- size += len(blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters))
64
+ size += len(blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters, **extra))
62
65
  return size
63
66
 
64
67
 
65
- def _select_filter(arr: np.ndarray, codec: blosc2.Codec, clevel: int) -> "blosc2.Filter":
68
+ def _select_filter(arr: np.ndarray, codec: blosc2.Codec, clevel: int, nthreads: Optional[int]) -> "blosc2.Filter":
66
69
  """Pick NOFILTER vs SHUFFLE for ``arr`` by trial-compressing a small centered crop.
67
70
 
68
71
  Uses ``compress2`` on the raw bytes — exactly the path pack_array takes — so the decision
@@ -79,7 +82,7 @@ def _select_filter(arr: np.ndarray, codec: blosc2.Codec, clevel: int) -> "blosc2
79
82
 
80
83
  best_filter, best_bytes = blosc2.Filter.NOFILTER, None
81
84
  for f in (blosc2.Filter.NOFILTER, blosc2.Filter.SHUFFLE):
82
- cb = _compress_all(raw, total, codec, clevel, [f])
85
+ cb = _compress_all(raw, total, codec, clevel, [f], nthreads)
83
86
  if best_bytes is None or cb < best_bytes:
84
87
  best_bytes, best_filter = cb, f
85
88
  return best_filter
@@ -95,6 +98,7 @@ def pack_array(
95
98
  codec: blosc2.Codec = blosc2.Codec.ZSTD,
96
99
  clevel: int = 3,
97
100
  filters: Optional[list] = None,
101
+ nthreads: Optional[int] = None,
98
102
  ) -> bytes:
99
103
  """Serialize a numpy array to a self-describing compressed byte string.
100
104
 
@@ -104,6 +108,10 @@ def pack_array(
104
108
  know the optimum (interactions and segmentations compress best with NOFILTER) should pass
105
109
  ``[blosc2.Filter.NOFILTER]`` to skip the selection. The chosen filter is self-describing
106
110
  inside the blosc2 frame, so unpack_array (decompress2) needs no changes.
111
+
112
+ ``nthreads`` is the per-call blosc2 thread count for compression. ``None`` (the default)
113
+ inherits blosc2's global ``nthreads`` (= core count). Passing an explicit value overrides
114
+ it for this call only, without mutating global state.
107
115
  """
108
116
  arr = np.ascontiguousarray(arr)
109
117
  dtype_str = arr.dtype.str.lstrip("<>|=").encode("ascii")
@@ -137,13 +145,14 @@ def pack_array(
137
145
  if filters is None:
138
146
  # Auto-select the better filter from a small centered crop, using the same
139
147
  # compress2 path as below for consistency.
140
- filters = [_select_filter(arr, codec, clevel)]
148
+ filters = [_select_filter(arr, codec, clevel, nthreads)]
141
149
 
150
+ extra = {} if nthreads is None else {"nthreads": nthreads}
142
151
  parts = [header, shape_bytes, struct.pack("<I", nchunks)]
143
152
  for i in range(nchunks):
144
153
  start = i * _CHUNK_SIZE
145
154
  end = min(start + _CHUNK_SIZE, total)
146
- chunk = blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters)
155
+ chunk = blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters, **extra)
147
156
  parts.append(struct.pack("<QQ", end - start, len(chunk)))
148
157
  parts.append(chunk)
149
158
  return b"".join(parts)
@@ -31,6 +31,15 @@ Concurrency model:
31
31
  prediction runs at a time.
32
32
  - The acquisition order is always (session lock → gpu lock) so there is no
33
33
  deadlock potential.
34
+ - The endpoints that carry large payloads (``set_image`` and the mask
35
+ interactions) are ``async`` so they can ``await`` the upload, but their
36
+ CPU-bound work (blosc2 decompression, image preprocessing, prediction,
37
+ response compression) is dispatched to a worker thread via
38
+ ``run_in_threadpool``. This keeps the event loop free during a long
39
+ ``set_image``/predict so lightweight endpoints — ``/heartbeat``,
40
+ ``/healthz`` — and the background reaper stay responsive, and so two
41
+ clients can genuinely preprocess concurrently. Acquiring a session/gpu
42
+ lock therefore also happens off the loop, never stalling it.
34
43
  """
35
44
 
36
45
  from __future__ import annotations
@@ -50,6 +59,7 @@ import blosc2
50
59
  import numpy as np
51
60
  import torch
52
61
  from fastapi import Depends, FastAPI, HTTPException, Header, Request, Response, status
62
+ from starlette.concurrency import run_in_threadpool
53
63
 
54
64
  from nnInteractive.inference.inference_session import nnInteractiveInferenceSession
55
65
  from nnInteractive.inference.remote._protocol import (
@@ -443,7 +453,9 @@ def make_app(
443
453
  session._last_paste_bbox = None
444
454
  return Response(
445
455
  # Segmentations compress best with NOFILTER; skip auto-selection.
446
- content=pack_array(sub, filters=[blosc2.Filter.NOFILTER]),
456
+ content=pack_array(
457
+ sub, filters=[blosc2.Filter.NOFILTER], nthreads=min(session.torch_n_threads, os.cpu_count())
458
+ ),
447
459
  media_type=CONTENT_TYPE_OCTET_STREAM,
448
460
  headers={META_HEADER: json.dumps(meta, separators=(",", ":"))},
449
461
  )
@@ -568,17 +580,24 @@ def make_app(
568
580
  async def set_image(request: Request, entry: SessionEntry = lease) -> dict:
569
581
  meta = _parse_meta_header(request.headers.get(META_HEADER))
570
582
  body = await request.body()
571
- image = unpack_array(body)
572
583
  image_properties = meta.get("image_properties") or {}
573
584
 
574
- def _do(session):
575
- session.set_image(image, image_properties)
576
- # set_image preprocesses in a background thread; force completion so
577
- # subsequent calls can safely use original_image_shape.
578
- session._finish_preprocessing_and_initialize_interactions()
579
- return {"original_image_shape": list(session.original_image_shape)}
585
+ # Decompression + full-volume preprocessing are CPU-bound and can run
586
+ # for many seconds on a large image. Run them in a worker thread so the
587
+ # event loop keeps servicing heartbeats/healthz and the reaper.
588
+ def _work():
589
+ image = unpack_array(body)
580
590
 
581
- return _under_session_lock(entry, _do)
591
+ def _do(session):
592
+ session.set_image(image, image_properties)
593
+ # set_image preprocesses in a background thread; force completion
594
+ # so subsequent calls can safely use original_image_shape.
595
+ session._finish_preprocessing_and_initialize_interactions()
596
+ return {"original_image_shape": list(session.original_image_shape)}
597
+
598
+ return _under_session_lock(entry, _do)
599
+
600
+ return await run_in_threadpool(_work)
582
601
 
583
602
  @app.post(PATH_SET_TARGET_BUFFER, dependencies=[auth])
584
603
  def set_target_buffer(payload: dict, entry: SessionEntry = lease) -> dict:
@@ -650,41 +669,53 @@ def make_app(
650
669
  async def _handle_mask_interaction(request: Request, entry: SessionEntry, kind: str) -> Response:
651
670
  meta = _parse_meta_header(request.headers.get(META_HEADER))
652
671
  body = await request.body()
653
- mask = unpack_array(body)
654
672
  run_prediction = bool(meta.get("run_prediction", True))
655
673
  interaction_bbox = meta.get("interaction_bbox")
656
674
  if interaction_bbox is not None:
657
675
  interaction_bbox = [list(b) for b in interaction_bbox]
658
676
 
659
- def _do(session):
660
- method = session.add_scribble_interaction if kind == "scribble" else session.add_lasso_interaction
661
- method(
662
- mask,
663
- bool(meta["include_interaction"]),
664
- run_prediction=run_prediction,
665
- override_capability_checks=bool(meta.get("override_capability_checks", False)),
666
- interaction_bbox=interaction_bbox,
667
- )
668
- return _build_prediction_response(session, run_prediction)
677
+ # Decompression + prediction + response compression are CPU/GPU-bound;
678
+ # run them off the event loop (see set_image).
679
+ def _work():
680
+ mask = unpack_array(body)
669
681
 
670
- return _under_session_and_gpu_lock(entry, _do)
682
+ def _do(session):
683
+ method = session.add_scribble_interaction if kind == "scribble" else session.add_lasso_interaction
684
+ method(
685
+ mask,
686
+ bool(meta["include_interaction"]),
687
+ run_prediction=run_prediction,
688
+ override_capability_checks=bool(meta.get("override_capability_checks", False)),
689
+ interaction_bbox=interaction_bbox,
690
+ )
691
+ return _build_prediction_response(session, run_prediction)
692
+
693
+ return _under_session_and_gpu_lock(entry, _do)
694
+
695
+ return await run_in_threadpool(_work)
671
696
 
672
697
  @app.post(PATH_ADD_INITIAL_SEG, dependencies=[auth])
673
698
  async def add_initial_seg_interaction(request: Request, entry: SessionEntry = lease) -> Response:
674
699
  meta = _parse_meta_header(request.headers.get(META_HEADER))
675
700
  body = await request.body()
676
- initial_seg = unpack_array(body)
677
701
  run_prediction = bool(meta.get("run_prediction", False))
678
702
 
679
- def _do(session):
680
- session.add_initial_seg_interaction(
681
- initial_seg=initial_seg,
682
- run_prediction=run_prediction,
683
- override_capability_checks=bool(meta.get("override_capability_checks", False)),
684
- )
685
- return _build_prediction_response(session, run_prediction)
703
+ # Decompression + (optional) prediction are CPU/GPU-bound; run them off
704
+ # the event loop (see set_image).
705
+ def _work():
706
+ initial_seg = unpack_array(body)
686
707
 
687
- return _under_session_and_gpu_lock(entry, _do)
708
+ def _do(session):
709
+ session.add_initial_seg_interaction(
710
+ initial_seg=initial_seg,
711
+ run_prediction=run_prediction,
712
+ override_capability_checks=bool(meta.get("override_capability_checks", False)),
713
+ )
714
+ return _build_prediction_response(session, run_prediction)
715
+
716
+ return _under_session_and_gpu_lock(entry, _do)
717
+
718
+ return await run_in_threadpool(_work)
688
719
 
689
720
  return app
690
721
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nnInteractive
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Inference code for nnInteractive
5
5
  Author: Helmholtz Imaging Applied Computer Vision Lab
6
6
  Author-email: Fabian Isensee <f.isensee@dkfz-heidelberg.de>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nnInteractive"
3
- version = "2.3.0"
3
+ version = "2.3.2"
4
4
  requires-python = ">=3.10"
5
5
  description = "Inference code for nnInteractive"
6
6
  readme = "readme.md"
File without changes
File without changes
File without changes
File without changes