Repository navigation
gpu: run one simulation on several GPUs (HIP backend) - #253
unsettledroell404 wants to merge 2 commits into
Conversation
The GPU engine used one device, so its memory limited the size of a model. With --gpu-devices=0,1 (gpu_devices=[0,1] from Python) the HIP backend now splits the grid along x into one part per listed device and runs the parts side by side. --gpu-device=N works as before, and a run on one device takes the same code path as before. How it works: - Each part holds the x lines it owns plus one copy of the neighbouring line at each cut (a halo line). The update kernels are unchanged: they see a smaller grid. - Before each voltage or current update the halo lines are copied from the neighbouring part, device to device. Stream events order the copies and the updates, so the host does not wait in between. - Every extension of the HIP backend is set up per part, for the nodes that part owns. Field snapshots and the frequency domain sums of the dumps are also taken per part and put together on the host. Limits when the grid is split: - The fused voltage and current step is not used, and the UPML runs in its separate kernels. A split run on one device is therefore slower than an unsplit one; the gain comes from the second device. - A Mur boundary or absorbing sheet normal to x whose lines fall on two parts is not split: the extensions then run on the host. - Cylindrical multi-grid meshes are refused, and each part needs at least 3 mesh lines along x. A device may be listed more than once (0,0): the parts then share one GPU. That gives no speed, but tests the split on a machine with one GPU. Engine_Compare.py does this for every case that can be split and requires results bit-identical to the unsplit run. For tests there are also OPENEMS_GPU_DEVICES (the device list) and OPENEMS_HIP_EXTENSIONS=0 (all extensions on the host). Measured on the two GPUs of a Tesla K80 (CUDA 11.4): all results are bit-identical to one GPU, for 2 to 13 parts. A cavity of 100 M cells runs at 2368 MC/s on both GPUs, against 1667 on one (1339 without the fused step); with a PML on all sides 1955 against 1310. Generated-by: Claude Fable 5.2 (Claude Code) Signed-off-by: Roel Budé <roel.bude@proton.me>
|
tbd if something like #252 should be considered together with this idea. |
|
Thanks for sharing this! We've also been running single simulations across mixed RTX 3080 and RTX 4090 cards in an experimental implementation, including configurations with up to six GPUs. Two things have been useful for our setup:
I noticed your implementation currently divides the X lines equally. Have you tested the device-to-device copy path on separate cards without P2P support? I understand the runtime may stage those transfers internally. Our implementation is here if useful: td-krzysiek#25. It keeps partitioning and communication separate from the backend, with HIP and Metal adapters. It is still experimental. Your event-based halo scheduling and per-part DFT accumulation look particularly useful — there may be opportunities to combine ideas from both approaches. |
Thanks, useful to see. No, I have only run it on the two GPUs of one Tesla K80. The code never enables peer access and uses a plain device-to-device hipMemcpyAsync, so I expect the runtime to stage the copy itself on a machine without P2P, but I have not measured that. If you have time, a run of this branch on your 3080/4090 machine would settle it: Results should be bit-identical to a one-GPU run of the same branch. If the direct copy fails or is slow there, a host-staged path like yours should be added. As for the unequal split; I think that is something I can implement here as well. |
--gpu-devices splits the grid into parts with the same number of x lines. With GPUs of different speed or memory the parts should differ: --gpu-domain-cuts=0,100,300 gives the x lines of the split, one more than devices, from 0 to the number of x lines. Here lines 0-99 go to the first device and 100-299 to the second. From Python: gpu_domain_cuts=[0, 100, 300]; for tests the environment variable OPENEMS_GPU_DOMAIN_CUTS. Each part needs at least 3 lines; cuts that do not fit the mesh or the number of devices are an error. The option name and format follow the multi-GPU work of td-krzysiek (td-krzysiek#25). Tested on a Tesla K80 (two GPUs): four models (cavity, PML on all sides, field dumps, lumped port) with parts of 7+34 lines on two GPUs and 5+32+4 lines in three parts give results bit-identical to a one-GPU run. Generated-by: Claude Fable 5.2 (Claude Code) Signed-off-by: Roel Budé <roel.bude@proton.me>
Context
I have access to an ancient K80 GPU, which has two separate GPU cores on a single board and each core can only talk to 12GB of VRAM. It acts essentially like a dual GPU setup with a fast link between then so I wanted to try and get this to work. The work done here should allow running several (modern) GPUs as well. The K80 is very slow, only outpacing my Laptop's GPU by 60%, but it gives me access to 24GB of VRAM which is great for large models.
I know the maintainer is busy with the Kernels and GPU support. This does not change the kernels, but I will keep it as WIP anyway, since so far some things don't work well and I still need to understand the code. Just wanted to show the possibility here. I'd love to see someone test this further.
(end human written part..)
The GPU engine uses one device, so that device's memory limits the model size. This PR lets the HIP backend split the grid along x over several GPUs:
or
FDTD.Run(..., engine='gpu', gpu_devices=[0, 1])from Python.--gpu-device=Nis unchanged, and a run on one device takes the same code path as before.How it works
Engine_GPU, theGPU_Backendinterface, the update kernels, or the Metal and reference backends.Limits with a split grid
Results (two GPUs of a Tesla K80, CUDA 11.4)
All probe files and field dumps are bit-identical to a one-GPU run, for 2 to 13 parts, with each extension.
Device memory for the PML model: 5329 MiB on one GPU, 1515 + 1509 MiB on two.
Tests
Engine_Compare.pynow runs every case that can be split a second time with the grid in two parts on one device (OPENEMS_GPU_DEVICES=0,0) and requires results bit-identical to theunsplit run. This needs only one GPU. On the K80 the new check passes in all 19 cases it applies to.
On this K80, five UPML cases of
Engine_Compare.pyare not bit-identical to the basic engine (4–5e-6) anddumpsfails on the reference backend. I see the same without this change.Not tested: AMD GPUs (only HIP compiled as CUDA), and GPUs of different types in one run.
Generated with Claude Fable 5.2 (Claude Code)