Skip to content

Commit c69098a

Browse files
committed
[V1][Metrics][Plugin] Add plugin support for custom StatLoggerBase implementations
Signed-off-by: tovam <[email protected]>
1 parent de92d91 commit c69098a

File tree

8 files changed

+147
-30
lines changed

8 files changed

+147
-30
lines changed

.buildkite/test-pipeline.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,11 @@ steps:
10041004
- pytest -v -s plugins_tests/test_io_processor_plugins.py
10051005
- pip uninstall prithvi_io_processor_plugin -y
10061006
# end io_processor plugins test
1007+
# begin stat_logger plugins test
1008+
- pip install -e ./plugins/vllm_add_dummy_stat_logger
1009+
- pytest -v -s plugins_tests/test_stats_logger_plugins.py
1010+
- pip uninstall dummy_stat_logger -y
1011+
# end stat_logger plugins test
10071012
# other tests continue here:
10081013
- pytest -v -s plugins_tests/test_scheduler_plugins.py
10091014
- pip install -e ./plugins/vllm_add_dummy_model

docs/design/plugin_system.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Every plugin has three parts:
4141

4242
1. **Plugin group**: The name of the entry point group. vLLM uses the entry point group `vllm.general_plugins` to register general plugins. This is the key of `entry_points` in the `setup.py` file. Always use `vllm.general_plugins` for vLLM's general plugins.
4343
2. **Plugin name**: The name of the plugin. This is the value in the dictionary of the `entry_points` dictionary. In the example above, the plugin name is `register_dummy_model`. Plugins can be filtered by their names using the `VLLM_PLUGINS` environment variable. To load only a specific plugin, set `VLLM_PLUGINS` to the plugin name.
44-
3. **Plugin value**: The fully qualified name of the function to register in the plugin system. In the example above, the plugin value is `vllm_add_dummy_model:register`, which refers to a function named `register` in the `vllm_add_dummy_model` module.
44+
3. **Plugin value**: The fully qualified name of the function or module to register in the plugin system. In the example above, the plugin value is `vllm_add_dummy_model:register`, which refers to a function named `register` in the `vllm_add_dummy_model` module.
4545

4646
## Types of supported plugins
4747

@@ -51,6 +51,8 @@ Every plugin has three parts:
5151

5252
- **IO Processor plugins** (with group name `vllm.io_processor_plugins`): The primary use case for these plugins is to register custom pre/post processing of the model prompt and model output for pooling models. The plugin function returns the IOProcessor's class fully qualified name.
5353

54+
- **Stat logger plugins** (with group name `vllm.stat_logger_plugins`): The primary use case for these plugins is to register custom, out-of-the-tree loggers into vLLM. The entry point should be a class that subclasses StatLoggerBase.
55+
5456
## Guidelines for Writing Plugins
5557

5658
- **Being re-entrant**: The function specified in the entry point should be re-entrant, meaning it can be called multiple times without causing issues. This is necessary because the function might be called multiple times in some processes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
4+
from vllm.v1.metrics.loggers import StatLoggerBase
5+
6+
7+
class DummyStatLogger(StatLoggerBase):
8+
"""
9+
A dummy stat logger for testing purposes.
10+
Implements the minimal interface expected by StatLoggerManager.
11+
"""
12+
13+
def __init__(self, vllm_config, engine_idx=0):
14+
self.vllm_config = vllm_config
15+
self.engine_idx = engine_idx
16+
self.recorded = []
17+
self.logged = False
18+
self.engine_initialized = False
19+
20+
def record(self, scheduler_stats, iteration_stats, engine_idx):
21+
self.recorded.append((scheduler_stats, iteration_stats, engine_idx))
22+
23+
def log(self):
24+
self.logged = True
25+
26+
def log_engine_initialized(self):
27+
self.engine_initialized = True
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
4+
from setuptools import setup
5+
6+
setup(
7+
name="dummy_stat_logger",
8+
version="0.1",
9+
packages=["dummy_stat_logger"],
10+
entry_points={
11+
"vllm.stat_logger_plugins": [
12+
"dummy_stat_logger = dummy_stat_logger.dummy_stat_logger:DummyStatLogger" # noqa
13+
]
14+
},
15+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
4+
import pytest
5+
from dummy_stat_logger.dummy_stat_logger import DummyStatLogger
6+
7+
from vllm.config import VllmConfig
8+
from vllm.engine.arg_utils import AsyncEngineArgs
9+
from vllm.v1.engine.async_llm import AsyncLLM
10+
from vllm.v1.metrics.loggers import load_stat_logger_plugin_factories
11+
12+
13+
def test_stat_logger_plugin_is_discovered(monkeypatch: pytest.MonkeyPatch):
14+
with monkeypatch.context() as m:
15+
m.setenv("VLLM_PLUGINS", "dummy_stat_logger")
16+
17+
factories = load_stat_logger_plugin_factories()
18+
assert len(factories) == 1, f"Expected 1 factory, got {len(factories)}"
19+
assert factories[0] is DummyStatLogger, (
20+
f"Expected DummyStatLogger class, got {factories[0]}"
21+
)
22+
23+
# instantiate and confirm the right type
24+
vllm_config = VllmConfig()
25+
instance = factories[0](vllm_config)
26+
assert isinstance(instance, DummyStatLogger)
27+
28+
29+
def test_no_plugins_loaded_if_env_empty(monkeypatch: pytest.MonkeyPatch):
30+
with monkeypatch.context() as m:
31+
m.setenv("VLLM_PLUGINS", "")
32+
33+
factories = load_stat_logger_plugin_factories()
34+
assert factories == []
35+
36+
37+
@pytest.mark.asyncio
38+
async def test_stat_logger_plugin_integration_with_engine(
39+
monkeypatch: pytest.MonkeyPatch,
40+
):
41+
with monkeypatch.context() as m:
42+
m.setenv("VLLM_PLUGINS", "dummy_stat_logger")
43+
44+
engine_args = AsyncEngineArgs(
45+
model="facebook/opt-125m",
46+
enforce_eager=True, # reduce test time
47+
disable_log_stats=True, # disable default loggers
48+
)
49+
50+
engine = AsyncLLM.from_engine_args(engine_args=engine_args)
51+
52+
assert len(engine.logger_manager.stat_loggers) == 2
53+
assert len(engine.logger_manager.stat_loggers[0].per_engine_stat_loggers) == 1
54+
assert isinstance(
55+
engine.logger_manager.stat_loggers[0].per_engine_stat_loggers[0],
56+
DummyStatLogger,
57+
)
58+
59+
engine.shutdown()

tests/v1/metrics/test_engine_logger_apis.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,13 @@
44

55
import pytest
66

7+
from tests.plugins.vllm_add_dummy_stat_logger.dummy_stat_logger.dummy_stat_logger import ( # noqa E501
8+
DummyStatLogger,
9+
)
710
from vllm.v1.engine.async_llm import AsyncEngineArgs, AsyncLLM
811
from vllm.v1.metrics.ray_wrappers import RayPrometheusStatLogger
912

1013

11-
class DummyStatLogger:
12-
"""
13-
A dummy stat logger for testing purposes.
14-
Implements the minimal interface expected by StatLoggerManager.
15-
"""
16-
17-
def __init__(self, vllm_config, engine_idx):
18-
self.vllm_config = vllm_config
19-
self.engine_idx = engine_idx
20-
self.recorded = []
21-
self.logged = False
22-
self.engine_initialized = False
23-
24-
def record(self, scheduler_stats, iteration_stats, engine_idx):
25-
self.recorded.append((scheduler_stats, iteration_stats, engine_idx))
26-
27-
def log(self):
28-
self.logged = True
29-
30-
def log_engine_initialized(self):
31-
self.engine_initialized = True
32-
33-
3414
@pytest.fixture
3515
def log_stats_enabled_engine_args():
3616
"""

vllm/v1/engine/async_llm.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@
3939
from vllm.v1.engine.parallel_sampling import ParentRequest
4040
from vllm.v1.engine.processor import Processor
4141
from vllm.v1.executor.abstract import Executor
42-
from vllm.v1.metrics.loggers import StatLoggerFactory, StatLoggerManager
42+
from vllm.v1.metrics.loggers import (
43+
StatLoggerFactory,
44+
StatLoggerManager,
45+
load_stat_logger_plugin_factories,
46+
)
4347
from vllm.v1.metrics.prometheus import shutdown_prometheus
4448
from vllm.v1.metrics.stats import IterationStats
4549

@@ -99,11 +103,16 @@ def __init__(
99103
self.observability_config = vllm_config.observability_config
100104
self.log_requests = log_requests
101105

102-
self.log_stats = log_stats or (stat_loggers is not None)
103-
if not log_stats and stat_loggers is not None:
106+
custom_stat_loggers = list(stat_loggers or [])
107+
custom_stat_loggers.extend(load_stat_logger_plugin_factories())
108+
109+
has_custom_loggers = bool(custom_stat_loggers)
110+
self.log_stats = log_stats or has_custom_loggers
111+
if not log_stats and has_custom_loggers:
104112
logger.info(
105-
"AsyncLLM created with log_stats=False and non-empty custom "
106-
"logger list; enabling logging without default stat loggers"
113+
"AsyncLLM created with log_stats=False, "
114+
"but custom stat loggers were found; "
115+
"enabling logging without default stat loggers."
107116
)
108117

109118
if self.model_config.skip_tokenizer_init:
@@ -143,7 +152,7 @@ def __init__(
143152
self.logger_manager = StatLoggerManager(
144153
vllm_config=vllm_config,
145154
engine_idxs=self.engine_core.engine_ranks_managed,
146-
custom_stat_loggers=stat_loggers,
155+
custom_stat_loggers=custom_stat_loggers,
147156
enable_default_loggers=log_stats,
148157
client_count=client_count,
149158
aggregate_engine_logging=aggregate_engine_logging,

vllm/v1/metrics/loggers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from vllm.config import SupportsMetricsInfo, VllmConfig
1313
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorLogging
1414
from vllm.logger import init_logger
15+
from vllm.plugins import load_plugins_by_group
1516
from vllm.v1.engine import FinishReason
1617
from vllm.v1.metrics.prometheus import unregister_vllm_metrics
1718
from vllm.v1.metrics.stats import (
@@ -56,6 +57,25 @@ def log(self): # noqa
5657
pass
5758

5859

60+
def load_stat_logger_plugin_factories() -> list[StatLoggerFactory]:
61+
factories: list[StatLoggerFactory] = []
62+
63+
for name, plugin_class in load_plugins_by_group("vllm.stat_logger_plugins").items():
64+
if not isinstance(plugin_class, type) or not issubclass(
65+
plugin_class, StatLoggerBase
66+
):
67+
logger.warning(
68+
"Stat logger plugin %s is not a valid subclass "
69+
"of StatLoggerBase. Skipping.",
70+
name,
71+
)
72+
continue
73+
74+
factories.append(plugin_class)
75+
76+
return factories
77+
78+
5979
class AggregateStatLoggerBase(StatLoggerBase):
6080
"""Abstract base class for loggers that
6181
aggregate across multiple DP engines."""

0 commit comments

Comments
 (0)