Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion openrag/services/workers/parsers/docling_workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,22 @@


def _docling_num_gpus(config) -> float:
return config.loader.docling_num_gpus if torch.cuda.is_available() else 0
"""Return Docling's Ray GPU reservation, falling back to CUDA detection.

Must mirror Marker: ``_docling_num_gpus`` runs inside the ``DoclingPool``
actor, which Ray schedules with no GPU — so ``torch.cuda.is_available()`` is
False there (``CUDA_VISIBLE_DEVICES=""``) and a naive local check reserves 0
GPUs for the worker, leaving Docling stuck on CPU. Query the Ray *cluster*
for GPU capacity instead, with local CUDA only as a startup fallback.
"""
requested_gpus = config.loader.docling_num_gpus
if requested_gpus <= 0:
return 0
try:
return requested_gpus if ray.cluster_resources().get("GPU", 0) > 0 else 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes fresh DoclingPool creation, but it may not take effect during a live upgrade. Startup uses get_or_create_actor("DoclingPool", ...), so if the old detached actor is still alive, it will keep its existing workers that were created with num_gpus=0. The deployment would still parse on CPU until someone manually restarts DoclingPool.

For this PR, it would be good to either add a small migration/restart guard for the existing actor, or call out clearly that DoclingPool must be restarted after deploying the fix.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-checked this. The code fix is already enough for any newly created DoclingPool. The only remaining case is a live upgrade with an old detached pool still running, and we can handle that by restarting DoclingPool after deployment. So I don’t think we need an extra code change here.

except Exception as exc:
logger.warning("Failed to query Ray cluster GPU resources; falling back to CUDA check", error=str(exc))
return requested_gpus if torch.cuda.is_available() else 0


@ray.remote
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/services/workers/parsers/test_docling_workers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

from types import SimpleNamespace

from services.workers.parsers import docling_workers


def _config(docling_num_gpus: float = 0.25):
"""Build the minimal config shape consumed by Docling GPU selection."""
return SimpleNamespace(loader=SimpleNamespace(docling_num_gpus=docling_num_gpus))


def test_docling_num_gpus_uses_ray_cluster_resources_when_cuda_is_hidden(monkeypatch):
"""Docling must request a GPU from Ray even when local CUDA is hidden in the
pool process (CUDA_VISIBLE_DEVICES=""), otherwise it silently parses on CPU."""
monkeypatch.setattr(docling_workers.torch.cuda, "is_available", lambda: False)
monkeypatch.setattr(docling_workers.ray, "cluster_resources", lambda: {"GPU": 1.0})

assert docling_workers._docling_num_gpus(_config()) == 0.25


def test_docling_num_gpus_zero_when_cluster_has_no_gpu(monkeypatch):
monkeypatch.setattr(docling_workers.torch.cuda, "is_available", lambda: False)
monkeypatch.setattr(docling_workers.ray, "cluster_resources", dict)

assert docling_workers._docling_num_gpus(_config()) == 0


def test_docling_num_gpus_zero_when_not_requested(monkeypatch):
monkeypatch.setattr(docling_workers.ray, "cluster_resources", lambda: {"GPU": 1.0})

assert docling_workers._docling_num_gpus(_config(docling_num_gpus=0)) == 0
Loading