Repository navigation
Conversation
A submit config could already declare several services, but every one of them rendered as a bare `srun --overlap`, so they all landed on the same node and competed for its GPUs. A scorer or judge that cannot share a GPU with the policy had nowhere to go. A service may now name a `node_pool`. Pools take contiguous slices of the allocation in declaration order, and a pinned service is launched with `srun --relative=<offset>`, which starts the step at that slice. Sizing follows placement: a service pinned to a one-node pool inside a ten-node job is a single-node deployment, so it is no longer built as a multi-node Ray one, and its GPU footprint is checked against its own pool instead of the allocation total. This also fixes multiple pools being silently useless. The directives were emitted per pool, so each --nodes/--partition overwrote the last and only the final pool survived, while the executor sized itself on the sum. There is now one directive block for the allocation, and pools that disagree on a whole-allocation directive are refused instead of resolved by ordering. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
`_build_ray_command` returned the fixed string "ray start --head". That starts
a cluster nobody can join, on whichever node srun picked, and returns
immediately -- ray daemonises, so the srun step exited and Slurm tore the
service down again.
A ray service now takes a mode. A head starts a cluster on a chosen port; a
worker joins one at `address`. Either may advertise custom Ray resources
(`{"extra_gpu": 4}`), which is how a benchmark asks for a node whose GPUs it
places actors on itself rather than letting Ray count them. --block keeps the
step alive for as long as the node should be up.
With node_pool from the previous commit, this is enough to put such a worker
on a node of its own.
Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
A worker could only join a fixed host:port, but the head's host is not known until Slurm places the job. A worker may now name a ray head service in the same config (`head: ray_head`); the job script exports that head's address as GYM_RAY_ADDRESS_<SERVICE>, resolved from its pool's first node, and the worker joins through it. The driver can read the same variable to connect. Also adds `extra_args` to ray services for flags the schema does not model, such as fixed ports that avoid another Ray instance on the same node. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
--relative is only where Slurm starts looking. On a real two-node job a ray head with --relative=0 landed on node 1, next to the worker, because node 0's resources were held by the policy step; it then tried to reach node 0's IP and never came up. The job script now exports each pool's hosts (GYM_POOL_<NAME>_NODES) from the allocation's node list, and a pinned service passes them to --nodelist. The ray head address reuses the same host list. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
With services pinned to pools, the driver's srun had no node constraint, and on a real two-node job Slurm ran it on the aux node. It reaches the policy on localhost and joins a Ray head through that node's raylet, so both failed. The driver now goes to the first node of the policy's pool. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
The driver reaches the policy on localhost, but its srun only asked for one node, so in a multi-node job Slurm could start it anywhere. A multi-node policy serves its API from node 0, a pinned one from its pool's first node; the driver now goes there in any multi-node job, not only when services are pinned. An unpinned service that fits on one node follows it, for the same reason. Also brace the --nodelist expansion. Unbraced, $gym_nodes[0] expands to the first host followed by a literal [0]. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
A ray service now names the node pools it spans instead of starting a head and workers separately. The head starts beside the driver, every other node of those pools joins it as a worker, and all of them use the service's one container. `resources` is keyed by pool, so only the pools that need a custom resource advertise it. Removes `mode`, `address` and `head`. Workers wait for the head, and flags Ray accepts only on a head are dropped from their command. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
When two or more vLLM services share a node, each now gets its own CUDA_VISIBLE_DEVICES (TP x PP x instances per node, in declaration order). A ray service there gets the leftover GPUs. A node with one GPU service renders nothing new. A manual CUDA_VISIBLE_DEVICES index list wins and reserves its GPUs. Overlaps and over-subscription fail validation. `share_gpus: true` opts a service out. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
…vLLM beside Ray Slurm resets CUDA_VISIBLE_DEVICES to the step's GPUs, so a value passed as `env K=V srun` never reaches the task. Checked on a 4-GPU GB200 node: the env-prefix value came back as 0,1,2,3, while `export` inside the step's command left CUDA seeing only the planned GPUs. Planned and manual values on a packed node are now exported in the step. A lone vLLM service beside a GPU-using ray service is now packed too, so Ray no longer advertises the model's GPUs. A ray service with num_gpus: 0, or a single GPU service on a node, still renders nothing new. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
A pool with `nodes: auto` is sized by Gym. The policy goes on the pool's first node, beside the driver. The other vLLM services go biggest first into the first node with room, never split. A service bigger than one node gets ceil(need / gpus_per_node) whole nodes of its own. More than one node splits the pool into sub-pools `<pool>-<i>`, placed with the existing node-pool --nodelist mechanism; a ray service spanning the pool spans them all and gets each node's leftover GPUs. Explicit node counts are unchanged. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
A service's own CUDA_VISIBLE_DEVICES was passed as `env K=V srun`, which Slurm resets to the step's GPUs, so it was silently ignored. It is now exported inside the step's command on every node, including a node with a single service. A runtime:VAR value is relayed as GYM_CUDA_VISIBLE_DEVICES so the step still exports the batch shell's $VAR. Drop runtime:CUDA_VISIBLE_DEVICES from the multi-instance example: it re-exported the job's full GPU set and did nothing. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
The batch script runs on node 0 and checked every service on localhost,
so a service pinned to a later pool, or placed beside a policy on one,
never passed its health check. `nodes: auto` makes that placement
routine. A service off node 0 is now checked at ${gym_nodes[i]}; node 0
keeps localhost, so single-node and node-0 renders are unchanged.
Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
The health probe runs from the batch script on node 0, but a pinned service answers on its pool's first node, and a ray head and the collector follow the driver's node. Probed on localhost, they waited out their timeout on a later node. Probe them where _service_nodelist places them. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
…-packing Keep the base branch's health-probe fix (_probe_host, _health_check_host, render_health_check host) and drop this branch's duplicate helper and its two overlapping tests. Sub-pools from `nodes: auto` are ordinary pinned pools by render time, so the base helper already probes them on their node; the auto test stays as coverage for that. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows up on review of #3654 (#3654 (comment)): automatic GPU placement, agreed to land separately. Stacked on it: base is
wprazuch/service-node-placement.In one sentence
Services that share a node now get their own GPUs automatically, unless you say otherwise.
The problem
Every service on a node sees every GPU. Put a 1-GPU judge and a 2-GPU policy on one 4-GPU node and both start on GPU 0. Today the only fix is writing
CUDA_VISIBLE_DEVICESby hand in each service'senv.What changed
When two or more services that use GPUs share a node (same
node_pool, or both in a one-node job), each vLLM service gets its ownCUDA_VISIBLE_DEVICES. The count is TP x PP x instances on that node. Services are handed the lowest free GPUs in the order they are declared.The value is exported inside the step's command, not passed as
env K=V srun. On a 4-GPU GB200 node, Slurm reset an env-prefix value to0,1,2,3(with and without pyxis). Anexportinside the step survived, and CUDA saw only the planned GPUs:CUDA_VISIBLE_DEVICESyou set yourself wins, and its GPUs count as taken. It now actually takes effect: it is exported inside the step too, even for a single service on a node (under Slurm it was silently ignored before).node_pool, another node, orshare_gpus.share_gpus: trueopts a service out.Automatic node choice
A pool with
nodes: autois sized by Gym. The policy goes on the pool's first node, beside the driver. The other services go biggest first into the first node with room, and are never split. A service bigger than one node gets whole nodes of its own: 6 GPUs on 4-GPU nodes takes 2 nodes. More than one node splits the pool into sub-pools<pool>-0,<pool>-1, ..., placed like any pool. A ray service spanning the pool spans all of them and gets each node's leftovers.Health checks reach a service on the node it runs on. That fix now comes from #3654. This PR only extends its coverage to auto-sized pools: a service
nodes: automoves to a later node is probed there.What stays the same
A node with a single GPU service renders nothing new, and explicit node counts are unchanged. A ray service with
num_gpus: 0does not count as a GPU service. A manual value that is not an index list (e.g.runtime:CUDA_VISIBLE_DEVICES) is kept and treated as sharing.Out of scope
nodes: autopacks only within its own pool.nodes: auto, a Ray-placed service bigger than a node can leave GPUs unused on its last node.Testing
tests/unit_tests/test_slurm_script.pycover: disjoint sets, a lone service renders nothing, manual wins and reserves, overlap and over-subscription errors, opt-out, Ray leftovers, per-pool packing, multi-node data parallel, the in-step export, andnodes: auto(1 GPU + 2 GPUs packs to one node and renders exactly likenodes: 1, 6 + 4 GPUs on 4-GPU nodes, the policy on node 0, explicit counts unchanged, Ray leftovers, manual values).ruff checkandruff format --checkare clean.gym eval submit, AIME25 with a vLLM policy and a separate vLLM judge on one node):nodes: 1: policy gotCUDA_VISIBLE_DEVICES=0,1, judge got2.nvidia-smishowed the policy's two workers on GPUs 0 and 1, the judge on GPU 2, and GPU 3 empty. 256/256 rollouts, 0 failures. The policy served 256 requests and the judge 70. No OOM.nodes: auto, same services: resolved to 1 node and rendered the same script, with the same GPU split. 256/256 rollouts, the judge served 64 requests.nodes: autowith 6 GPUs of need: resolved to 2 nodes. Policy (0,1) and judge (2,3) on node 0, the third model alone on node 1. It passed its health check at${gym_nodes[1]}and answered a request there.