Assign gRPC workers to distinct GPUs via cudaSetDevice - #1449
Conversation
Round-robin workers across visible CUDA devices at worker startup and initialize a per-process RMM pool (CUOPT_GIGABYTES_PER_PROC) so multiple workers can share a GPU without exhausting memory.
| #include "grpc_pipe_serialization.hpp" | ||
| #include "grpc_server_types.hpp" | ||
|
|
||
| #include <cctype> |
There was a problem hiding this comment.
Note that these routines below are simply for logging in the main server, so that we can log information about the number of workers and gpus being used without actually initializing CUDA before forking the worker processes (which causes issues).
The workers themselves simply ask CUDA for available devices and apply a mod on worker id
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds GPU layout detection and logging to the gRPC server startup sequence, and initializes per-worker CUDA device selection and RMM memory pool before workers begin processing jobs. GPU count is determined from ChangesGPU Layout Logging and Worker CUDA/RMM Initialization
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
cpp/src/grpc/server/grpc_worker.cpp (1)
21-25: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winHarden
CUOPT_GIGABYTES_PER_PROCparsing and bounds checks.Line 23 uses
std::atoi, which silently accepts partial strings and has undefined overflow behavior; a malformed env value can produce unintended pool sizing and unstable startup behavior.💡 Proposed refactor
+ `#include` <cerrno> + `#include` <limits> @@ int pool_gigs = 1; if (const char* env = std::getenv("CUOPT_GIGABYTES_PER_PROC")) { - const int parsed = std::atoi(env); - if (parsed > 0) { pool_gigs = parsed; } + char* end = nullptr; + errno = 0; + long long parsed = std::strtoll(env, &end, 10); + if (errno == 0 && end != env && *end == '\0' && parsed > 0 && + parsed <= std::numeric_limits<int>::max()) { + pool_gigs = static_cast<int>(parsed); + } else { + SERVER_LOG_WARN("[Worker] Ignoring invalid CUOPT_GIGABYTES_PER_PROC='%s'", env); + } }As per coding guidelines, “Prevent numerical instability (overflow, underflow, precision loss) producing wrong results.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/src/grpc/server/grpc_worker.cpp` around lines 21 - 25, The CUOPT_GIGABYTES_PER_PROC environment variable parsing on line 23 uses std::atoi which silently accepts partial strings and has undefined overflow behavior, creating numerical instability. Replace std::atoi with std::stoi wrapped in a try-catch block to handle parsing errors gracefully, and add upper bounds checking to ensure the parsed value does not exceed a reasonable maximum (in addition to the existing positive check). This will prevent malformed environment values from producing unintended pool sizing and unstable startup behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cpp/src/grpc/server/grpc_worker.cpp`:
- Around line 42-50: The init_worker_cuda_environment function currently logs
errors but returns void, making it impossible for the caller to detect
initialization failures. Change the function signature from void to return a
bool or status code, then modify the error handling at lines 46-50 and the other
CUDA error cases to return false instead of just logging and returning. Update
the caller at line 582 where init_worker_cuda_environment is invoked to check
the return status and fail worker startup if the CUDA environment initialization
returns false, preventing the worker from becoming active with a failed GPU
state.
---
Nitpick comments:
In `@cpp/src/grpc/server/grpc_worker.cpp`:
- Around line 21-25: The CUOPT_GIGABYTES_PER_PROC environment variable parsing
on line 23 uses std::atoi which silently accepts partial strings and has
undefined overflow behavior, creating numerical instability. Replace std::atoi
with std::stoi wrapped in a try-catch block to handle parsing errors gracefully,
and add upper bounds checking to ensure the parsed value does not exceed a
reasonable maximum (in addition to the existing positive check). This will
prevent malformed environment values from producing unintended pool sizing and
unstable startup behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 8c7410cd-ca08-4ff6-a3a0-8e77c3c7f64a
📒 Files selected for processing (4)
cpp/src/grpc/server/grpc_server_main.cppcpp/src/grpc/server/grpc_server_types.hppcpp/src/grpc/server/grpc_worker.cppcpp/src/grpc/server/grpc_worker_infra.cpp
|
|
||
| void init_worker_rmm_pool() | ||
| { | ||
| int pool_gigs = 1; |
There was a problem hiding this comment.
Are we using 1GB by default as rmm pool memory?
There was a problem hiding this comment.
I think so, let me double check that against the Python server
There was a problem hiding this comment.
@ramakrishnap-nv yes the default in the Python server is 1GB, can be overridden with environment variable
| const int pool_gigs = parse_pool_gigs_env(); | ||
|
|
||
| // Keep the pool alive for the lifetime of this worker process. | ||
| static std::unique_ptr<rmm::mr::pool_memory_resource> pool_mr; |
There was a problem hiding this comment.
Do you think we will ever do multi-threading or is it always going to be multi-processing? If we plan to do multi threading, static variables might cause issues.
There was a problem hiding this comment.
I think we'll always do multi-process, because it is the easiest way to cancel a job (we don't have cancel hooks in the solver) or restart a worker if it has CUDA errors or something. And it makes it easy to map a GPU to an isolated entity
| return false; | ||
| } | ||
|
|
||
| init_worker_rmm_pool(); |
There was a problem hiding this comment.
Shouldn't this be done once per device instead of once per worker, otherwise we are allocating a new memory resource and assign it to the device again which might cause issues when there are multiple workers using the same device?
There was a problem hiding this comment.
good feedback, I'll take a look at that
There was a problem hiding this comment.
@akifcorduk
This is the way we did it in the Python server -- each process gets it own pool on a device, and that only happens once per process. Not sure how we would share a pool across processes -- is there a simple way to do that?
If it might be a problem, the simple fix is to cap the number of workers to the number of GPUs available. This would guarantee only 1 pool per device (held by 1 process) unless you ran multiple servers. It would just prevent oversubscribing.
There was a problem hiding this comment.
Oh worker is a process. Sorry for the confusion, in cpp OMP worker keyword is per thread. Yes, one pool per process is needed, it won't work otherwise, a different process cannot access cuda memory.
|
/merge |
Round-robin workers across visible CUDA devices at worker startup and initialize a per-process RMM pool (CUOPT_GIGABYTES_PER_PROC) so multiple workers can share a GPU without exhausting memory.
The logic to actually map a worker process to a specific GPU was missing previously (oversight).