blob: 19a3f7e5579521f8b780dda85c8d25ad4f36c33a [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Helpers for tests that use exclusive machine-local resources.
Parallel test runners (for example ``pytest -n auto``) spread tests across
several worker processes. Tests that reach for a shared, machine-local
resource such as a single GPU must serialize with each other so that
concurrent access does not exhaust device memory or corrupt device state.
:func:`run_with_gpu_lock` provides that serialization through an advisory
file lock shared by every cooperating worker on the machine.
"""
from __future__ import annotations
import getpass
import os
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any, TypeVar
from ..utils import FileLock
_LOCK_DIR_ENV_VAR = "TVM_FFI_TEST_LOCK_DIR"
_LOCK_DIR_PREFIX = "tvm-ffi-test-locks"
_LOCK_FILENAME = "gpu.lock"
_R = TypeVar("_R")
# Resolved GPU lock path, cached on the first ``run_with_gpu_lock`` call.
#
# Init is lazy (not at import) because importing does not imply a GPU test will
# run, and resolving the path creates a directory. Concurrent first-callers may
# race to set this global, but the race is benign: every worker derives the same
# path.
_GPU_LOCK_PATH: Path | None = None
def _ensure_gpu_lock_path() -> Path:
"""Return the path to the machine-local GPU lock file, creating its directory.
Returns
-------
path
The full path to the ``gpu.lock`` file. The parent directory is created
if it does not yet exist.
Notes
-----
The lock directory defaults to a per-user directory under the system
temporary directory, ``<tempdir>/tvm-ffi-test-locks-<user>``. Scoping the
default to the current user avoids ownership and permission conflicts when
several users share one host. It can be redirected with the
``TVM_FFI_TEST_LOCK_DIR`` environment variable when all cooperating
processes need an explicitly shared machine-local path.
"""
lock_dir_override = os.environ.get(_LOCK_DIR_ENV_VAR)
if lock_dir_override:
lock_dir = Path(lock_dir_override).expanduser()
else:
# Tag the default directory with the current user, falling back to the
# numeric uid then ``unknown`` when a login name cannot be resolved.
try:
user_tag = getpass.getuser()
except Exception:
uid = getattr(os, "getuid", None)
user_tag = str(uid()) if uid is not None else "unknown"
lock_dir = Path(tempfile.gettempdir()) / f"{_LOCK_DIR_PREFIX}-{user_tag}"
lock_dir.mkdir(parents=True, exist_ok=True)
return lock_dir / _LOCK_FILENAME
def run_with_gpu_lock(func: Callable[..., _R], /, *args: Any, **kwargs: Any) -> _R:
"""Run a callable while holding the machine-local GPU lock.
The lock serializes GPU access across parallel test workers so that
concurrent device use does not break GPU-related tests. Pass a callable
that contains the complete live-device lifetime (device creation,
allocation, execution, synchronization, and result checks); keep work that
does not touch the device, such as source compilation, outside the callable
so it can still run in parallel.
Parameters
----------
func
Callable containing the complete live local-GPU lifetime.
args
Positional arguments forwarded to ``func``.
kwargs
Keyword arguments forwarded to ``func``.
Returns
-------
result
The return value of ``func``.
"""
# Resolve and cache the lock path on the first call (see ``_GPU_LOCK_PATH``
# above for why this is lazy and why the concurrent-init race is benign).
global _GPU_LOCK_PATH # noqa: PLW0603 -- intentional first-call memoization cache
lock_path = _GPU_LOCK_PATH
if lock_path is None:
lock_path = _ensure_gpu_lock_path()
_GPU_LOCK_PATH = lock_path
with FileLock(str(lock_path)):
return func(*args, **kwargs)