[CI] Fix gpu deps import in cpu test (#21950)
This commit is contained in:
@@ -172,7 +172,7 @@ if __name__ == "__main__":
|
|||||||
unittest.main()
|
unittest.main()
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `unittest.mock.patch` / `MagicMock` to mock dependencies and isolate the logic under test. If the module fails to import on CPU CI (e.g., imports `torch` or CUDA ops at module level), use `sys.modules` stubs to make the import succeed. See existing tests in `test/registered/unit/` for examples.
|
Use `unittest.mock.patch` / `MagicMock` to mock dependencies and isolate the logic under test. If the module transitively imports GPU-only packages (e.g. `sgl_kernel`), they can be stubbed so the test runs on CPU CI. See `test/registered/unit/README.md` for details and examples.
|
||||||
|
|
||||||
**Quality bar** — test real logic (validation boundaries, state transitions, error paths, branching, etc.). Skip tests that just verify Python itself works (e.g., "does calling an abstract method raise `NotImplementedError`?", "does a dataclass store the field I assigned?"). Consolidate repetitive patterns into parameterized tests. No production code changes in test PRs.
|
**Quality bar** — test real logic (validation boundaries, state transitions, error paths, branching, etc.). Skip tests that just verify Python itself works (e.g., "does calling an abstract method raise `NotImplementedError`?", "does a dataclass store the field I assigned?"). Consolidate repetitive patterns into parameterized tests. No production code changes in test PRs.
|
||||||
|
|
||||||
|
|||||||
@@ -2056,6 +2056,44 @@ def _distributed_worker(rank, world_size, backend, port, func, result_queue, kwa
|
|||||||
dist.destroy_process_group()
|
dist.destroy_process_group()
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_stub_sgl_kernel():
|
||||||
|
"""Stub sgl_kernel if it cannot be imported (e.g. no GPU).
|
||||||
|
|
||||||
|
Must be called before any import that transitively depends on sgl_kernel.
|
||||||
|
On machines with a working sgl_kernel this is a no-op.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import sgl_kernel # noqa: F401
|
||||||
|
|
||||||
|
return
|
||||||
|
except (ImportError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
import importlib.abc
|
||||||
|
import importlib.machinery
|
||||||
|
|
||||||
|
class _SglKernelLoader(importlib.abc.Loader):
|
||||||
|
def create_module(self, spec):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def exec_module(self, module):
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
module.__getattr__ = lambda name: MagicMock()
|
||||||
|
|
||||||
|
class _SglKernelFinder(importlib.abc.MetaPathFinder):
|
||||||
|
def find_spec(self, fullname, path, target=None):
|
||||||
|
if fullname == "sgl_kernel" or fullname.startswith("sgl_kernel."):
|
||||||
|
return importlib.machinery.ModuleSpec(
|
||||||
|
fullname,
|
||||||
|
_SglKernelLoader(),
|
||||||
|
is_package=True,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
sys.meta_path.insert(0, _SglKernelFinder())
|
||||||
|
|
||||||
|
|
||||||
class CustomTestCase(unittest.TestCase):
|
class CustomTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ Tests can use CPU or GPU — the key criterion is **no server process**.
|
|||||||
diff-cover coverage.xml --compare-branch=origin/main --fail-under=60
|
diff-cover coverage.xml --compare-branch=origin/main --fail-under=60
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
## Examples
|
||||||
|
|
||||||
|
### Basic unit test
|
||||||
|
|
||||||
```python
|
```python
|
||||||
"""Unit tests for <module> — no server, no model loading."""
|
"""Unit tests for <module> — no server, no model loading."""
|
||||||
@@ -57,6 +59,30 @@ if __name__ == "__main__":
|
|||||||
unittest.main()
|
unittest.main()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Stubbing GPU-only imports for CPU tests
|
||||||
|
|
||||||
|
Some modules (e.g. `scheduler.py`, `io_struct.py`) transitively import packages like
|
||||||
|
`sgl_kernel` that require a GPU to initialize. To run pure-mock tests against these
|
||||||
|
modules on CPU-only CI, stub the problematic package **before** importing it.
|
||||||
|
|
||||||
|
`maybe_stub_sgl_kernel()` in `test_utils.py` does this for `sgl_kernel`: it's a no-op
|
||||||
|
on GPU machines, and on CPU it installs a `sys.meta_path` finder that auto-creates empty
|
||||||
|
stub modules for all `sgl_kernel.*` submodules.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
from sglang.test.test_utils import maybe_stub_sgl_kernel
|
||||||
|
|
||||||
|
maybe_stub_sgl_kernel() # must precede any import that pulls in sgl_kernel
|
||||||
|
|
||||||
|
from sglang.srt.managers.io_struct import FlushCacheReqInput
|
||||||
|
from sglang.srt.managers.scheduler import Scheduler
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=2, suite="stage-a-test-cpu")
|
||||||
|
```
|
||||||
|
|
||||||
|
The same pattern can be applied to other GPU-only packages: try importing the real package, and if it fails, register a `sys.meta_path` finder that stubs it. See `maybe_stub_sgl_kernel()` in `python/sglang/test/test_utils.py` for the implementation.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- **No** `popen_launch_server()` or `Engine(...)`.
|
- **No** `popen_launch_server()` or `Engine(...)`.
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
from sglang.test.test_utils import maybe_stub_sgl_kernel
|
||||||
|
|
||||||
|
maybe_stub_sgl_kernel()
|
||||||
|
|
||||||
from sglang.srt.managers.io_struct import FlushCacheReqInput
|
from sglang.srt.managers.io_struct import FlushCacheReqInput
|
||||||
from sglang.srt.managers.scheduler import Scheduler
|
from sglang.srt.managers.scheduler import Scheduler
|
||||||
from sglang.test.ci.ci_register import register_cpu_ci
|
|
||||||
|
|
||||||
register_cpu_ci(est_time=2, suite="stage-a-cpu-only")
|
register_cpu_ci(est_time=2, suite="stage-a-test-cpu")
|
||||||
|
|
||||||
|
|
||||||
class TestSchedulerFlushCache(unittest.TestCase):
|
class TestSchedulerFlushCache(unittest.TestCase):
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import unittest
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
from sglang.test.test_utils import maybe_stub_sgl_kernel
|
||||||
|
|
||||||
|
maybe_stub_sgl_kernel()
|
||||||
|
|
||||||
from sglang.srt.managers.io_struct import PauseGenerationReqInput
|
from sglang.srt.managers.io_struct import PauseGenerationReqInput
|
||||||
from sglang.srt.managers.scheduler import Scheduler
|
from sglang.srt.managers.scheduler import Scheduler
|
||||||
from sglang.test.ci.ci_register import register_cpu_ci
|
|
||||||
|
|
||||||
register_cpu_ci(est_time=2, suite="stage-a-cpu-only")
|
register_cpu_ci(est_time=2, suite="stage-a-test-cpu")
|
||||||
|
|
||||||
|
|
||||||
class TestSchedulerPauseGeneration(unittest.TestCase):
|
class TestSchedulerPauseGeneration(unittest.TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user