CUDA: Support CUDA Virtual Devices - #25228
Conversation
|
Could you please rebase your changes on the TOT master? |
Can't this already be achieved by using RPC without any additional code changes? Connecting to a edit: note from future self: Virtual device is much lighter abstraction than rpc, which additionally does not support all operations that virtual device do. Similar feature already exists for metal backend. |
dc6c012 to
f57debf
Compare
|
|
||
| static void ggml_backend_cuda_comm_init_nccl(ggml_backend_cuda_comm_context * ret) { | ||
| #ifdef GGML_USE_NCCL | ||
| const size_t n = ret->dev_ids.size(); |
There was a problem hiding this comment.
IIUC, these dev_ids are virtual devices, ncclCommInitAll will fail if you pass [0,1] on a single device.
I think it might be better to disable the NCCL path for the virtual device case.
There was a problem hiding this comment.
I've disabled the NCCL path when virtual devices are being used.
| } | ||
|
|
||
| static void ggml_backend_cuda_comm_init_internal(ggml_backend_cuda_comm_context * ret) { | ||
| ret->ar_pipeline = ggml_cuda_ar_pipeline_init(ret->dev_ids.data(), ret->dev_ids.size()); |
There was a problem hiding this comment.
Does the internal allReduce implementation work fine with virtual devices?
There was a problem hiding this comment.
Yes, the internal AllReduce does work with virtual devices because every CUDA-ordinal call in the pipeline goes through ggml_cuda_set_device() which uses the virtual to physical device mapping. Also, each rank's buffers / streams / events are separate allocations, so two ranks backed by the same physical GPU don't collide. Let me know if I've missed anything here.
In my testing, I verified this by forcing each AllReduce path with virtual devices via GGML_CUDA_ALLREDUCE (internal / none) and setting device count via GGML_CUDA_DEVICES.
internalpath:CUDA_VISIBLE_DEVICES=0 GGML_CUDA_DEVICES=2(2 vdevs, 1 phys) plus explicitGGML_CUDA_ALLREDUCE=internal. Got coherent and deterministic results, and the chunked kernel and copy-engine paths agreed (toggled viaGGML_CUDA_AR_COPY_THRESHOLD).- meta backend (butterfly) fallback:
GGML_CUDA_ALLREDUCE=none(n=2), and auto-fallback forn=3,4was also coherent and deterministic. 2 vdevs / 1 physgave byte-identical output to2 vdevs / 2 phys.
This testing was done across Linux (2x B200) and Windows (2x RTX 5090).
de9cc50 to
2976e33
Compare
|
Let's try adding llama.cpp/.github/workflows/server-self-hosted.yml Lines 83 to 101 in 683f0c7 Btw, I notice that with CUDA, I have to explicitly pass the 2 devices with # this automatically uses 2 virtual Metal devices:
make -j && GGML_METAL_DEVICES=2 ./bin/llama-completion -hf ggml-org/Qwen3-0.6B-GGUF:Q8_0 -p "I believe the meaning of life is" -n 32 --sampling-seq "k" --top-k 1 -no-cnv -lv 4
# in contrast, this uses only one device:
make -j && GGML_CUDA_DEVICES=2 ./bin/llama-completion -hf ggml-org/Qwen3-0.6B-GGUF:Q8_0 -p "I believe the meaning of life is" -n 32 --sampling-seq "k" --top-k 1 -no-cnv -lv 4
# adding the devices explicitly works as expected
make -j && GGML_CUDA_DEVICES=2 ./bin/llama-completion -hf ggml-org/Qwen3-0.6B-GGUF:Q8_0 -p "I believe the meaning of life is" -n 32 --sampling-seq "k" --top-k 1 -no-cnv -lv 4 -sm layer -dev CUDA0,CUDA1 |
JohannesGaessler
left a comment
There was a problem hiding this comment.
In terms of the program logic this is I think a good implementation and would require only minimal changes from my end.
| void ggml_backend_cuda_get_device_description(int device, char * description, size_t description_size) { | ||
| cudaDeviceProp prop; | ||
| CUDA_CHECK(cudaGetDeviceProperties(&prop, device)); | ||
| CUDA_CHECK(cudaGetDeviceProperties(&prop, ggml_cuda_get_physical_device(device))); |
There was a problem hiding this comment.
Preferable the device description would specify the index of a virtual device per physical device.
@ggerganov I haven't been able to reproduce this issue in my tests across two systems. In my tests,
@JohannesGaessler the description now looks like the following: |
Yes, I think the problem was on my end. It works correctly now. Running the CI here to see how it goes: https://github.com/ggml-org/llama.cpp/actions/runs/29398656862/job/87297892207. This should run the server E2E tests on a DGX Spark machine using 2 virtual devices. |
| // map a (possibly virtual) device id to the physical CUDA device that backs it | ||
| int ggml_cuda_get_physical_device(int device); | ||
|
|
There was a problem hiding this comment.
nit: this can be a static function inside ggml-cuda.cu - we don't expect it to be used in the rest of the CUDA backend, so no need to declare it in the common header.
|
After resolving the conflict we can merge. |
d42b94b to
8406160
Compare
@anavp-nvidia To follow-up on this, the problem does occur only on the DGX Spark for some reason. Here is a command to test with: make -j && GGML_CUDA_DEVICES=2 ./bin/llama-completion -hf ggml-org/Qwen3-0.6B-GGUF:Q8_0 -p "I believe the meaning of life is" -n 32 --sampling-seq "k" --top-k 1 -no-cnv -lv 4On my RTX 5090 box this correctly uses 2 devices, while on the DGX Spark it uses only one: # running on the RTX 5090 machine (correct):
0.00.638.987 I common_memory_breakdown_print: | memory breakdown [MiB] | total free self model context compute unaccounted |
0.00.638.990 I common_memory_breakdown_print: | - CUDA0 (RTX 5090 (physical device 0, virtual device 0)) | 16054 = 10763 + (2831 = 239 + 2400 + 192) + 2459 |
0.00.638.990 I common_memory_breakdown_print: | - CUDA1 (RTX 5090 (physical device 0, virtual device 1)) | 16054 = 10763 + (2927 = 364 + 2080 + 482) + 2363 |
0.00.638.990 I common_memory_breakdown_print: | - Host | 321 = 157 + 0 + 164 |
# running on the DSX Spark (incorrect):
0.00.712.915 I common_memory_breakdown_print: | memory breakdown [MiB] | total free self model context compute unaccounted |
0.00.712.921 I common_memory_breakdown_print: | - CUDA0 (GB10 (physical device 0, virtual device 0)) | 61286 = 58654 + (5382 = 604 + 4480 + 298) + -2751 |
0.00.712.921 I common_memory_breakdown_print: | - Host | 201 = 157 + 0 + 44 |This likely means that the server CI workflow that we added in this PR does not actually exercise the virtual devices because it runs on a DGX Spark machine. |
* support cuda virtual devices * disable NCCL path when virtual devices are used * label virtual devices in description; add GPUx2 server CI jobs * code refactor
* support cuda virtual devices * disable NCCL path when virtual devices are used * label virtual devices in description; add GPUx2 server CI jobs * code refactor
…king, Q1_0 vec_dot optimization, DSV4 deepseek4 fix, HIP build flags - Backport CUDA Virtual Devices (GGML_CUDA_DEVICES env var, ggml-org#25228): virtual device emulation with physical_device tracking, round-robin mapping, VRAM splitting, PCI bus ID uniquification, NCCL fallback, P2P peer access, cudaSetDevice/memcpyPeerAsync physical device translation - Backport MMQ kernel configuration refactor (ggml-org#24127) and tighter native fp4 buffer sizing (ggml-org#25613): QK_FP4_MMQ/QK8_1_MMQ named constants, block size fixes, disable MMQ on GPUs with <48 KiB shared mem (ggml-org#26141) - Backport argsort/top-k chunking (ggml-org#24776): process data in smaller chunks to reduce temporary buffer memory usage - Backport Q1_0 vec_dot optimization: use __byte_perm for faster crumb unpacking (ggml-org#25628), matching upstream llama.cpp's Q1_0 extraction - Backport transpose-free gemmv (ggml-org#26171): allow F32 MMVQ path for transposed vector matrix multiplies - Backport FLASH_ATTN_EXT backend scheduling skip (ggml-backend.cpp): prevent flash attn sinks tensor from incorrectly steering backend choice - Backport prop.integrated restore on HIP (ggml-org#24233), -funsafe-math-optimizations for HIP builds (ggml-org#24668), -ffast-math fix: no -ffast-math on HIP (ggml-org#25495) - Backport DeepSeek V4 l_last naming and graph fix: rename l_out -> l_last, add ggml_build_forward_expand for residual/post/comb in deepseek4.cpp, extend llama-context.cpp backend nudging to l_last - koboldcpp.py: add antirunopts list, fix getattr guard for checkforupdates - easy_KCPP-ROCm_install.sh: use -j$(nproc) instead of hardcoded -j4
* support cuda virtual devices * disable NCCL path when virtual devices are used * label virtual devices in description; add GPUx2 server CI jobs * code refactor
@ggml-org/ggml-cuda This continues to be a problem - for some reason the DGX Spark systems need to explicitly list the virtual devices in the command line, otherwise they are not being picked-up. This makes most multi-GPU runs of the CI useless because they effectively run on one GPU. For example, here when the workflow runs on non-DGX Spark machine it correctly reveals an issue: https://github.com/ggml-org/llama.cpp/actions/runs/31569042233/job/94026908737#step:7:96 But the same workflow when running on a DGX Spark does not detect the issue (due to the problem explained above): https://github.com/ggml-org/llama.cpp/actions/runs/31522118519/job/93881466133 |
Overview
Adds support for exposing a configurable number of virtual CUDA devices on top of the physical GPUs actually present, controlled by a new
GGML_CUDA_DEVICESenvironment variable. This allows developing and exercising multi-GPU code paths on machines with a single GPU.When
GGML_CUDA_DEVICESis unset, behavior is identical to before.-sm layer,-sm row, and-sm tensorare all supported under virtual CUDA devices. For-sm tensor, the internal CUDA AllReduce or the meta-backend fallbacks are supported since NCCL seems to require distinct physical GPUs.Additional information
The core idea is a single indirection: every (virtual) device id is mapped to a backing physical device id, and all raw CUDA calls that take a device ordinal use the physical id, while ggml-level bookkeeping (pools, buffers, device registry) stays per-virtual-device.
Design choices
ggml_cuda_init()parsesGGML_CUDA_DEVICES, builds the round-robin virtual to physical map, and computes how many virtual devices share each GPU.ggml_cuda_set_device()resolves to the physical device, and a newggml_cuda_get_physical_device()exposes the mapping.ggml_cuda_device_infogainsphysical_device_countand, per device,physical_deviceandphysical_share_count.-v<i>suffix to each virtual device'spci_bus_idso they stay unique.op_mul_mat_device(context member, set byggml_cuda_op_mul_matdispatcher) is used to identify the main device in mul mat execution instead ofggml_cuda_get_device(), which returns the physical device and was otherwise causing out-of-bounds writes.total/physical_share_countfor both free and total memory, so per-device VRAM sums to the physical total and model distribution doesn't overcommit a shared GPU.Testing
Validated on three Blackwell systems, using four models (Llama-3.2-1B-Instruct, Llama-3.1-8B-Instruct, Qwen2.5-32B-Instruct, Llama-3.3-70B-Instruct) with greedy decoding (temp 0) and a fixed seed, so as to compare virtual-device output against physical-GPU reference.
test-backend-opsunder2 vdev (virtual device) / 1 phys (physical device)configuration. TestedMUL_MAT/MUL_MAT_IDon baseline,2 vdev / 1 physand4 vdev / 2 physconfigurations.2 vdev / 2 phys,2 vdev / 1 phys,3 vdev / 2 phys,4 vdev / 2 phys,4 vdev / 1 physfor-sm layer,-sm row, and-sm tensor(non-NCCL paths):-sm layer/-sm row: output identical to the real-GPU baseline (flash-attn on/off on 1B/8B; on for 32B/70B)-sm tensor: valid coherent output, deterministic across configurations (n vdev/1 physwheren = 2, 3, 4), and independent of the virtual to physical mapping (n vdev/1 physproduces the same asn vdev/2 phys,n = 2, 4). Both all-reduce fallbacks exercised (GGML_CUDA_ALLREDUCE=internal/none).Requirements