This repository was archived by the owner on Sep 18, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 64
Add all-to-all benchmark #760
Open
pentschev
wants to merge
44
commits into
rapidsai:branch-0.21
Choose a base branch
from
pentschev:all-to-all-benchmark
base: branch-0.21
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
3c0a12a
Add multiprocess test to perform all-to-all ep creation
pentschev 35664da
Add support for persistent endpoints in multiprocess all_to_all test
pentschev 65f1870
Improve closing of endpoints in multiproc all_to_all test
pentschev ba66c92
Do more transfers between worker pairs
pentschev b712ce9
Support multiple endpoints per worker in multiproc all_to_all test
pentschev e8fee54
Merge remote-tracking branch 'upstream/branch-0.21' into all-to-all-test
pentschev aa06731
Remove monitor process in favor of multiprocessing shared memory
pentschev ce98f43
Merge remote-tracking branch 'upstream/branch-0.21' into all-to-all-test
pentschev 68c07a6
Mark some multiple processes all-to-all tests as slow
pentschev c91ec3c
Store remote EP port in dictionary
pentschev 33b630d
Mark more tests as slow
pentschev 4f41ebe
Benchmark for multiprocess all-to-all
pentschev b6508ed
Add Tornado-based all-to-all benchmark
pentschev ac88060
Change all-to-all worker function into class
pentschev 4c485c2
Use BaseWorker class to create TornadoWorker all-to-all benchmark
pentschev 6ddcf04
Add asyncio all-to-all benchmark
pentschev 9675844
Remove non-persistent endpoints
pentschev c8fb2a7
Split UCX transfers in frames to make it similar to Tornado/Asyncio
pentschev bf48ebd
Move serialization to comms send method
pentschev 7d2eaff
Add monitor process to allow syncing workers over network
pentschev 8ae422a
Split `run` into multiple methods
pentschev da2320e
Parametrize communication and monitor enable/disable
pentschev e21a973
Remove worker_num argument
pentschev 6c7de45
Make shm synchronization arguments optional
pentschev 1bdc4d2
Add listener_address argument
pentschev da75bc1
Separate benchmark into multiple files
pentschev 9202310
Improve benchmark result formatting
pentschev dba6dbf
Reorganize run_worker and run_monitor
pentschev 26c3712
Fix single-node benchmark
pentschev 199f960
Add more all-to-all benchmark CLI arguments
pentschev 49b194f
Remove hardcoded enp1s0f0 interface
pentschev 796880f
Remove redundant --multi-node argument
pentschev cc7961b
Improve documentation for all-to-all benchmark CLI arguments
pentschev 18a4557
Fix all-to-all benchmark test
pentschev 58a30f8
Change worker address formatting in benchmark output
pentschev 5a509e3
Add --port argument to specify monitor port
pentschev 75fca3d
Fix issues with all-to-all pytest
pentschev b1b0b1b
Remove usage of partial
pentschev d3e835d
Add all-to-all benchmark with uvloop
pentschev bd86b11
Add uvloop support for multi-node benchmark
pentschev a96358c
Fix bandwidth calculation
pentschev b3bdfb0
Parse bytes with --size
pentschev 29468b4
Skip uvloop test when not installed
pentschev 14010ac
Raise when number of workers don't match
pentschev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| import argparse | ||
| import multiprocessing | ||
|
|
||
| from utils_all_to_all import ( | ||
| asyncio_process, | ||
| tornado_process, | ||
| ucx_process, | ||
| uvloop_process, | ||
| ) | ||
|
|
||
| from dask.utils import parse_bytes | ||
|
|
||
| import ucp | ||
|
|
||
|
|
||
| def parse_args(): | ||
| parser = argparse.ArgumentParser(description="All-to-all benchmark") | ||
| parser.add_argument( | ||
| "--monitor", | ||
| default=False, | ||
| action="store_true", | ||
| help="Start a monitor process only. Requires --num-worker processes " | ||
| "started with --worker to connect to monitor. Default: disabled.", | ||
| ) | ||
| parser.add_argument( | ||
| "--worker", | ||
| default=False, | ||
| action="store_true", | ||
| help="Start a worker process only. Requires a --monitor process " | ||
| "to connect to, with an address specified with --monitor-address. " | ||
| "Default: disabled.", | ||
| ) | ||
| parser.add_argument( | ||
| "--enable-monitor", | ||
| default=False, | ||
| action="store_true", | ||
| help="Use a monitor process to synchronize workers when in single-node " | ||
| "mode (when neither --monitor or --worker are requested), otherwise " | ||
| "synchronize processes via multiprocessing shared memory. Default: " | ||
| "disabled.", | ||
| ) | ||
| parser.add_argument( | ||
| "--listen-interface", | ||
| default=None, | ||
| help="Interface where monitor (if --monitor), or worker (if --worker), " | ||
| "or all (in single-node mode, i.e., no --monitor or --worker are " | ||
| "specified) will listen for connections.", | ||
| ) | ||
| parser.add_argument( | ||
| "--monitor-address", | ||
| metavar="IP:PORT", | ||
| default=None, | ||
| help="Address where --monitor process is listening to in the HOST:PORT " | ||
| "format.", | ||
| ) | ||
| parser.add_argument( | ||
| "--port", | ||
| default=None, | ||
| type=int, | ||
| help="Port where --monitor will listen. Only applies to --monitor " | ||
| "process, --worker process should still use --monitor-address to " | ||
| "specify the monitor address. Default: random port.", | ||
| ) | ||
| parser.add_argument( | ||
| "--num-workers", | ||
| default=2, | ||
| type=int, | ||
| help="Number of workers to start in single-node mode, or number of " | ||
| "workers that --monitor process will wait to connect before starting " | ||
| "transfers between workers. Default: 2.", | ||
| ) | ||
| parser.add_argument( | ||
| "--endpoints-per-worker", | ||
| default=1, | ||
| type=int, | ||
| help="Number of simultaneous endpoints between each worker pair that " | ||
| "will send and receive data. In a case where --num-workers=2, this " | ||
| "translates to Worker1 creating two endpoints connecting to the " | ||
| "listener on Worker2, with Worker2 creating another two endpoints " | ||
| "connecting to the listener on Worker1, totalling four endpoint pairs " | ||
| "sending and receiving benchmark data simultaneously. Default: 1.", | ||
| ) | ||
| parser.add_argument( | ||
| "--communication-lib", | ||
| default="ucx", | ||
| type=str, | ||
| help="Communication library to benchmark. Options are " | ||
| "'ucx', 'asyncio', 'tornado', 'uvloop'. Default: 'ucx'.", | ||
| ) | ||
| parser.add_argument( | ||
| "--size", | ||
| metavar="BYTES", | ||
| default="1 MiB", | ||
| type=parse_bytes, | ||
| help="Size to be passed for data generation function. Default: '1 Mb'.", | ||
| ) | ||
| parser.add_argument( | ||
| "--iterations", | ||
| default=15, | ||
| type=int, | ||
| help="Number of iterations of data transfers per worker pair. " | ||
| "Each iteration consists in sending and receiving the same " | ||
| "data amount. Default: 15.", | ||
| ) | ||
| parser.add_argument( | ||
| "--gather-send-recv", | ||
| default=False, | ||
| action="store_true", | ||
| help="If disabled (default), send and receive operations will " | ||
| "be awaited individually, otherwise they are launched " | ||
| "simultaneously via an asyncio.gather or gen.multi operation. " | ||
| "Default: disabled.", | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
| if args.worker and args.monitor: | ||
| raise RuntimeError("--monitor and --worker can't be defined together.") | ||
| return args | ||
|
|
||
|
|
||
| def main(): | ||
| args = parse_args() | ||
|
|
||
| num_workers = args.num_workers | ||
| endpoints_per_worker = args.endpoints_per_worker | ||
| listener_address = ucp.get_address(ifname=args.listen_interface) | ||
| if args.monitor_address is not None: | ||
| monitor_address, monitor_port = args.monitor_address.split(":") | ||
| monitor_port = int(monitor_port) | ||
|
|
||
| if args.communication_lib == "ucx": | ||
| communication_func = ucx_process | ||
| elif args.communication_lib == "asyncio": | ||
| communication_func = asyncio_process | ||
| elif args.communication_lib == "uvloop": | ||
| communication_func = uvloop_process | ||
| elif args.communication_lib == "tornado": | ||
| communication_func = tornado_process | ||
| else: | ||
| raise ValueError( | ||
| f"Communication library {args.communication_lib} not supported" | ||
| ) | ||
|
|
||
| if args.monitor is True: | ||
| communication_func( | ||
| listener_address, | ||
| num_workers, | ||
| endpoints_per_worker, | ||
| True, | ||
| args.port, | ||
| args.size, | ||
| args.iterations, | ||
| args.gather_send_recv, | ||
| shm_sync=False, | ||
| ) | ||
| elif args.worker is True: | ||
| communication_func( | ||
| listener_address, | ||
| num_workers, | ||
| endpoints_per_worker, | ||
| False, | ||
| monitor_port, | ||
| args.size, | ||
| args.iterations, | ||
| args.gather_send_recv, | ||
| shm_sync=False, | ||
| ) | ||
| else: | ||
| ctx = multiprocessing.get_context("spawn") | ||
|
|
||
| signal = ctx.Array("i", [0, 0]) | ||
| ports = ctx.Array("i", range(num_workers)) | ||
| lock = ctx.Lock() | ||
|
|
||
| monitor_port = 0 | ||
|
|
||
| if args.enable_monitor: | ||
| monitor_process = ctx.Process( | ||
| name="worker", | ||
| target=communication_func, | ||
| args=[ | ||
| listener_address, | ||
| num_workers, | ||
| endpoints_per_worker, | ||
| True, | ||
| 0, | ||
| args.size, | ||
| args.iterations, | ||
| args.gather_send_recv, | ||
| ], | ||
| kwargs={ | ||
| "shm_sync": True, | ||
| "signal": signal, | ||
| "ports": ports, | ||
| "lock": lock, | ||
| }, | ||
| ) | ||
| monitor_process.start() | ||
|
|
||
| while signal[0] == 0: | ||
| pass | ||
|
|
||
| monitor_port = signal[0] | ||
|
|
||
| worker_processes = [] | ||
| for worker_num in range(num_workers): | ||
| worker_process = ctx.Process( | ||
| name="worker", | ||
| target=communication_func, | ||
| args=[ | ||
| listener_address, | ||
| num_workers, | ||
| endpoints_per_worker, | ||
| False, | ||
| monitor_port, | ||
| args.size, | ||
| args.iterations, | ||
| args.gather_send_recv, | ||
| ], | ||
| kwargs={ | ||
| "shm_sync": not args.enable_monitor, | ||
| "signal": signal, | ||
| "ports": ports, | ||
| "lock": lock, | ||
| }, | ||
| ) | ||
| worker_process.start() | ||
| worker_processes.append(worker_process) | ||
|
|
||
| for worker_process in worker_processes: | ||
| worker_process.join() | ||
|
|
||
| if args.enable_monitor: | ||
| monitor_process.join() | ||
|
|
||
| assert worker_process.exitcode == 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import multiprocessing | ||
|
|
||
| import pytest | ||
| from utils_all_to_all import ( | ||
| asyncio_process, | ||
| tornado_process, | ||
| ucx_process, | ||
| uvloop_process, | ||
| ) | ||
|
|
||
| import ucp | ||
|
|
||
|
|
||
| def _test_send_recv_cu( | ||
| num_workers, endpoints_per_worker, enable_monitor, size, iterations, communication | ||
| ): | ||
| ctx = multiprocessing.get_context("spawn") | ||
|
|
||
| listener_address = ucp.get_address() | ||
| monitor_port = 0 | ||
|
|
||
| signal = ctx.Array("i", [0, 0]) | ||
| ports = ctx.Array("i", range(num_workers)) | ||
| lock = ctx.Lock() | ||
|
|
||
| if enable_monitor: | ||
| monitor_process = ctx.Process( | ||
| name="worker", | ||
| target=communication, | ||
| args=[ | ||
| listener_address, | ||
| num_workers, | ||
| endpoints_per_worker, | ||
| True, | ||
| 0, | ||
| size, | ||
| iterations, | ||
| False, | ||
| ], | ||
| kwargs={"shm_sync": True, "signal": signal, "ports": ports, "lock": lock}, | ||
| ) | ||
| monitor_process.start() | ||
|
|
||
| while signal[0] == 0: | ||
| pass | ||
|
|
||
| monitor_port = signal[0] | ||
|
|
||
| worker_processes = [] | ||
| for worker_num in range(num_workers): | ||
| worker_process = ctx.Process( | ||
| name="worker", | ||
| target=communication, | ||
| args=[ | ||
| listener_address, | ||
| num_workers, | ||
| endpoints_per_worker, | ||
| False, | ||
| monitor_port, | ||
| size, | ||
| iterations, | ||
| False, | ||
| ], | ||
| kwargs={ | ||
| "shm_sync": not enable_monitor, | ||
| "signal": signal, | ||
| "ports": ports, | ||
| "lock": lock, | ||
| }, | ||
| ) | ||
| worker_process.start() | ||
| worker_processes.append(worker_process) | ||
|
|
||
| for worker_process in worker_processes: | ||
| worker_process.join() | ||
|
|
||
| if enable_monitor: | ||
| monitor_process.join() | ||
|
|
||
| assert worker_process.exitcode == 0 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("num_workers", [2, 4, 8]) | ||
| @pytest.mark.parametrize("endpoints_per_worker", [1]) | ||
| @pytest.mark.parametrize("enable_monitor", [True, False]) | ||
| @pytest.mark.parametrize("size", [2 ** 20]) | ||
| @pytest.mark.parametrize("iterations", [5]) | ||
| @pytest.mark.parametrize( | ||
| "communication", [ucx_process, asyncio_process, uvloop_process, tornado_process] | ||
| ) | ||
| def test_send_recv_cu( | ||
| num_workers, endpoints_per_worker, enable_monitor, size, iterations, communication | ||
| ): | ||
| if communication == uvloop_process: | ||
| pytest.importorskip("uvloop", reason="uvloop not installed") | ||
|
|
||
| _test_send_recv_cu( | ||
| num_workers, | ||
| endpoints_per_worker, | ||
| enable_monitor, | ||
| size, | ||
| iterations, | ||
| communication, | ||
| ) |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want this in
tests/or would it make sense to include inbenchmarks/?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still thinking about it. I don't want it in tests, but I want a test there (for the UCX part only). However, a lot of the code is going to be shared and we don't have a good place currently where that common code would be visible to both. I'm not even sure the non-UCX code should live in this repo as we'll soon upstream it to OpenUCX, so it doesn't really make sense to have non-UCX code there. I'm still thinking of an appropriate place for this, if you have any suggestions, please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that makes sense. Will think about it as well.