mirror of
https://github.com/vllm-project/vllm.git
synced 2026-08-10 07:48:09 +00:00
161 lines
5.7 KiB
Python
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)()
|