Files
vllm/tests/utils_/test_gpu_sync_debug.py

161 lines
5.7 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import threading
import warnings
import pytest
import torch
import vllm.utils.gpu_sync_debug as gsd
from vllm.utils.gpu_sync_debug import (
SYNC_ERROR_MESSAGE,
gpu_sync_allowed,
with_gpu_sync_check,
)
from ..utils import create_new_process_for_each_test
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
def _no_sync():
# Pure on-GPU compute, no implicit CPU sync...
x = torch.ones(4, device="cuda") + 1
# ...plus a sync that we explicitly allow.
with gpu_sync_allowed():
return x.cpu()
def _causes_sync():
x = torch.ones(4, device="cuda")
# An allowed sync (suppressed)...
with gpu_sync_allowed():
x.cpu()
# ...then an un-allowed sync that should trip the check.
return x.cpu()
@pytest.mark.parametrize("mode", ["warn", "error"])
@create_new_process_for_each_test()
def test_with_env_set(monkeypatch, mode):
# Env set + gate flipped on: the unguarded sync is detected.
# `_SYNC_CHECK_MODE` is read from the env once at import, so patch the
# module attribute rather than the environment.
monkeypatch.setattr(gsd, "_SYNC_CHECK_MODE", mode)
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
# Guarded syncs always pass.
with_gpu_sync_check(_no_sync)()
if mode == "error":
# "error" mode turns the stray sync into a RuntimeError.
with pytest.raises(RuntimeError, match=SYNC_ERROR_MESSAGE):
with_gpu_sync_check(_causes_sync)()
else:
# "warn" mode only warns, so the call still succeeds.
with_gpu_sync_check(_causes_sync)()
@create_new_process_for_each_test()
def test_other_threads_are_not_policed(monkeypatch):
"""A background thread that syncs deliberately must not be broken by the
check being armed on the thread running the decorated function."""
monkeypatch.setattr(gsd, "_SYNC_CHECK_MODE", "error")
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
def sync_on_worker():
failure: list[BaseException] = []
def worker():
try:
torch.ones(4, device="cuda").cpu()
except BaseException as exc: # pragma: no cover - failure path
failure.append(exc)
thread = threading.Thread(target=worker)
thread.start()
thread.join()
assert not failure, f"background thread raised: {failure[0]!r}"
with_gpu_sync_check(sync_on_worker)()
@create_new_process_for_each_test()
def test_allow_on_other_thread_does_not_disarm(monkeypatch):
"""`gpu_sync_allowed()` on one thread must not suppress the check on
another. It is scoped by ContextVar rather than torch's process-global
sync debug mode, which a previous implementation mutated."""
monkeypatch.setattr(gsd, "_SYNC_CHECK_MODE", "error")
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
def main_syncs_while_worker_allows():
stop = threading.Event()
def worker():
with gpu_sync_allowed():
while not stop.is_set():
torch.ones(4, device="cuda").cpu()
thread = threading.Thread(target=worker)
thread.start()
try:
# Must still be reported despite the worker's open allow region.
torch.ones(4, device="cuda").cpu()
finally:
stop.set()
thread.join()
with pytest.raises(RuntimeError, match=SYNC_ERROR_MESSAGE):
with_gpu_sync_check(main_syncs_while_worker_allows)()
@create_new_process_for_each_test()
def test_suppressing_works_while_compiling(monkeypatch):
"""`_suppressing` wraps torch compile entry points, which run with
`torch.compiler.is_compiling()` true. `gpu_sync_allowed()` deliberately
no-ops in that state, so `_suppressing` must not route through it."""
monkeypatch.setattr(gsd, "_SYNC_CHECK_MODE", "error")
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
# Emulate being inside a torch compile, as inductor passes are.
monkeypatch.setattr(torch.compiler, "is_compiling", lambda: True)
suppressed = gsd._suppressing(lambda: torch.ones(4, device="cuda").cpu())
with_gpu_sync_check(suppressed)()
@create_new_process_for_each_test()
def test_sync_debug_mode_restored_after_checked_call(monkeypatch):
"""The mode is armed only for the duration of a checked call. Leaving it
on process-wide made every sync outside a checked region emit a
`UserWarning` whenever our handler was not the installed one."""
monkeypatch.setattr(gsd, "_SYNC_CHECK_MODE", "error")
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
before = torch.cuda.get_sync_debug_mode()
def nested():
# `execute_model` -> `sample_tokens` both carry the decorator.
assert torch.cuda.get_sync_debug_mode() != 0, "armed inside"
with_gpu_sync_check(lambda: None)()
assert torch.cuda.get_sync_debug_mode() != 0, "still armed after inner"
with_gpu_sync_check(nested)()
assert torch.cuda.get_sync_debug_mode() == before
# With the mode back to its original value, torch emits nothing for a
# sync outside a checked region.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
torch.ones(4, device="cuda").cpu()
assert not [r for r in caught if gsd._TORCH_SYNC_WARNING in str(r.message)]
@create_new_process_for_each_test()
def test_without_env_set(monkeypatch):
# Env unset: the decorator is a pass-through, no sync is detected.
monkeypatch.setattr(gsd, "_SYNC_CHECK_MODE", None)
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
with_gpu_sync_check(_no_sync)()
with_gpu_sync_check(_causes_sync)()