chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,94 @@
"""Lazy re-exports for batch processors.
Each ``*_proc.py`` module pulls in heavy ML dependencies (transformers, vllm,
sglang, ...). Eagerly importing all of them here causes a single
``from ray.llm._internal.batch.processor import HttpRequestProcessorConfig``
to load the entire ML stack and to fail when optional dependencies (e.g.
sglang) are not installed.
We use PEP 562 ``__getattr__`` to load each engine-specific config only when
it is first referenced. The ``base`` module is imported eagerly because it is
cheap and exports types used everywhere else.
Note on registration side effects: each ``*_proc.py`` calls
``ProcessorBuilder.register(...)`` at import time. With lazy loading the
registration happens the first time the corresponding config is accessed via
this package -- which is exactly when a user constructs the config and asks
``ProcessorBuilder.build`` to build a processor for it, so the registry is
populated in time for every realistic usage.
"""
from typing import TYPE_CHECKING
from ray.llm._internal.batch.processor.base import (
Processor,
ProcessorBuilder,
ProcessorConfig,
)
# Mapping of public attribute name -> (submodule, attribute name in submodule).
# Each entry is an engine-specific processor config whose defining submodule
# transitively imports heavy optional dependencies.
_LAZY_ATTRS = {
"HttpRequestProcessorConfig": (
"http_request_proc",
"HttpRequestProcessorConfig",
),
"ServeDeploymentProcessorConfig": (
"serve_deployment_proc",
"ServeDeploymentProcessorConfig",
),
"SGLangEngineProcessorConfig": (
"sglang_engine_proc",
"SGLangEngineProcessorConfig",
),
"vLLMEngineProcessorConfig": (
"vllm_engine_proc",
"vLLMEngineProcessorConfig",
),
}
def __getattr__(name):
"""Lazily import engine-specific processor configs (PEP 562)."""
try:
submodule, attr = _LAZY_ATTRS[name]
except KeyError:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
import importlib
module = importlib.import_module(f"{__name__}.{submodule}")
value = getattr(module, attr)
globals()[name] = value
return value
def __dir__():
return sorted(set(globals()).union(_LAZY_ATTRS))
if TYPE_CHECKING:
from ray.llm._internal.batch.processor.http_request_proc import ( # noqa: F401
HttpRequestProcessorConfig,
)
from ray.llm._internal.batch.processor.serve_deployment_proc import ( # noqa: F401
ServeDeploymentProcessorConfig,
)
from ray.llm._internal.batch.processor.sglang_engine_proc import ( # noqa: F401
SGLangEngineProcessorConfig,
)
from ray.llm._internal.batch.processor.vllm_engine_proc import ( # noqa: F401
vLLMEngineProcessorConfig,
)
__all__ = [
"ProcessorConfig",
"ProcessorBuilder",
"HttpRequestProcessorConfig",
"vLLMEngineProcessorConfig",
"SGLangEngineProcessorConfig",
"ServeDeploymentProcessorConfig",
"Processor",
]
@@ -0,0 +1,533 @@
import logging
from collections import OrderedDict
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from pydantic import Field, field_validator, model_validator
from ray.data import Dataset
from ray.data.block import UserDefinedFunction
from ray.llm._internal.batch.stages import (
StatefulStage,
wrap_postprocess,
wrap_preprocess,
)
from ray.llm._internal.common.base_pydantic import BaseModelExtended
from ray.util.annotations import DeveloperAPI, PublicAPI
logger = logging.getLogger(__name__)
class ProcessorConfig(BaseModelExtended):
"""The processor configuration."""
batch_size: int = Field(
default=32,
description="Large batch sizes are likely to saturate the compute resources "
"and could achieve higher throughput. On the other hand, small batch sizes "
"are more fault-tolerant and could reduce bubbles in the data pipeline. "
"You can tune the batch size to balance the throughput and fault-tolerance "
"based on your use case. Defaults to 32.",
)
resources_per_bundle: Optional[Dict[str, float]] = Field(
default=None,
description="[DEPRECATED] This parameter is deprecated and will be removed in a future version. ",
deprecated=True,
)
accelerator_type: Optional[str] = Field(
default=None,
description="The accelerator type used by the LLM stage in a processor. "
"Default to None, meaning that only the CPU will be used.",
)
concurrency: Union[int, Tuple[int, int]] = Field(
default=1,
description="The number of workers for data parallelism. Default to 1. "
"If ``concurrency`` is a ``tuple`` ``(m, n)``, Ray creates an autoscaling "
"actor pool that scales between ``m`` and ``n`` workers (``1 <= m <= n``). "
"If ``concurrency`` is an ``int`` ``n``, Ray uses either a fixed pool of ``n`` "
"workers or an autoscaling pool from ``1`` to ``n`` workers, depending on "
"the processor and stage.",
)
experimental: Dict[str, Any] = Field(
default_factory=dict,
description="[Experimental] Experimental configurations. "
"Supported keys:\n"
"`max_tasks_in_flight_per_actor`: [DEPRECATED] Prefer the top-level "
"`max_tasks_in_flight_per_actor` field on `OfflineProcessorConfig`. "
"Setting it here is still respected (and overridden by the top-level "
"field if both are set), but logs a deprecation warning.",
)
@field_validator("concurrency")
def validate_concurrency(
cls, concurrency: Union[int, Tuple[int, int]]
) -> Union[int, Tuple[int, int]]:
"""Validate that `concurrency` is either:
- a positive int, or
- a 2-tuple `(min, max)` of positive ints with `min <= max`.
"""
def require(condition: bool, message: str) -> None:
if not condition:
raise ValueError(message)
if isinstance(concurrency, int):
require(
concurrency > 0,
f"A positive integer for `concurrency` is expected! Got: `{concurrency}`.",
)
elif isinstance(concurrency, tuple):
require(
all(c > 0 for c in concurrency),
f"`concurrency` tuple items must be positive integers! Got: `{concurrency}`.",
)
min_concurrency, max_concurrency = concurrency
require(
min_concurrency <= max_concurrency,
f"min > max in the concurrency tuple `{concurrency}`!",
)
return concurrency
def get_concurrency(self, autoscaling_enabled: bool = True) -> Dict[str, int]:
"""Return a normalized dict of worker pool parameters from `self.concurrency`.
Behavior:
- If `concurrency` is an int `n`:
- `autoscaling_enabled` is True -> return `{"min_size": 1, "max_size": n}` (autoscaling).
- `autoscaling_enabled` is False -> return `{"size": n}` (fixed-size pool).
- If `concurrency` is a 2-tuple `(m, n)`, return `{"min_size": m, "max_size": n}`
(the `autoscaling_enabled` flag is ignored).
Args:
autoscaling_enabled: When False, treat an integer `concurrency` as fixed size;
otherwise treat it as an autoscaling range from 1 to n. Defaults to True.
Returns:
Dict[str, int]: A dictionary with either:
- `{"size": n}` for fixed-size pools
- `{"min_size": m, "max_size": n}` for autoscaling pools
Examples:
>>> self.concurrency = (2, 4)
>>> self.get_concurrency()
{'min_size': 2, 'max_size': 4}
>>> self.concurrency = 4
>>> self.get_concurrency()
{'min_size': 1, 'max_size': 4}
>>> self.get_concurrency(autoscaling_enabled=False)
{'size': 4}
"""
if isinstance(self.concurrency, int):
if autoscaling_enabled:
return {"min_size": 1, "max_size": self.concurrency}
else:
return {"size": self.concurrency}
return {
"min_size": self.concurrency[0],
"max_size": self.concurrency[1],
}
class Config:
validate_assignment = True
arbitrary_types_allowed = True
class OfflineProcessorConfig(ProcessorConfig):
"""The processor configuration for offline processing."""
model_source: str = Field(
description="The model source to use for the offline processing.",
)
runtime_env: Optional[Dict[str, Any]] = Field(
default=None,
description="The runtime environment to use for the offline processing.",
)
max_pending_requests: Optional[int] = Field(
default=None,
description="The maximum number of pending requests. If not specified, "
"will use the default value from the backend engine.",
)
max_concurrent_batches: int = Field(
default=8,
description="The maximum number of concurrent batches in the engine. "
"This is to overlap the batch processing to avoid the tail latency of "
"each batch. The default value may not be optimal when the batch size "
"or the batch processing latency is too small, but it should be good "
"enough for batch size >= 32. Sets the engine actor's Ray Core "
"`max_concurrency`.",
)
max_tasks_in_flight_per_actor: Optional[int] = Field(
default=None,
description="Max tasks Ray Data submits concurrently to each engine "
"actor. Passed through to `ray.data.ActorPoolStrategy`. If unset, Ray "
"Data uses `ray.data.DataContext.max_tasks_in_flight_per_actor` if set "
"globally. Otherwise, it defaults to `2 * max_concurrent_batches`; the "
"factor can be overridden via the "
"`RAY_DATA_ACTOR_DEFAULT_MAX_TASKS_IN_FLIGHT_TO_MAX_CONCURRENCY_FACTOR` "
"env var. "
"Setting this lower than `max_concurrent_batches` can underutilize the "
"engine actor because Ray Data submits fewer tasks than the actor can "
"process concurrently.",
)
should_continue_on_error: bool = Field(
default=False,
description="If True, continue processing when inference fails for a row "
"instead of raising an exception. Failed rows will have a non-empty "
"'__inference_error__' column containing the error message, and other "
"output columns will be empty strings. Error rows bypass postprocess. "
"If False (default), any inference error will raise an exception.",
)
# Processor stage configurations (legacy booleans, will be deprecated).
# TODO (jeffreywang): Remove apply_chat_template, chat_template, tokenize,
# detokenize in Ray 2.57.0 in favor of the *_stage fields below.
apply_chat_template: bool = Field(
default=True,
description="[DEPRECATED] Prefer `chat_template_stage`. Whether to apply chat template.",
)
chat_template: Optional[str] = Field(
default=None,
description="[DEPRECATED] Prefer `chat_template_stage.chat_template`. The chat template to use.",
)
tokenize: bool = Field(
default=True,
description="[DEPRECATED] Prefer `tokenize_stage`. Whether to tokenize input before engine.",
)
detokenize: bool = Field(
default=True,
description="[DEPRECATED] Prefer `detokenize_stage`. Whether to detokenize the output.",
)
# New nested stage configuration (bool | dict | typed config).
chat_template_stage: Any = Field(
default=True,
description="Chat templating stage config (bool | dict | ChatTemplateStageConfig).",
)
tokenize_stage: Any = Field(
default=True,
description="Tokenizer stage config (bool | dict | TokenizerStageConfig).",
)
detokenize_stage: Any = Field(
default=True,
description="Detokenizer stage config (bool | dict | DetokenizeStageConfig).",
)
prepare_multimodal_stage: Any = Field(
default=False,
description="Prepare multimodal stage config (bool | dict | PrepareMultimodalStageConfig).",
)
@model_validator(mode="before")
def _coerce_legacy_to_stage_config(cls, values: Dict[str, Any]) -> Dict[str, Any]:
# Only set stage fields if not explicitly provided.
# Emit deprecation warnings when legacy boolean flags are used.
# Chat template stage: special case (handles both apply_chat_template and chat_template fields)
if "chat_template_stage" not in values:
if "apply_chat_template" in values or "chat_template" in values:
logger.warning(
"The `apply_chat_template` and `chat_template` fields are deprecated. "
"Use `chat_template_stage` instead. For example: "
"`chat_template_stage=ChatTemplateStageConfig(enabled=True, chat_template='...')` "
"or `chat_template_stage={'enabled': True, 'chat_template': '...'}`. "
"This will raise an error in a future version."
)
enabled_value = values.get("apply_chat_template")
enabled = enabled_value if enabled_value is not None else True
stage: Dict[str, Any] = {"enabled": enabled}
if values.get("chat_template") is not None:
stage["chat_template"] = values["chat_template"]
values["chat_template_stage"] = stage
# Other stages: simple boolean-to-stage mapping
stage_mappings = [
("tokenize_stage", "tokenize", True, "TokenizerStageConfig"),
("detokenize_stage", "detokenize", True, "DetokenizeStageConfig"),
]
for (
stage_field,
legacy_field,
default_enabled,
config_class_name,
) in stage_mappings:
if stage_field not in values and legacy_field in values:
logger.warning(
f"The `{legacy_field}` field is deprecated. "
f"Use `{stage_field}` instead. For example: "
f"`{stage_field}={config_class_name}(enabled=True)` "
f"or `{stage_field}={{'enabled': True}}`. "
"This will raise an error in a future version."
)
legacy_value = values.get(legacy_field)
enabled = default_enabled if legacy_value is None else legacy_value
values[stage_field] = {"enabled": enabled}
return values
@model_validator(mode="before")
def _migrate_experimental_max_tasks_in_flight_per_actor(
cls, values: Dict[str, Any]
) -> Dict[str, Any]:
"""Migrate deprecated `experimental[max_tasks_in_flight_per_actor]` to
the top-level field; top-level wins if both are set."""
experimental = values.get("experimental") or {}
if "max_tasks_in_flight_per_actor" in experimental:
logger.warning(
"Setting `max_tasks_in_flight_per_actor` via `experimental` is "
"deprecated; use the top-level `max_tasks_in_flight_per_actor` "
"field on `OfflineProcessorConfig` instead. The value in "
"`experimental` is still respected for now (and overridden by "
"the top-level field if both are set), but will be removed in "
"a future version."
)
if values.get("max_tasks_in_flight_per_actor") is None:
values["max_tasks_in_flight_per_actor"] = experimental[
"max_tasks_in_flight_per_actor"
]
return values
@model_validator(mode="after")
def _warn_if_max_tasks_in_flight_underutilizes_actor(self):
if (
self.max_tasks_in_flight_per_actor is not None
and self.max_tasks_in_flight_per_actor < self.max_concurrent_batches
):
logger.warning(
"Setting `max_tasks_in_flight_per_actor` (%s) lower than "
"`max_concurrent_batches` (%s) can underutilize each engine "
"actor because Ray Data will submit fewer tasks than the actor "
"can process concurrently.",
self.max_tasks_in_flight_per_actor,
self.max_concurrent_batches,
)
return self
@PublicAPI(stability="beta")
class Processor:
"""A processor is composed of a preprocess stage, followed by one or more
processing stages, and finally a postprocess stage. We use processor as a
paradigm for processing data using LLMs.
Args:
config: The processor config.
stages: List of processing stages.
preprocess: An optional lambda function that takes a row (dict) as input
and returns a preprocessed row (dict). The output row must contain the
required fields for the following processing stages.
postprocess: An optional lambda function that takes a row (dict) as input
and returns a postprocessed row (dict).
preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
preprocess stage (e.g., num_cpus, memory, concurrency).
postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
postprocess stage (e.g., num_cpus, memory, concurrency).
"""
# The internal used data column name ("__data"). Your input
# dataset should not contain this column. If you want to use this column
# in your input dataset, you have to derive and customize Processor.
DATA_COLUMN: str = "__data"
def __init__(
self,
config: ProcessorConfig,
stages: List[StatefulStage],
preprocess: Optional[UserDefinedFunction] = None,
postprocess: Optional[UserDefinedFunction] = None,
preprocess_map_kwargs: Optional[Dict[str, Any]] = None,
postprocess_map_kwargs: Optional[Dict[str, Any]] = None,
):
self.config = config
self.preprocess = None
self.postprocess = None
self.preprocess_map_kwargs = preprocess_map_kwargs or {}
self.postprocess_map_kwargs = postprocess_map_kwargs or {}
self.stages: OrderedDict[str, StatefulStage] = OrderedDict()
# NOTE (Kourosh): If pre/postprocess is not provided, use the identity function.
# Wrapping is required even if they are identity functions, b/c data_column
# gets inserted/removed via wrap_preprocess/wrap_postprocess.
preprocess = preprocess or (lambda row: row)
postprocess = postprocess or (lambda row: row)
self.preprocess = wrap_preprocess(
preprocess,
self.DATA_COLUMN,
)
# When should_continue_on_error is enabled, include __inference_error__ column
# in all output rows for consistent schema (Empty string for success, message for error).
include_error_column = getattr(config, "should_continue_on_error", False)
self.postprocess = wrap_postprocess(
postprocess,
self.DATA_COLUMN,
include_error_column=include_error_column,
)
for stage in stages:
self._append_stage(stage)
def __call__(self, dataset: Dataset) -> Dataset:
"""Execute the processor:
preprocess -> stages -> postprocess.
Note that the dataset won't be materialized during the execution.
Args:
dataset: The input dataset.
Returns:
The output dataset.
"""
if self.preprocess is not None:
dataset = dataset.map(self.preprocess, **self.preprocess_map_kwargs)
# Apply stages.
for stage in self.stages.values():
kwargs = stage.get_dataset_map_batches_kwargs(
batch_size=self.config.batch_size,
data_column=self.DATA_COLUMN,
)
dataset = dataset.map_batches(stage.fn, **kwargs)
if self.postprocess is not None:
dataset = dataset.map(self.postprocess, **self.postprocess_map_kwargs)
return dataset
def _append_stage(self, stage: StatefulStage) -> None:
"""Append a stage before postprocess. The stage class name will be used as
the stage name. If there are multiple stages with the same type, a suffix
will be added to the stage name to avoid conflicts.
Args:
stage: The stage to append.
"""
stage_name = type(stage).__name__
# When a processor has multiple stages with the same type,
# append a index suffix to the stage name to avoid conflicts.
if stage_name in self.stages:
num_same_type_stage = len([s for s in self.stages.values() if s is stage])
stage_name = f"{stage_name}_{num_same_type_stage + 1}"
self.stages[stage_name] = stage
def list_stage_names(self) -> List[str]:
"""List the stage names of this processor in order. Preprocess and postprocess
are not included.
Returns:
A list of stage names.
"""
return list(self.stages.keys())
def get_stage_by_name(self, name: str) -> StatefulStage:
"""Get a particular stage by its name. If the stage is not found,
a ValueError will be raised.
Args:
name: The stage name.
Returns:
The pipeline stage.
"""
if name in self.stages:
return self.stages[name]
raise ValueError(f"Stage {name} not found")
def log_input_column_names(self):
"""Log.info the input stage and column names of this processor.
If the input dataset does not contain these columns, you have to
provide a preprocess function to bridge the gap.
"""
name, stage = list(self.stages.items())[0]
expected_input_keys = stage.get_required_input_keys()
optional_input_keys = stage.get_optional_input_keys()
message = f"The first stage of the processor is {name}."
if expected_input_keys:
message += "\nRequired input columns:\n"
message += "\n".join(f"\t{k}: {v}" for k, v in expected_input_keys.items())
if optional_input_keys:
message += "\nOptional input columns:\n"
message += "\n".join(f"\t{k}: {v}" for k, v in optional_input_keys.items())
logger.info(message)
@DeveloperAPI
class ProcessorBuilder:
"""Build a processor based on the configuration."""
_registry: Dict[str, Callable] = {}
@classmethod
def register(cls, config_type: Type[ProcessorConfig], builder: Callable) -> None:
"""A decorator to associate a particular pipeline config
with its build function.
"""
type_name = config_type.__name__
if type_name in cls._registry:
raise ValueError(f"Processor config type {type_name} already registered.")
cls._registry[type_name] = builder
@classmethod
def clear_registry(cls) -> None:
"""Clear the processor builder registry."""
cls._registry.clear()
@classmethod
def validate_builder_kwargs(cls, builder_kwargs: Optional[Dict[str, Any]]) -> None:
"""Validate builder kwargs for conflicts with reserved keys.
Args:
builder_kwargs: Optional additional kwargs to pass to the processor builder
function.
Raises:
ValueError: If builder_kwargs contains reserved keys that conflict with
explicit arguments.
"""
if builder_kwargs is not None:
# Check for conflicts with explicitly passed arguments
reserved_keys = {
"preprocess",
"postprocess",
"preprocess_map_kwargs",
"postprocess_map_kwargs",
}
conflicting_keys = reserved_keys & builder_kwargs.keys()
if conflicting_keys:
raise ValueError(
f"builder_kwargs cannot contain {conflicting_keys} as these are "
"passed as explicit arguments to build_processor. "
"Please pass these directly instead of in builder_kwargs."
)
@classmethod
def build(
cls,
config: ProcessorConfig,
override_stage_config_fn: Optional[Callable] = None,
**kwargs,
) -> Processor:
"""Build a processor.
Args:
config: The processor config.
override_stage_config_fn: Custom stages configurations.
**kwargs: Additional keyword arguments to pass through to the
registered builder function. The builder function must accept
these kwargs in its signature, otherwise a TypeError will be raised.
Returns:
The built processor.
"""
type_name = type(config).__name__
if type_name not in cls._registry:
raise ValueError(
f"Processor config type {type_name} not registered. "
f"Available types: {cls._registry.keys()}"
)
processor = cls._registry[type_name](config, **kwargs)
if override_stage_config_fn is not None:
for name, stage in processor.stages.items():
override_stage_config_fn(name, stage)
return processor
@@ -0,0 +1,146 @@
"""The HTTP request processor."""
import hashlib
from typing import Any, Dict, Optional
from pydantic import Field
from ray.data.block import UserDefinedFunction
from ray.llm._internal.batch.observability.usage_telemetry.usage import (
BatchModelTelemetry,
get_or_create_telemetry_agent,
)
from ray.llm._internal.batch.processor.base import (
Processor,
ProcessorBuilder,
ProcessorConfig,
)
from ray.llm._internal.batch.processor.utils import build_cpu_stage_map_kwargs
from ray.llm._internal.batch.stages import HttpRequestStage
from ray.llm._internal.batch.stages.configs import (
HttpRequestStageConfig,
resolve_stage_config,
)
class HttpRequestProcessorConfig(ProcessorConfig):
"""The configuration for the HTTP request processor."""
batch_size: int = Field(
default=64,
description="The batch size.",
)
url: str = Field(
description="The URL to query.",
)
headers: Optional[Dict[str, Any]] = Field(
default=None,
description="The query header. Note that we will add "
"'Content-Type: application/json' to be the header for sure "
"because we only deal with requests body in JSON.",
)
qps: Optional[int] = Field(
default=None,
description="The maximum number of requests per second to avoid rate limit. "
"If None, the request will be sent sequentially.",
)
max_retries: int = Field(
default=0,
description="The maximum number of retries per request in the event of failures.",
)
base_retry_wait_time_in_s: float = Field(
default=1,
description="The base wait time for a retry during exponential backoff.",
)
# Since `session_factory` is a callable, we use type Any to avoid pydantic serialization issues
session_factory: Optional[Any] = Field(
default=None,
description="Optional session factory to be used for initializing a client session. Type: Callable[[], ClientSession]",
# exclude from JSON serialization since `session_factory` is a callable
exclude=True,
)
http_request_stage: Any = Field(
default=True,
description="Http request stage config (bool | dict | HttpRequestStageConfig).",
)
def build_http_request_processor(
config: HttpRequestProcessorConfig,
preprocess: Optional[UserDefinedFunction] = None,
postprocess: Optional[UserDefinedFunction] = None,
preprocess_map_kwargs: Optional[Dict[str, Any]] = None,
postprocess_map_kwargs: Optional[Dict[str, Any]] = None,
) -> Processor:
"""Construct a Processor and configure stages.
Args:
config: The configuration for the processor.
preprocess: An optional lambda function that takes a row (dict) as input
and returns a preprocessed row (dict). The output row must contain the
required fields for the following processing stages.
postprocess: An optional lambda function that takes a row (dict) as input
and returns a postprocessed row (dict).
preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
preprocess stage (e.g., num_cpus, memory, concurrency).
postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
postprocess stage (e.g., num_cpus, memory, concurrency).
Returns:
The constructed processor.
"""
# Prepare processor defaults for merging into stage configs
processor_defaults = {
"batch_size": config.batch_size,
"concurrency": config.concurrency,
}
# Resolve and build HttpRequestStage if enabled
http_request_stage_cfg = resolve_stage_config(
config.http_request_stage,
HttpRequestStageConfig,
processor_defaults,
)
if not http_request_stage_cfg.enabled:
raise ValueError(
"The HTTP request stage is required and cannot be disabled in HttpRequestProcessorConfig."
)
stages = [
HttpRequestStage(
fn_constructor_kwargs=dict(
url=config.url,
additional_header=config.headers,
qps=config.qps,
max_retries=config.max_retries,
base_retry_wait_time_in_s=config.base_retry_wait_time_in_s,
session_factory=config.session_factory,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(http_request_stage_cfg),
)
]
telemetry_agent = get_or_create_telemetry_agent()
telemetry_agent.push_telemetry_report(
BatchModelTelemetry(
# Hash the target URL so distinct endpoints stay separate in the
# dedup key without the cleartext URL reaching the head-node actor.
model_id_hash=hashlib.sha256(config.url.encode("utf-8")).hexdigest(),
processor_config_name=type(config).__name__,
batch_size=config.batch_size,
concurrency=config.concurrency,
)
)
processor = Processor(
config,
stages,
preprocess=preprocess,
postprocess=postprocess,
preprocess_map_kwargs=preprocess_map_kwargs,
postprocess_map_kwargs=postprocess_map_kwargs,
)
return processor
ProcessorBuilder.register(HttpRequestProcessorConfig, build_http_request_processor)
@@ -0,0 +1,107 @@
"""The processor that runs serve deployment."""
from typing import Any, Dict, Optional, Type
from pydantic import Field
from ray.data import ActorPoolStrategy
from ray.data.block import UserDefinedFunction
from ray.llm._internal.batch.processor.base import (
Processor,
ProcessorBuilder,
ProcessorConfig,
)
from ray.llm._internal.batch.stages import (
ServeDeploymentStage,
)
class ServeDeploymentProcessorConfig(ProcessorConfig):
"""The configuration for the serve deployment processor."""
# Configurations used to build the serve deployment
deployment_name: str = Field(
description="The name of the serve deployment to use.",
)
app_name: str = Field(
description="The name of the serve application to use.",
default="default",
)
dtype_mapping: Dict[str, Type[Any]] = Field(
description="A dictionary mapping data type names to their corresponding request classes for the serve deployment.",
default=None,
)
should_continue_on_error: bool = Field(
default=False,
description="If True, continue processing when inference fails for a row "
"instead of raising an exception. Failed rows will have a non-null "
"'__inference_error__' column containing the error message. Error rows "
"bypass postprocess. If False (default), any inference error raises.",
)
request_timeout_s: Optional[float] = Field(
default=None,
gt=0,
description="Optional per-request timeout in seconds. When set, a request "
"that does not return within this many seconds raises TimeoutError instead "
"of blocking indefinitely (e.g. when replicas are saturated). TimeoutError "
"is recoverable, so combine with should_continue_on_error=True to drop the "
"slow row as an error instead of failing the job. If None (default), "
"requests wait indefinitely.",
)
def build_serve_deployment_processor(
config: ServeDeploymentProcessorConfig,
preprocess: Optional[UserDefinedFunction] = None,
postprocess: Optional[UserDefinedFunction] = None,
preprocess_map_kwargs: Optional[Dict[str, Any]] = None,
postprocess_map_kwargs: Optional[Dict[str, Any]] = None,
) -> Processor:
"""Construct a processor that runs a serve deployment.
Args:
config: The configuration for the processor.
preprocess: An optional lambda function that takes a row (dict) as input
and returns a preprocessed row (dict). The output row must contain the
required fields for the following processing stages.
postprocess: An optional lambda function that takes a row (dict) as input
and returns a postprocessed row (dict).
preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
preprocess stage (e.g., num_cpus, memory, concurrency).
postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
postprocess stage (e.g., num_cpus, memory, concurrency).
Returns:
The constructed processor.
"""
stages = [
ServeDeploymentStage(
fn_constructor_kwargs=dict(
deployment_name=config.deployment_name,
app_name=config.app_name,
dtype_mapping=config.dtype_mapping,
should_continue_on_error=config.should_continue_on_error,
request_timeout_s=config.request_timeout_s,
),
map_batches_kwargs=dict(
compute=ActorPoolStrategy(
**config.get_concurrency(autoscaling_enabled=False),
)
),
)
]
# TODO (Kourosh): Add telemetry for ServeDeploymentStage
processor = Processor(
config,
stages,
preprocess=preprocess,
postprocess=postprocess,
preprocess_map_kwargs=preprocess_map_kwargs,
postprocess_map_kwargs=postprocess_map_kwargs,
)
return processor
ProcessorBuilder.register(
ServeDeploymentProcessorConfig, build_serve_deployment_processor
)
@@ -0,0 +1,279 @@
"""The SGLang engine processor."""
import hashlib
import logging
from typing import Any, Dict, Optional
import transformers
from pydantic import Field, root_validator
import ray
from ray.data.block import UserDefinedFunction
from ray.llm._internal.batch.constants import SGLangTaskType, TypeSGLangTaskType
from ray.llm._internal.batch.observability.usage_telemetry.usage import (
BatchModelTelemetry,
TelemetryAgent,
get_or_create_telemetry_agent,
)
from ray.llm._internal.batch.processor.base import (
OfflineProcessorConfig,
Processor,
ProcessorBuilder,
)
from ray.llm._internal.batch.processor.utils import (
build_cpu_stage_map_kwargs,
get_value_or_fallback,
)
from ray.llm._internal.batch.stages import (
ChatTemplateStage,
DetokenizeStage,
SGLangEngineStage,
TokenizeStage,
)
from ray.llm._internal.batch.stages.configs import (
ChatTemplateStageConfig,
DetokenizeStageConfig,
TokenizerStageConfig,
resolve_stage_config,
)
from ray.llm._internal.common.observability.telemetry_utils import DEFAULT_GPU_TYPE
from ray.llm._internal.common.utils.download_utils import (
NodeModelDownloadable,
download_model_files,
)
logger = logging.getLogger(__name__)
DEFAULT_MODEL_ARCHITECTURE = "UNKNOWN_MODEL_ARCHITECTURE"
class SGLangEngineProcessorConfig(OfflineProcessorConfig):
"""The configuration for the SGLang engine processor."""
# SGLang stage configurations.
engine_kwargs: Dict[str, Any] = Field(
default_factory=dict,
description="The kwargs to pass to the SGLang engine. See "
"https://docs.sglang.ai/backend/server_arguments.html "
"for more details.",
)
task_type: TypeSGLangTaskType = Field(
default=SGLangTaskType.GENERATE,
description="The task type to use. If not specified, will use "
"'generate' by default.",
)
@root_validator(pre=True)
def validate_task_type(cls, values):
task_type = values.get("task_type", SGLangTaskType.GENERATE)
if task_type not in SGLangTaskType.values():
raise ValueError(f"Invalid task type: {task_type}")
engine_kwargs = values.get("engine_kwargs", {})
engine_kwargs_task = engine_kwargs.get("task", "")
if engine_kwargs_task != task_type:
logger.warning(
"The task set in engine kwargs (%s) is different from the "
"stage (%s). Overriding the task in engine kwargs to %s.",
engine_kwargs_task,
task_type,
task_type,
)
engine_kwargs["task"] = task_type
values["engine_kwargs"] = engine_kwargs
return values
def build_sglang_engine_processor(
config: SGLangEngineProcessorConfig,
chat_template_kwargs: Optional[Dict[str, Any]] = None,
preprocess: Optional[UserDefinedFunction] = None,
postprocess: Optional[UserDefinedFunction] = None,
preprocess_map_kwargs: Optional[Dict[str, Any]] = None,
postprocess_map_kwargs: Optional[Dict[str, Any]] = None,
telemetry_agent: Optional[TelemetryAgent] = None,
) -> Processor:
"""Construct a Processor and configure stages.
Args:
config: The configuration for the processor.
chat_template_kwargs: The optional kwargs to pass to apply_chat_template.
preprocess: An optional lambda function that takes a row (dict) as input
and returns a preprocessed row (dict). The output row must contain the
required fields for the following processing stages.
postprocess: An optional lambda function that takes a row (dict) as input
and returns a postprocessed row (dict).
preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
preprocess stage (e.g., num_cpus, memory, concurrency).
postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
postprocess stage (e.g., num_cpus, memory, concurrency).
telemetry_agent: An optional telemetry agent for collecting usage telemetry.
Returns:
The constructed processor.
"""
ray.init(runtime_env=config.runtime_env, ignore_reinit_error=True)
stages = []
# Prepare processor defaults for merging into stage configs
trust_remote_code = config.engine_kwargs.get("trust_remote_code", False)
processor_defaults = {
"batch_size": config.batch_size,
"concurrency": config.concurrency,
"runtime_env": config.runtime_env,
"model_source": config.model_source,
}
# Resolve and build ChatTemplateStage if enabled
chat_template_stage_cfg = resolve_stage_config(
config.chat_template_stage,
ChatTemplateStageConfig,
processor_defaults,
)
if chat_template_stage_cfg.enabled:
stages.append(
ChatTemplateStage(
fn_constructor_kwargs=dict(
model=chat_template_stage_cfg.model_source,
chat_template=get_value_or_fallback(
chat_template_stage_cfg.chat_template, config.chat_template
),
chat_template_kwargs=get_value_or_fallback(
chat_template_stage_cfg.chat_template_kwargs,
chat_template_kwargs,
),
trust_remote_code=trust_remote_code,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(chat_template_stage_cfg),
)
)
# Resolve and build TokenizeStage if enabled
tokenize_stage_cfg = resolve_stage_config(
getattr(config, "tokenize_stage", config.tokenize),
TokenizerStageConfig,
processor_defaults,
)
if tokenize_stage_cfg.enabled:
stages.append(
TokenizeStage(
fn_constructor_kwargs=dict(
model=tokenize_stage_cfg.model_source,
trust_remote_code=trust_remote_code,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(tokenize_stage_cfg),
)
)
# Core stage -- the SGLang engine.
stages.append(
SGLangEngineStage(
fn_constructor_kwargs=dict(
model=config.model_source,
engine_kwargs=config.engine_kwargs,
task_type=config.task_type,
max_pending_requests=config.max_pending_requests,
),
map_batches_kwargs=dict(
zero_copy_batch=True,
# The number of running replicas. This is a deprecated field, but
# we need to set `max_tasks_in_flight_per_actor` through `compute`,
# which initiates enough many overlapping UDF calls per actor, to
# saturate `max_concurrency`.
compute=ray.data.ActorPoolStrategy(
**config.get_concurrency(autoscaling_enabled=True),
max_tasks_in_flight_per_actor=config.max_tasks_in_flight_per_actor,
),
# The number of running batches "per actor" in Ray Core level.
# This is used to make sure we overlap batches to avoid the tail
# latency of each batch.
max_concurrency=config.max_concurrent_batches,
accelerator_type=config.accelerator_type,
runtime_env=config.runtime_env,
),
)
)
# Resolve and build DetokenizeStage if enabled
detokenize_stage_cfg = resolve_stage_config(
getattr(config, "detokenize_stage", config.detokenize),
DetokenizeStageConfig,
processor_defaults,
)
if detokenize_stage_cfg.enabled:
stages.append(
DetokenizeStage(
fn_constructor_kwargs=dict(
model=detokenize_stage_cfg.model_source,
trust_remote_code=trust_remote_code,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(detokenize_stage_cfg),
)
)
# Download model files for telemetry before engine init.
# Use EXCLUDE_SAFETENSORS for trust_remote_code models so custom .py config
# files are available locally.
try:
download_mode = (
NodeModelDownloadable.EXCLUDE_SAFETENSORS
if trust_remote_code
else NodeModelDownloadable.TOKENIZER_ONLY
)
model_path_or_id = download_model_files(
model_id=config.model_source,
mirror_config=None,
download_model=download_mode,
download_extra_files=False,
)
hf_config = transformers.AutoConfig.from_pretrained(
model_path_or_id,
trust_remote_code=trust_remote_code,
)
except Exception as e:
# Failed to retrieve HuggingFace config for telemetry purposes.
# This is non-fatal: we fall back to DEFAULT_MODEL_ARCHITECTURE for telemetry.
# The actual model loading happens later in SGLang, which may support models
# that aren't available via HuggingFace's AutoConfig.
logger.warning(
"Failed to retrieve HuggingFace config for %s: %s",
config.model_source,
e,
)
hf_config = None
architectures = getattr(hf_config, "architectures", [])
architecture = architectures[0] if architectures else DEFAULT_MODEL_ARCHITECTURE
telemetry_agent = get_or_create_telemetry_agent()
telemetry_agent.push_telemetry_report(
BatchModelTelemetry(
model_id_hash=hashlib.sha256(
config.model_source.encode("utf-8")
).hexdigest(),
processor_config_name=type(config).__name__,
model_architecture=architecture,
batch_size=config.batch_size,
accelerator_type=config.accelerator_type or DEFAULT_GPU_TYPE,
concurrency=config.concurrency,
task_type=config.task_type,
tensor_parallel_size=config.engine_kwargs.get("tp_size", 1),
data_parallel_size=config.engine_kwargs.get("dp_size", 1),
)
)
processor = Processor(
config,
stages,
preprocess=preprocess,
postprocess=postprocess,
preprocess_map_kwargs=preprocess_map_kwargs,
postprocess_map_kwargs=postprocess_map_kwargs,
)
return processor
ProcessorBuilder.register(SGLangEngineProcessorConfig, build_sglang_engine_processor)
@@ -0,0 +1,58 @@
"""Shared utility functions for processor builders."""
from typing import Any, Dict, Optional, Tuple, Union
from ray.data import ActorPoolStrategy
from ray.llm._internal.batch.stages.configs import _StageConfigBase
def get_value_or_fallback(value: Any, fallback: Any) -> Any:
"""Return value if not None, otherwise return fallback."""
return value if value is not None else fallback
def extract_resource_kwargs(
runtime_env: Optional[Dict[str, Any]],
num_cpus: Optional[float],
memory: Optional[float],
) -> Dict[str, Any]:
"""Extract non-None resource kwargs for map_batches."""
kwargs = {}
if runtime_env is not None:
kwargs["runtime_env"] = runtime_env
if num_cpus is not None:
kwargs["num_cpus"] = num_cpus
if memory is not None:
kwargs["memory"] = memory
return kwargs
def normalize_cpu_stage_concurrency(
concurrency: Optional[Union[int, Tuple[int, int]]]
) -> Dict[str, int]:
"""Normalize concurrency for CPU stages (int -> (1, int) for autoscaling)."""
if concurrency is None:
return {"size": 1} # Default to minimal autoscaling pool
if isinstance(concurrency, int):
return {"min_size": 1, "max_size": concurrency}
return {
"min_size": concurrency[0],
"max_size": concurrency[1],
}
def build_cpu_stage_map_kwargs(
stage_cfg: _StageConfigBase,
) -> Dict[str, Any]:
"""Build map_batches_kwargs for CPU stages."""
concurrency = normalize_cpu_stage_concurrency(stage_cfg.concurrency)
return dict(
zero_copy_batch=True,
compute=ActorPoolStrategy(**concurrency),
batch_size=stage_cfg.batch_size,
**extract_resource_kwargs(
stage_cfg.runtime_env,
stage_cfg.num_cpus,
stage_cfg.memory,
),
)
@@ -0,0 +1,359 @@
"""The vLLM engine processor."""
import hashlib
import logging
from typing import Any, Dict, Optional
import transformers
from pydantic import Field, field_validator, model_validator
import ray
from ray.data.block import UserDefinedFunction
from ray.llm._internal.batch.constants import TypeVLLMTaskType, vLLMTaskType
from ray.llm._internal.batch.observability.usage_telemetry.usage import (
BatchModelTelemetry,
TelemetryAgent,
get_or_create_telemetry_agent,
)
from ray.llm._internal.batch.processor.base import (
OfflineProcessorConfig,
Processor,
ProcessorBuilder,
)
from ray.llm._internal.batch.processor.utils import (
build_cpu_stage_map_kwargs,
get_value_or_fallback,
)
from ray.llm._internal.batch.stages import (
ChatTemplateStage,
DetokenizeStage,
PrepareMultimodalStage,
TokenizeStage,
vLLMEngineStage,
)
from ray.llm._internal.batch.stages.configs import (
ChatTemplateStageConfig,
DetokenizeStageConfig,
PrepareMultimodalStageConfig,
TokenizerStageConfig,
resolve_stage_config,
)
from ray.llm._internal.common.observability.telemetry_utils import DEFAULT_GPU_TYPE
from ray.llm._internal.common.placement import PlacementGroupConfig
from ray.llm._internal.common.utils.download_utils import (
STREAMING_LOAD_FORMATS,
NodeModelDownloadable,
download_model_files,
)
logger = logging.getLogger(__name__)
DEFAULT_MODEL_ARCHITECTURE = "UNKNOWN_MODEL_ARCHITECTURE"
class vLLMEngineProcessorConfig(OfflineProcessorConfig):
"""The configuration for the vLLM engine processor."""
# vLLM stage configurations.
engine_kwargs: Dict[str, Any] = Field(
default_factory=dict,
description="The kwargs to pass to the vLLM engine. See "
"https://docs.vllm.ai/en/latest/serving/engine_args.html "
"for more details.",
)
task_type: TypeVLLMTaskType = Field(
default=vLLMTaskType.GENERATE,
description="The task type to use. If not specified, will use "
"'generate' by default.",
)
log_engine_metrics: bool = Field(
default=True,
description="Enable vLLM engine metrics export via Ray's Prometheus endpoint. "
"When enabled, metrics like prefix cache hit rate, TTFT, TPOT, KV cache "
"utilization, and scheduler state are available at Ray's metrics endpoint. "
"Requires Ray to be initialized with _metrics_export_port "
"(e.g., ray.init(_metrics_export_port=8080)).",
)
# LoRA configurations.
dynamic_lora_loading_path: Optional[str] = Field(
default=None,
description="The path to the dynamic LoRA adapter. It is expected "
"to hold subfolders each for a different lora checkpoint. If not "
"specified and LoRA is enabled, then the 'model' in LoRA "
"requests will be interpreted as model ID used by HF transformers.",
)
# Custom placement group config for TP/PP.
placement_group_config: Optional[Dict[str, Any]] = Field(
default=None,
description="Ray placement group configuration for scheduling vLLM engine workers. "
"Can specify either 'bundle_per_worker' (auto-replicated by tp*pp) or 'bundles' "
"(full list of resource dicts). Optionally include 'strategy' key "
"('PACK', 'STRICT_PACK', 'SPREAD', or 'STRICT_SPREAD'). "
"Example with bundle_per_worker: {'bundle_per_worker': {'CPU': 1, 'GPU': 1}, 'strategy': 'SPREAD'}. "
"Example with bundles: {'bundles': [{'CPU': 1, 'GPU': 1}] * 4, 'strategy': 'SPREAD'}.",
)
@model_validator(mode="before")
@classmethod
def validate_task_type(cls, values):
task_type = values.get("task_type", vLLMTaskType.GENERATE)
if task_type not in vLLMTaskType.values():
raise ValueError(f"Invalid task type: {task_type}")
engine_kwargs = values.get("engine_kwargs", {})
engine_kwargs_task_type = engine_kwargs.get("task_type", "")
if engine_kwargs_task_type != task_type:
if engine_kwargs_task_type:
logger.warning(
"The task_type set in engine kwargs (%s) is different from the "
"config (%s). Overriding the task_type in engine kwargs to %s.",
engine_kwargs_task_type,
task_type,
task_type,
)
engine_kwargs["task_type"] = task_type
values["engine_kwargs"] = engine_kwargs
return values
@field_validator("placement_group_config")
@classmethod
def validate_placement_group_config(cls, value):
if value is None:
return None
# Validate through PlacementGroupConfig, then dump back to dict
validated = PlacementGroupConfig(**value)
return validated.model_dump()
def build_vllm_engine_processor(
config: vLLMEngineProcessorConfig,
chat_template_kwargs: Optional[Dict[str, Any]] = None,
preprocess: Optional[UserDefinedFunction] = None,
postprocess: Optional[UserDefinedFunction] = None,
preprocess_map_kwargs: Optional[Dict[str, Any]] = None,
postprocess_map_kwargs: Optional[Dict[str, Any]] = None,
telemetry_agent: Optional[TelemetryAgent] = None,
) -> Processor:
"""Construct a Processor and configure stages.
Args:
config: The configuration for the processor.
chat_template_kwargs: The optional kwargs to pass to apply_chat_template.
preprocess: An optional lambda function that takes a row (dict) as input
and returns a preprocessed row (dict). The output row must contain the
required fields for the following processing stages.
postprocess: An optional lambda function that takes a row (dict) as input
and returns a postprocessed row (dict).
preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
preprocess stage (e.g., num_cpus, memory, concurrency).
postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the
postprocess stage (e.g., num_cpus, memory, concurrency).
telemetry_agent: An optional telemetry agent for collecting usage telemetry.
Returns:
The constructed processor.
"""
ray.init(runtime_env=config.runtime_env, ignore_reinit_error=True)
stages = []
# Prepare processor defaults for merging into stage configs
trust_remote_code = config.engine_kwargs.get("trust_remote_code", False)
processor_defaults = {
"batch_size": config.batch_size,
"concurrency": config.concurrency,
"runtime_env": config.runtime_env,
"model_source": config.model_source,
}
# Resolve and build PrepareMultimodalStage if enabled.
prepare_multimodal_stage_cfg = resolve_stage_config(
config.prepare_multimodal_stage,
PrepareMultimodalStageConfig,
processor_defaults,
)
if prepare_multimodal_stage_cfg.enabled:
base_model_config_kwargs = (
prepare_multimodal_stage_cfg.model_config_kwargs or {}
)
# Respect the model source from the processor
model_config_kwargs = {
**base_model_config_kwargs,
"model": processor_defaults.get("model_source"),
}
stages.append(
PrepareMultimodalStage(
fn_constructor_kwargs=dict(
model_config_kwargs=model_config_kwargs,
chat_template_content_format=prepare_multimodal_stage_cfg.chat_template_content_format,
apply_sys_msg_formatting=prepare_multimodal_stage_cfg.apply_sys_msg_formatting,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(
prepare_multimodal_stage_cfg
),
)
)
# Resolve and build ChatTemplateStage if enabled
chat_template_stage_cfg = resolve_stage_config(
getattr(config, "chat_template_stage", config.apply_chat_template),
ChatTemplateStageConfig,
processor_defaults,
)
if chat_template_stage_cfg.enabled:
stages.append(
ChatTemplateStage(
fn_constructor_kwargs=dict(
model=chat_template_stage_cfg.model_source,
chat_template=get_value_or_fallback(
chat_template_stage_cfg.chat_template, config.chat_template
),
chat_template_kwargs=get_value_or_fallback(
chat_template_stage_cfg.chat_template_kwargs,
chat_template_kwargs,
),
trust_remote_code=trust_remote_code,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(chat_template_stage_cfg),
)
)
# Resolve and build TokenizeStage if enabled
tokenize_stage_cfg = resolve_stage_config(
getattr(config, "tokenize_stage", config.tokenize),
TokenizerStageConfig,
processor_defaults,
)
if tokenize_stage_cfg.enabled:
stages.append(
TokenizeStage(
fn_constructor_kwargs=dict(
model=tokenize_stage_cfg.model_source,
trust_remote_code=trust_remote_code,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(tokenize_stage_cfg),
)
)
# Core stage -- the vLLM engine.
stages.append(
vLLMEngineStage(
fn_constructor_kwargs=dict(
batch_size=config.batch_size,
max_concurrent_batches=config.max_concurrent_batches,
model=config.model_source,
engine_kwargs=config.engine_kwargs,
task_type=config.task_type,
max_pending_requests=config.max_pending_requests,
dynamic_lora_loading_path=config.dynamic_lora_loading_path,
placement_group_config=config.placement_group_config,
should_continue_on_error=config.should_continue_on_error,
log_engine_metrics=config.log_engine_metrics,
),
map_batches_kwargs=dict(
zero_copy_batch=True,
# The number of running replicas. This is a deprecated field, but
# we need to set `max_tasks_in_flight_per_actor` through `compute`,
# which initiates enough many overlapping UDF calls per actor, to
# saturate `max_concurrency`.
compute=ray.data.ActorPoolStrategy(
**config.get_concurrency(autoscaling_enabled=True),
max_tasks_in_flight_per_actor=config.max_tasks_in_flight_per_actor,
),
# The number of running batches "per actor" in Ray Core level.
# This is used to make sure we overlap batches to avoid the tail
# latency of each batch.
max_concurrency=config.max_concurrent_batches,
accelerator_type=config.accelerator_type,
runtime_env=config.runtime_env,
),
)
)
# Resolve and build DetokenizeStage if enabled
detokenize_stage_cfg = resolve_stage_config(
getattr(config, "detokenize_stage", config.detokenize),
DetokenizeStageConfig,
processor_defaults,
)
if detokenize_stage_cfg.enabled:
stages.append(
DetokenizeStage(
fn_constructor_kwargs=dict(
model=detokenize_stage_cfg.model_source,
trust_remote_code=trust_remote_code,
),
map_batches_kwargs=build_cpu_stage_map_kwargs(detokenize_stage_cfg),
)
)
# We download the config files here so that we can report the underlying architecture to the telemetry system.
# This should be a lightweight operation.
# Use EXCLUDE_SAFETENSORS for streaming formats or trust_remote_code models,
# since custom model architectures require Python config files to be downloaded.
if config.engine_kwargs.get(
"load_format", None
) in STREAMING_LOAD_FORMATS or config.engine_kwargs.get("trust_remote_code", False):
download_model_mode = NodeModelDownloadable.EXCLUDE_SAFETENSORS
else:
download_model_mode = NodeModelDownloadable.TOKENIZER_ONLY
model_path = download_model_files(
model_id=config.model_source,
mirror_config=None,
download_model=download_model_mode,
download_extra_files=False,
)
try:
hf_config = transformers.AutoConfig.from_pretrained(
model_path,
trust_remote_code=config.engine_kwargs.get("trust_remote_code", False),
)
except Exception:
# Failed to retrieve HuggingFace config for telemetry purposes.
# This is non-fatal: we fall back to DEFAULT_MODEL_ARCHITECTURE for telemetry.
# The actual model loading happens later in vLLM, which may support models
# that aren't available via HuggingFace's AutoConfig.
logger.warning(
f"Failed to retrieve HuggingFace config for {config.model_source}"
)
hf_config = None
architectures = getattr(hf_config, "architectures", [])
architecture = architectures[0] if architectures else DEFAULT_MODEL_ARCHITECTURE
telemetry_agent = get_or_create_telemetry_agent()
telemetry_agent.push_telemetry_report(
BatchModelTelemetry(
model_id_hash=hashlib.sha256(
config.model_source.encode("utf-8")
).hexdigest(),
processor_config_name=type(config).__name__,
model_architecture=architecture,
batch_size=config.batch_size,
accelerator_type=config.accelerator_type or DEFAULT_GPU_TYPE,
concurrency=config.concurrency,
task_type=config.task_type,
pipeline_parallel_size=config.engine_kwargs.get(
"pipeline_parallel_size", 1
),
tensor_parallel_size=config.engine_kwargs.get("tensor_parallel_size", 1),
data_parallel_size=config.engine_kwargs.get("data_parallel_size", 1),
)
)
processor = Processor(
config,
stages,
preprocess=preprocess,
postprocess=postprocess,
preprocess_map_kwargs=preprocess_map_kwargs,
postprocess_map_kwargs=postprocess_map_kwargs,
)
return processor
ProcessorBuilder.register(vLLMEngineProcessorConfig, build_vllm_engine_processor)