| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """ECConnectorBase Class for Distributed Encoder Cache & |
| 4 | P2P Encoder cache communication in V1 |
| 5 | |
| 6 | The class provides the following primitives: |
| 7 | Scheduler-side: runs in the scheduler, binds metadata, which |
| 8 | is used by the worker-side to load/save Encoder cache. |
| 9 | check_caches_exist() - Check whether Encoder cache of requests exist |
| 10 | update_state_after_alloc() - update ECConnector state after |
| 11 | allocate. This will decide to load the cache or not |
| 12 | request_finished() - called when a request is finished, |
| 13 | free the cache with the requests |
| 14 | |
| 15 | Worker-side: runs in each worker, loads/saves Encoder Cache to/from |
| 16 | the Connector based on the metadata. |
| 17 | start_load_ec() - starts loading all ECs (maybe async) |
| 18 | wait_for_save() - blocks until all saves are done |
| 19 | |
| 20 | get_finished() - called with ids of finished requests, returns |
| 21 | ids of requests that have completed async sending/recving. |
| 22 | build_connector_worker_meta() - builds metadata to be sent |
| 23 | back to the scheduler-side connector |
| 24 | """ |
| 25 | |
| 26 | import enum |
| 27 | from abc import ABC, abstractmethod |
| 28 | from typing import TYPE_CHECKING, Any |
| 29 | |
| 30 | import torch |
| 31 | |
| 32 | from vllm.logger import init_logger |
| 33 | from vllm.v1.core.sched.output import SchedulerOutput |
| 34 | from vllm.v1.outputs import ECConnectorOutput |
| 35 | |
| 36 | if TYPE_CHECKING: |
| 37 | from vllm.config import VllmConfig |
| 38 | from vllm.distributed.ec_transfer.ec_connector.metrics import ( |
| 39 | ECConnectorPromMetrics, |
| 40 | ECConnectorStats, |
| 41 | PromMetric, |
| 42 | PromMetricT, |
| 43 | ) |
| 44 | from vllm.v1.request import Request |
| 45 | |
| 46 | logger = init_logger(__name__) |
| 47 | |
| 48 | |
| 49 | class ECConnectorRole(enum.Enum): |
| 50 | # Connector running in the scheduler process |
| 51 | SCHEDULER = 0 |
| 52 | |
| 53 | # Connector running in the worker process |
| 54 | WORKER = 1 |
| 55 | |
| 56 | |
| 57 | class ECConnectorMetadata(ABC): # noqa: B024 |
| 58 | """Abstract Metadata used to communicate between the |
| 59 | Scheduler ECConnector and Worker ECConnector. |
| 60 | """ |
| 61 | |
| 62 | pass |
| 63 | |
| 64 | |
| 65 | class ECConnectorWorkerMetadata(ABC): |
| 66 | """Abstract Metadata used to communicate back |
| 67 | Worker ECConnector -> Scheduler ECConnector. |
| 68 | |
| 69 | Each worker can output its own metadata. |
| 70 | For a single engine step, all metadata objects returned by workers |
| 71 | will be aggregated using the `aggregate` method below, before |
| 72 | being passed to the Scheduler ECConnector. |
| 73 | """ |
| 74 | |
| 75 | @abstractmethod |
| 76 | def aggregate( |
| 77 | self, other: "ECConnectorWorkerMetadata" |
| 78 | ) -> "ECConnectorWorkerMetadata": |
| 79 | """Aggregate metadata with another `ECConnectorWorkerMetadata` object.""" |
| 80 | pass |
| 81 | |
| 82 | |
| 83 | class ECConnectorBase(ABC): |
| 84 | def __init__(self, vllm_config: "VllmConfig", role: ECConnectorRole): |
| 85 | self._connector_metadata: ECConnectorMetadata | None = None |
| 86 | self._vllm_config = vllm_config |
| 87 | self._role = role |
| 88 | if vllm_config.ec_transfer_config is not None: |
| 89 | self._is_producer = vllm_config.ec_transfer_config.is_ec_producer |
| 90 | self._is_consumer = vllm_config.ec_transfer_config.is_ec_consumer |
| 91 | else: |
| 92 | raise ValueError("ec_transfer_config must be set for ECConnectorBase") |
| 93 | |
| 94 | @property |
| 95 | def role(self) -> ECConnectorRole: |
| 96 | return self._role |
| 97 | |
| 98 | @property |
| 99 | def is_producer(self) -> bool: |
| 100 | return self._is_producer |
| 101 | |
| 102 | @property |
| 103 | def is_consumer(self) -> bool: |
| 104 | return self._is_consumer |
| 105 | |
| 106 | def shutdown(self) -> None: |
| 107 | """Shutdown the connector. This is called when the process |
| 108 | is shutting down to ensure that all the async operations are |
| 109 | completed and the connector is cleaned up properly. |
| 110 | """ |
| 111 | return None |
| 112 | |
| 113 | # ============================== |
| 114 | # Worker-side methods |
| 115 | # ============================== |
| 116 | |
| 117 | def bind_connector_metadata(self, connector_metadata: ECConnectorMetadata) -> None: |
| 118 | """Set the connector metadata from the scheduler. |
| 119 | |
| 120 | This function should be called by the model runner every time |
| 121 | before the model execution. The metadata will be used for runtime |
| 122 | EC cache loading. |
| 123 | |
| 124 | Args: |
| 125 | connector_metadata (dict): the connector metadata. |
| 126 | |
| 127 | """ |
| 128 | self._connector_metadata = connector_metadata |
| 129 | |
| 130 | def clear_connector_metadata(self) -> None: |
| 131 | """Clear the connector metadata. |
| 132 | |
| 133 | This function should be called by the model runner every time |
| 134 | after the model execution. |
| 135 | """ |
| 136 | self._connector_metadata = None |
| 137 | |
| 138 | def _get_connector_metadata(self) -> ECConnectorMetadata: |
| 139 | """Get the connector metadata. |
| 140 | |
| 141 | This function should only be called inside the connector. |
| 142 | |
| 143 | Returns: |
| 144 | ConnectorMetadata: the connector metadata. |
| 145 | |
| 146 | """ |
| 147 | # Should only be called while set to valid metadata. |
| 148 | assert self._connector_metadata is not None |
| 149 | return self._connector_metadata |
| 150 | |
| 151 | def register_caches( |
| 152 | self, |
| 153 | ec_caches: dict[str, torch.Tensor], |
| 154 | ): |
| 155 | """Initialize with the EC caches. |
| 156 | |
| 157 | Args: |
| 158 | ec_caches: dictionary of encoder cache |
| 159 | |
| 160 | """ |
| 161 | # TODO: Implement this later for P2P feature |
| 162 | return |
| 163 | |
| 164 | @abstractmethod |
| 165 | def start_load_caches( |
| 166 | self, encoder_cache: dict[str, torch.Tensor], **kwargs |
| 167 | ) -> None: |
| 168 | """Start loading the cache from the connector into vLLM's encoder cache. |
| 169 | |
| 170 | This method loads the encoder cache based on metadata provided by the scheduler. |
| 171 | It is called before `_gather_mm_embeddings` for the EC Connector. For EC, |
| 172 | the `encoder_cache` and `mm_hash` are stored in `kwargs`. |
| 173 | |
| 174 | Args: |
| 175 | encoder_cache (dict[str, torch.Tensor]): A dictionary mapping multimodal |
| 176 | data hashes (`mm_hash`) to encoder cache tensors. |
| 177 | kwargs (dict): Additional keyword arguments for the connector. |
| 178 | |
| 179 | """ |
| 180 | pass |
| 181 | |
| 182 | @abstractmethod |
| 183 | def save_caches( |
| 184 | self, encoder_cache: dict[str, torch.Tensor], mm_hash: str, **kwargs |
| 185 | ) -> None: |
| 186 | """Save the encoder cache to the connector. |
| 187 | |
| 188 | This method saves the encoder cache from the worker's local storage |
| 189 | to shared storage or another external connector. |
| 190 | |
| 191 | Args: |
| 192 | encoder_cache (dict[str, torch.Tensor]): A dictionary mapping multimodal |
| 193 | data hashes (`mm_hash`) to encoder cache tensors. |
| 194 | mm_hash (str): The hash of the multimodal data whose cache is being saved. |
| 195 | kwargs (dict): Additional keyword arguments for the connector. |
| 196 | |
| 197 | """ |
| 198 | pass |
| 199 | |
| 200 | def get_finished( |
| 201 | self, finished_req_ids: set[str] |
| 202 | ) -> tuple[set[str] | None, set[str] | None]: |
| 203 | """Notifies worker-side connector ids of requests that have |
| 204 | finished generating tokens on the worker. |
| 205 | The scheduler process (via the Executors) will use this output |
| 206 | to track which workers are done. |
| 207 | |
| 208 | Returns: |
| 209 | ids of requests that have finished asynchronous transfer |
| 210 | (requests that previously returned True from request_finished()), |
| 211 | tuple of (sending/saving ids, recving/loading ids). |
| 212 | The finished saves/sends req ids must belong to a set provided in a |
| 213 | call to this method (this call or a prior one). |
| 214 | |
| 215 | """ |
| 216 | return None, None |
| 217 | |
| 218 | def build_connector_worker_meta(self) -> ECConnectorWorkerMetadata | None: |
| 219 | """Build the ECConnector worker metadata for this engine step. |
| 220 | |
| 221 | Returns: |
| 222 | ECConnectorWorkerMetadata: the worker metadata. |
| 223 | None if no worker metadata is available. |
| 224 | |
| 225 | """ |
| 226 | return None |
| 227 | |
| 228 | def get_ec_connector_stats(self) -> "ECConnectorStats | None": |
| 229 | """Get the EC connector stats collected during the last interval. |
| 230 | |
| 231 | Callable on either a worker-side or scheduler-side connector |
| 232 | instance: the worker-side instance reports stats gathered during |
| 233 | cache save/load, while the scheduler-side instance reports stats |
| 234 | gathered during scheduling decisions. |
| 235 | """ |
| 236 | return None |
| 237 | |
| 238 | # ============================== |
| 239 | # Scheduler-side methods |
| 240 | # ============================== |
| 241 | |
| 242 | @abstractmethod |
| 243 | def has_cache_item( |
| 244 | self, |
| 245 | identifier: str, |
| 246 | ) -> bool: |
| 247 | """Check if a single encoder cache exists. |
| 248 | |
| 249 | Args: |
| 250 | identifier (str): the identifier of the media. |
| 251 | |
| 252 | Returns: |
| 253 | A bool where value is True if cache exist for |
| 254 | the media |
| 255 | |
| 256 | """ |
| 257 | pass |
| 258 | |
| 259 | def ensure_cache_available( |
| 260 | self, request: "Request", num_computed_tokens: int |
| 261 | ) -> bool: |
| 262 | """Ensure encoder cache items are available for the given request. |
| 263 | May initiate asynchronous transfers for items not yet local. |
| 264 | |
| 265 | Args: |
| 266 | request: the request whose multimodal features to check. |
| 267 | num_computed_tokens: tokens already covered by cached KV blocks. |
| 268 | |
| 269 | Returns: |
| 270 | True if all items are ready or no transfer is needed. |
| 271 | False if any items are still in transit (request should be deferred). |
| 272 | |
| 273 | """ |
| 274 | return True |
| 275 | |
| 276 | def start_save_caches(self, **kwargs: Any) -> None: |
| 277 | """Prepare this step's outbound pushes before the model runs.""" |
| 278 | return |
| 279 | |
| 280 | def start_worker_services(self) -> None: |
| 281 | """Start Worker-side services once the model is resident.""" |
| 282 | return |
| 283 | |
| 284 | def take_unavailable_requests(self) -> set[str]: |
| 285 | """Request IDs whose encoder inputs the connector can no longer obtain. |
| 286 | |
| 287 | The Scheduler fails these; re-issuing the request re-runs the encode. |
| 288 | """ |
| 289 | return set() |
| 290 | |
| 291 | def update_state_after_free(self, request: "Request", index: int): |
| 292 | """Notify the connector that an encoder cache entry was released.""" |
| 293 | return |
| 294 | |
| 295 | @abstractmethod |
| 296 | def update_state_after_alloc(self, request: "Request", index: int): |
| 297 | """Update ECConnector state to decide allocate cache for requests. |
| 298 | |
| 299 | Args: |
| 300 | request (Request): the request object. |
| 301 | index (int): index of the encoder cache entry being allocated. |
| 302 | |
| 303 | """ |
| 304 | pass |
| 305 | |
| 306 | @abstractmethod |
| 307 | def build_connector_meta( |
| 308 | self, scheduler_output: SchedulerOutput |
| 309 | ) -> ECConnectorMetadata: |
| 310 | """Build the connector metadata for this step. |
| 311 | |
| 312 | This function should NOT modify fields in the scheduler_output. |
| 313 | Also, calling this function will reset the state of the connector. |
| 314 | |
| 315 | Args: |
| 316 | scheduler_output (SchedulerOutput): the scheduler output object. |
| 317 | |
| 318 | """ |
| 319 | pass |
| 320 | |
| 321 | def update_connector_output(self, connector_output: ECConnectorOutput): |
| 322 | """Update ECConnector state from worker-side connectors output. |
| 323 | |
| 324 | Args: |
| 325 | connector_output (ECConnectorOutput): the worker-side |
| 326 | connectors output. |
| 327 | |
| 328 | """ |
| 329 | return |
| 330 | |
| 331 | def request_finished( |
| 332 | self, request: "Request" |
| 333 | ) -> tuple[bool, dict[str, Any] | None]: |
| 334 | """Called when a request has finished, before its encoder cache is freed. |
| 335 | |
| 336 | Returns: |
| 337 | True if the request is being saved/sent asynchronously and cached |
| 338 | should not be freed until the request_id is returned from |
| 339 | get_finished(). |
| 340 | |
| 341 | """ |
| 342 | return False, None |
| 343 | |
| 344 | def has_pending_push_work(self) -> bool: |
| 345 | """Return True if the connector has push-mode work that requires |
| 346 | the engine main loop to keep stepping (e.g. for EPD, |
| 347 | Producer has push work when Xfer is in progress - Consumer |
| 348 | is reading it). |
| 349 | This mirrors exactly the KV Connector's has_pending_push_work(). |
| 350 | |
| 351 | Connectors that don't implement push-based EC transfer should |
| 352 | leave this as False. |
| 353 | """ |
| 354 | return False |
| 355 | |
| 356 | @classmethod |
| 357 | def build_ec_connector_stats( |
| 358 | cls, data: dict[str, Any] | None = None |
| 359 | ) -> "ECConnectorStats | None": |
| 360 | """ECConnectorStats resolution method. This method allows dynamically |
| 361 | registered connectors to return their own ECConnectorStats object, |
| 362 | which can implement custom aggregation logic on the data dict. |
| 363 | """ |
| 364 | return None |
| 365 | |
| 366 | @classmethod |
| 367 | def build_prom_metrics( |
| 368 | cls, |
| 369 | vllm_config: "VllmConfig", |
| 370 | metric_types: dict[type["PromMetric"], type["PromMetricT"]], |
| 371 | labelnames: list[str], |
| 372 | per_engine_labelvalues: dict[int, list[object]], |
| 373 | ) -> "ECConnectorPromMetrics | None": |
| 374 | """Create an ECConnectorPromMetrics subclass which should register |
| 375 | per-connector Prometheus metrics and implement observe() to |
| 376 | expose connector transfer stats via Prometheus. |
| 377 | """ |
| 378 | return None |