186 lines
6.4 KiB
Python
186 lines
6.4 KiB
Python
"""Tests for CUDAGraphWrapper.__getattr__ performance optimization.
|
|
|
|
This module tests that the patched CUDAGraphWrapper avoids expensive __repr__
|
|
calls when hasattr() is used for non-existent attributes. The original vLLM
|
|
implementation includes {self.runnable} in the AttributeError message, which
|
|
triggers model tree traversal and can take ~6ms on large models.
|
|
"""
|
|
|
|
import time
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
from vllm_omni.worker.gpu_model_runner import CUDAGraphWrapper
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
|
|
class SlowReprModel(nn.Module):
|
|
"""A mock model with artificially slow __repr__ to detect unwanted calls."""
|
|
|
|
def __init__(self, repr_delay_ms: float = 10.0):
|
|
super().__init__()
|
|
self.linear = nn.Linear(16, 16)
|
|
self.repr_delay_ms = repr_delay_ms
|
|
self.repr_call_count = 0
|
|
|
|
def forward(self, x):
|
|
return self.linear(x)
|
|
|
|
def __repr__(self):
|
|
self.repr_call_count += 1
|
|
# Simulate expensive repr by sleeping
|
|
time.sleep(self.repr_delay_ms / 1000.0)
|
|
return f"SlowReprModel(delay={self.repr_delay_ms}ms)"
|
|
|
|
|
|
class MockCUDAGraphWrapper:
|
|
"""A minimal mock that mimics CUDAGraphWrapper structure for CPU testing."""
|
|
|
|
def __init__(self, runnable):
|
|
# Store in __dict__ directly to avoid triggering __getattr__
|
|
object.__setattr__(self, "runnable", runnable)
|
|
|
|
def __getattr__(self, key: str):
|
|
# This is the optimized implementation we're testing
|
|
runnable = object.__getattribute__(self, "runnable")
|
|
if hasattr(runnable, key):
|
|
return getattr(runnable, key)
|
|
# Key optimization: DO NOT include {self.runnable} in error message
|
|
# as it triggers expensive __repr__ on large models
|
|
raise AttributeError(f"Attribute {key} not exists in the runnable of cudagraph wrapper")
|
|
|
|
|
|
def test_hasattr_nonexistent_does_not_trigger_repr():
|
|
"""Verify that hasattr for non-existent attributes doesn't call __repr__."""
|
|
model = SlowReprModel(repr_delay_ms=100.0) # Very slow repr
|
|
wrapper = MockCUDAGraphWrapper(model)
|
|
|
|
# Reset counter
|
|
model.repr_call_count = 0
|
|
|
|
# Call hasattr for non-existent attribute multiple times
|
|
for _ in range(10):
|
|
result = hasattr(wrapper, "nonexistent_attribute_xyz")
|
|
assert result is False
|
|
|
|
# __repr__ should never have been called
|
|
assert model.repr_call_count == 0, (
|
|
f"__repr__ was called {model.repr_call_count} times when checking "
|
|
"for non-existent attributes. This indicates the AttributeError "
|
|
"message contains {self.runnable} which triggers expensive repr."
|
|
)
|
|
|
|
|
|
def test_hasattr_nonexistent_is_fast():
|
|
"""Verify that hasattr for non-existent attributes is fast (<1ms per call)."""
|
|
model = SlowReprModel(repr_delay_ms=100.0)
|
|
wrapper = MockCUDAGraphWrapper(model)
|
|
|
|
num_iterations = 100
|
|
start = time.perf_counter()
|
|
for _ in range(num_iterations):
|
|
hasattr(wrapper, "nonexistent_attribute_xyz")
|
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
|
|
avg_ms = elapsed_ms / num_iterations
|
|
# If __repr__ were being called, each would take ~100ms
|
|
# We expect <1ms per call with the fix
|
|
assert avg_ms < 1.0, (
|
|
f"hasattr for non-existent attribute took {avg_ms:.2f}ms on average. "
|
|
"Expected <1ms. This suggests __repr__ is being triggered."
|
|
)
|
|
|
|
|
|
def test_hasattr_existing_attribute_works():
|
|
"""Verify that hasattr for existing attributes returns True and works correctly."""
|
|
model = SlowReprModel()
|
|
wrapper = MockCUDAGraphWrapper(model)
|
|
|
|
# 'forward' exists on nn.Module
|
|
assert hasattr(wrapper, "forward") is True
|
|
|
|
# 'linear' exists on our model
|
|
assert hasattr(wrapper, "linear") is True
|
|
|
|
# Can actually access the attribute
|
|
linear = wrapper.linear
|
|
assert isinstance(linear, nn.Linear)
|
|
|
|
|
|
def test_getattr_existing_attribute_returns_value():
|
|
"""Verify that getattr for existing attributes returns the correct value."""
|
|
model = SlowReprModel()
|
|
wrapper = MockCUDAGraphWrapper(model)
|
|
|
|
# Access forward method
|
|
forward_method = wrapper.forward
|
|
assert callable(forward_method)
|
|
|
|
# Access linear layer
|
|
linear = wrapper.linear
|
|
assert isinstance(linear, nn.Linear)
|
|
assert linear.in_features == 16
|
|
assert linear.out_features == 16
|
|
|
|
|
|
def test_getattr_nonexistent_raises_attribute_error():
|
|
"""Verify that getattr for non-existent attributes raises AttributeError."""
|
|
model = SlowReprModel()
|
|
wrapper = MockCUDAGraphWrapper(model)
|
|
|
|
with pytest.raises(AttributeError) as exc_info:
|
|
_ = wrapper.nonexistent_attribute
|
|
|
|
# Verify error message format (should NOT contain model repr)
|
|
error_msg = str(exc_info.value)
|
|
assert "nonexistent_attribute" in error_msg
|
|
assert "cudagraph wrapper" in error_msg
|
|
# Should NOT contain the slow repr output
|
|
assert "SlowReprModel(delay=" not in error_msg
|
|
|
|
|
|
def test_attribute_error_message_does_not_contain_runnable_repr():
|
|
"""Explicitly verify the error message doesn't trigger runnable repr."""
|
|
model = SlowReprModel(repr_delay_ms=100.0)
|
|
wrapper = MockCUDAGraphWrapper(model)
|
|
model.repr_call_count = 0
|
|
|
|
try:
|
|
_ = wrapper.nonexistent_attr
|
|
except AttributeError:
|
|
pass
|
|
|
|
# __repr__ should not have been called during error construction
|
|
assert model.repr_call_count == 0, (
|
|
"AttributeError message construction triggered __repr__. The error message should not include {self.runnable}."
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
|
def test_real_cudagraph_wrapper_hasattr_performance():
|
|
"""Test the actual CUDAGraphWrapper from vllm_omni (requires CUDA)."""
|
|
from vllm.config import CUDAGraphMode
|
|
|
|
model = SlowReprModel(repr_delay_ms=50.0).cuda()
|
|
model.repr_call_count = 0
|
|
|
|
# Create actual CUDAGraphWrapper
|
|
try:
|
|
wrapper = CUDAGraphWrapper(model, runtime_mode=CUDAGraphMode.NONE)
|
|
except Exception:
|
|
pytest.skip("Could not create CUDAGraphWrapper")
|
|
|
|
# Test hasattr performance
|
|
num_iterations = 50
|
|
start = time.perf_counter()
|
|
for _ in range(num_iterations):
|
|
hasattr(wrapper, "nonexistent_xyz")
|
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
|
|
avg_ms = elapsed_ms / num_iterations
|
|
assert avg_ms < 1.0, f"Real CUDAGraphWrapper hasattr took {avg_ms:.2f}ms avg. Expected <1ms with the optimization."
|
|
assert model.repr_call_count == 0, f"__repr__ called {model.repr_call_count} times"
|