-
Notifications
You must be signed in to change notification settings - Fork 58
[AIROCMLIR-375] Run tests in parallel across multiple GPUs #2419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 19 commits
f427ee2
fd1bc54
0dfda17
c3ac08b
8aad30c
f4cdd78
426fb79
8074d5f
5a54e9e
331bee4
34cf444
343fecf
3eb674f
b605c51
909f187
953f801
39be80c
f244dd0
837250c
c9e14cd
566b730
f1ffcb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1092,10 +1092,24 @@ int setLitWorkerCount() { | |
| return limit_lit_workers | ||
| } | ||
|
|
||
| // Run the lit suite sharded across the node's GPUs: one lit process per GPU, | ||
| // each pinned via ROCR_VISIBLE_DEVICES; single-GPU and heterogeneous nodes fall | ||
| // back to one run. jobsPerGpu keeps the per-GPU cap from setLitWorkerCount() | ||
| // instead of splitting it across shards (see #1845/#1841). shStrict mirrors the | ||
| // output into the per-row log so withHealthyNode can retry just this node. | ||
| // ci-performance-scripts copies the driver and gpu_topology.py into ./bin. | ||
| void runShardedE2E(int jobsPerGpu) { | ||
| dir('build') { | ||
| shStrict "python3 ./bin/run_e2e_multigpu.py --build-dir . " + | ||
| "--jobs-per-gpu ${jobsPerGpu} " + | ||
| "\"--lit-args=-v --time-tests --timeout=3600 --max-failures=1\"" | ||
| } | ||
| } | ||
|
|
||
| void build_fixedE2ETests(String codepath) { | ||
| // Limit the number of lit workers for gfx908, gfx90a to (8, 30) on CI as a workaround for issue #1845 and #1841 | ||
| int limit_lit_workers = setLitWorkerCount() | ||
| buildProject("check-mlir-build-only check-rocmlir-build-only${params.nightly ? ' hipblaslt-benchmark-driver' : ''}", """ | ||
| buildProject("check-mlir-build-only check-rocmlir-build-only ci-performance-scripts${params.nightly ? ' hipblaslt-benchmark-driver' : ''}", """ | ||
| -DROCMLIR_DRIVER_PR_E2E_TEST_ENABLED=${params.nightly ? '0' : '1'} | ||
| -DROCMLIR_DRIVER_E2E_TEST_ENABLED=${params.nightly ? '1' : '0'} | ||
| -DROCK_E2E_TEST_ENABLED=${params.nightly ? '1' : '0'} | ||
|
|
@@ -1109,21 +1123,19 @@ void build_fixedE2ETests(String codepath) { | |
| void check_randomE2ETests(String codepath) { | ||
| // Limit the number of lit workers for gfx908, gfx90a to (8, 30) on CI as a workaround for issue #1845 and #1841 | ||
| int limit_lit_workers = setLitWorkerCount() | ||
| // Configure and build the E2E deps without running the tests, then run the GPU tests via | ||
| // shStrict so their stdout is mirrored to the per-row log (withHealthyNode classifies GPU | ||
| // hangs there and retries only this node). Running check-rocmlir directly through cmakeBuild | ||
| // would bypass shStrict and force a whole-job re-kick instead. | ||
| buildProject('check-rocmlir-build-only', """ | ||
| // Build the E2E deps without running the tests, then run the suite through the sharding | ||
| // driver: going through cmakeBuild would bypass shStrict and force a whole-job re-kick | ||
| // instead of a single-node retry. | ||
| buildProject('check-rocmlir-build-only ci-performance-scripts', """ | ||
| -DROCMLIR_DRIVER_PR_E2E_TEST_ENABLED=0 | ||
| -DROCMLIR_DRIVER_E2E_TEST_ENABLED=1 | ||
| -DROCK_E2E_TEST_ENABLED=1 | ||
| -DROCMLIR_DRIVER_RANDOM_DATA_SEED=1 | ||
| -DROCMLIR_DRIVER_TEST_GPU_VALIDATION=0 | ||
| -DLLVM_LIT_ARGS='-v --time-tests --timeout=3600 --max-failures=1 -j ${limit_lit_workers}' | ||
| -DCMAKE_EXPORT_COMPILE_COMMANDS=1 | ||
| """) | ||
| timeout(time: 60, activity: true, unit: 'MINUTES') { | ||
| shStrict 'cd build; ninja check-rocmlir' | ||
| runShardedE2E(limit_lit_workers) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are passing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per-GPU it's unchanged so each GPU gets the same setLitWorkerCount() cap it had when everything ran on GPU 0 which is why I figured running the same count on each GPU was safe. But you have a point that host-side it's now 8× what that box has ever seen. My inclination would be to keep the per-GPU cap but also clamp the total, something like min(jobsPerGpu, hostBudget / numGpus), so the GPU-side limit still holds on small nodes while big nodes don't oversubscribe the host. What do you think is the best approach here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problems that we saw with high concurrency in the past were always with running on the GPU, so as long as those caps remain in place it does make sense to bump the CPU side. The only thing that should be checked is if the CPU (host) actually supports the amount of parallelism that we are requesting. I.e., should we do a check of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, the driver now caps at min(jobsPerGpu, hostCPUs / numShards), using sched_getaffinity (what nproc reports, and it respects the container's cpuset). Per-GPU caps are untouched. On an 8-GPU gfx942 node with 128 cores that's 8×16 instead of 8×64 |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -1555,7 +1567,8 @@ pipeline { | |
| build_fixedE2ETests("${CODEPATH}") | ||
| preMergeCheck("${CODEPATH}") | ||
| timeout(time: 60, activity: true, unit: 'MINUTES') { | ||
| shStrict 'cd build; ninja check-mlir check-rocmlir' | ||
| shStrict 'cd build; ninja check-mlir' | ||
| runShardedE2E(setLitWorkerCount()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| # Part of the MLIR Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| # See https://llvm.org/LICENSE.txt for license information. | ||
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| """Shared helpers for distributing GPU test workloads across multiple devices. | ||
|
|
||
| CI nodes commonly expose several GPUs. These helpers detect the visible GPUs, | ||
| confirm they share a single architecture (a prerequisite for safely reusing the | ||
| same compiled kernels), and build per-process environments that isolate work to | ||
| one device via ROCR_VISIBLE_DEVICES (the same mechanism tuningRunner.py uses). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from typing import List, Optional, Tuple | ||
|
|
||
|
|
||
| def _hip_check(call_result): | ||
| """Unwrap a hip-python call result, raising on a non-success status.""" | ||
| from hip import hip | ||
| err = call_result[0] | ||
| result = call_result[1:] | ||
| if len(result) == 1: | ||
| result = result[0] | ||
| if isinstance(err, hip.hipError_t) and err != hip.hipError_t.hipSuccess: | ||
| raise RuntimeError(str(err)) | ||
| return result | ||
|
|
||
|
|
||
| def get_per_device_archs() -> List[str]: | ||
| """Return the gfx architecture name of every visible GPU, indexed by id. | ||
|
|
||
| Uses hip-python, matching the rest of the test tooling (the lit configs and | ||
| perfRunner already require it); callers treat a failure as "use one GPU". | ||
| """ | ||
| from hip import hip | ||
| archs = [] | ||
| device_count = _hip_check(hip.hipGetDeviceCount()) | ||
| for device in range(device_count): | ||
| props = hip.hipDeviceProp_t() | ||
| _hip_check(hip.hipGetDeviceProperties(props, device)) | ||
| archs.append(props.gcnArchName.decode('utf-8')) | ||
| return archs | ||
|
|
||
|
|
||
| def select_gpu_ids( | ||
| requested: Optional[List[int]] = None) -> Tuple[List[Optional[int]], Optional[str], str]: | ||
| """Decide which GPUs to spread work across. | ||
|
|
||
| Returns ``(gpu_ids, arch, message)``. ``gpu_ids == [None]`` means run on a | ||
| single, unpinned GPU (and ``arch`` is ``None``). Otherwise ``gpu_ids`` lists | ||
| the physical devices to isolate work to, all sharing architecture ``arch``. | ||
| """ | ||
| # Respect a caller that already pinned visibility (e.g. lit shards, manual | ||
| # runs); don't second-guess their device selection. | ||
| if os.environ.get('ROCR_VISIBLE_DEVICES') or os.environ.get('HIP_VISIBLE_DEVICES'): | ||
| return [None], None, "respecting pre-set *_VISIBLE_DEVICES; using a single GPU" | ||
|
|
||
| try: | ||
| archs = get_per_device_archs() | ||
| except Exception as e: # noqa: BLE001 - any GPU/runtime issue means fall back | ||
| return [None], None, f"GPU enumeration failed ({e}); using the default GPU" | ||
|
|
||
| count = len(archs) | ||
| if count <= 1: | ||
| return [None], None, "single GPU detected; running on one GPU" | ||
|
|
||
| if requested: | ||
| # Never shard the same device twice. | ||
| unique_requested = list(dict.fromkeys(requested)) | ||
| invalid = [i for i in unique_requested if not 0 <= i < count] | ||
| if invalid: | ||
| return [None], None, (f"requested GPU ids {invalid} are out of range " | ||
| f"(node has {count} GPU(s)); using the default GPU") | ||
| selected_archs = {archs[i] for i in unique_requested} | ||
| if len(selected_archs) != 1: | ||
| return [None], None, (f"requested GPUs {unique_requested} are not a single arch " | ||
| f"({sorted(selected_archs)}); using the default GPU") | ||
| arch = next(iter(selected_archs)) | ||
| return unique_requested, arch, f"using requested GPUs {unique_requested} ({arch})" | ||
|
|
||
| if len(set(archs)) > 1: | ||
| return [None], None, (f"mixed GPU architectures ({sorted(set(archs))}); " | ||
| "using a single GPU") | ||
|
|
||
| return list(range(count)), archs[0], f"distributing across {count} GPUs ({archs[0]})" | ||
|
|
||
|
|
||
| def make_isolated_gpu_env(gpu_id: Optional[int]) -> Optional[dict]: | ||
| """Build an environment dict that isolates a child process to one GPU. | ||
|
|
||
| Returns ``None`` when ``gpu_id`` is ``None`` so callers can pass it straight | ||
| through to ``subprocess``/``asyncio`` APIs to mean "inherit the environment". | ||
| ROCR_VISIBLE_DEVICES isolates at the HSA/ROCr level (below HIP); we clear | ||
| HIP_VISIBLE_DEVICES to avoid the two layers disagreeing. | ||
| """ | ||
| if gpu_id is None: | ||
| return None | ||
| env = os.environ.copy() | ||
| env["ROCR_VISIBLE_DEVICES"] = str(gpu_id) | ||
| env.pop("HIP_VISIBLE_DEVICES", None) | ||
| return env |
Uh oh!
There was an error while loading. Please reload this page.