@@ -196,6 +196,82 @@ def _get_model(cellpose_dict: dict[str, Any]) -> Any:
196196 return _model_cache [key ]
197197
198198
199+ def _is_cuda_oom (exc : Exception ) -> bool :
200+ """Return whether *exc* looks like a CUDA out-of-memory error.
201+
202+ Parameters
203+ ----------
204+ exc : Exception
205+ Exception raised by ``model.eval``.
206+
207+ Returns
208+ -------
209+ bool
210+ True if the exception is a CUDA OOM.
211+ """
212+ return "out of memory" in str (exc ).lower ()
213+
214+
215+ _OOM_RETRIES = 4
216+ _OOM_BACKOFF_SECONDS = 30
217+
218+
219+ def _eval_with_oom_fallback (
220+ model : Any , img : np .ndarray , kwargs : dict [str , Any ], cellpose_dict : dict [str , Any ]
221+ ) -> np .ndarray :
222+ """Run ``model.eval``, surviving transient GPU contention.
223+
224+ Cluster GPUs are often shared: another job's memory footprint can grow
225+ mid-run and push an otherwise-fine tile size over the edge. Staying on
226+ GPU matters — this tile can take well over an hour, so falling back to
227+ CPU would be far worse than waiting. Instead this clears the allocator
228+ cache (fixes fragmentation) and retries with a backoff, giving the
229+ cotenant job time to free memory, before finally giving up.
230+
231+ Parameters
232+ ----------
233+ model : Any
234+ Cellpose model instance.
235+ img : np.ndarray
236+ Image to segment.
237+ kwargs : dict
238+ Keyword arguments for ``model.eval``.
239+ cellpose_dict : dict
240+ Configuration from :func:`_make_config`.
241+
242+ Returns
243+ -------
244+ np.ndarray
245+ Label array from ``model.eval``.
246+ """
247+ import time
248+
249+ for attempt in range (_OOM_RETRIES + 1 ):
250+ try :
251+ return model .eval (img , ** kwargs )[0 ]
252+ except RuntimeError as exc :
253+ if not cellpose_dict .get ("gpu" , False ) or not _is_cuda_oom (exc ):
254+ raise
255+ import torch
256+
257+ torch .cuda .empty_cache ()
258+ if attempt == _OOM_RETRIES :
259+ logger .error (
260+ "CUDA OOM persisted after %d retries; giving up on tile." ,
261+ _OOM_RETRIES ,
262+ )
263+ raise
264+ wait = _OOM_BACKOFF_SECONDS * (attempt + 1 )
265+ logger .warning (
266+ "CUDA OOM on tile (likely GPU contention); cleared cache, "
267+ "retrying in %ds (attempt %d/%d)." ,
268+ wait ,
269+ attempt + 1 ,
270+ _OOM_RETRIES ,
271+ )
272+ time .sleep (wait )
273+
274+
199275def _run (block : np .ndarray , cellpose_dict : dict [str , Any ]) -> np .ndarray :
200276 """Segment one tile with a cached Cellpose model.
201277
@@ -231,12 +307,15 @@ def _run(block: np.ndarray, cellpose_dict: dict[str, Any]) -> np.ndarray:
231307
232308 if do_3D :
233309 kwargs ["z_axis" ] = 0
234- return model .eval (block , ** kwargs )[0 ].astype ("int32" )
310+ masks = _eval_with_oom_fallback (model , block , kwargs , cellpose_dict )
311+ return masks .astype ("int32" )
235312 else :
236313 # Squeeze singleton z so Cellpose gets a clean 2-D image
237314 squeeze = block .ndim == 3 and block .shape [0 ] == 1
238315 img = block [0 ] if squeeze else block
239- masks = model .eval (img , ** kwargs )[0 ].astype ("int32" )
316+ masks = _eval_with_oom_fallback (model , img , kwargs , cellpose_dict ).astype (
317+ "int32"
318+ )
240319 return masks [np .newaxis ] if squeeze else masks
241320
242321
0 commit comments