chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from ray.llm._internal.batch.processor.vllm_engine_proc import vLLMEngineProcessorConfig
|
||||
from ray.llm._internal.batch.stages.configs import (
|
||||
ChatTemplateStageConfig,
|
||||
DetokenizeStageConfig,
|
||||
PrepareMultimodalStageConfig,
|
||||
TokenizerStageConfig,
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_booleans_coerced_to_stage_configs():
|
||||
"""Legacy flags → stage configs (dict form)."""
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="test-model",
|
||||
apply_chat_template=True,
|
||||
tokenize=False,
|
||||
detokenize=True,
|
||||
)
|
||||
|
||||
# Legacy flags should be coerced to stage configs
|
||||
assert isinstance(config.chat_template_stage, dict)
|
||||
assert config.chat_template_stage["enabled"] is True
|
||||
|
||||
assert isinstance(config.tokenize_stage, dict)
|
||||
assert config.tokenize_stage["enabled"] is False
|
||||
|
||||
assert isinstance(config.detokenize_stage, dict)
|
||||
assert config.detokenize_stage["enabled"] is True
|
||||
|
||||
|
||||
def test_explicit_stage_configs_preserved():
|
||||
"""Explicit stage configs not overwritten by legacy flags."""
|
||||
explicit_chat_template = ChatTemplateStageConfig(enabled=False, batch_size=64)
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="test-model",
|
||||
chat_template_stage=explicit_chat_template,
|
||||
apply_chat_template=True, # Legacy flag should be ignored
|
||||
)
|
||||
|
||||
# Explicit stage config should be preserved
|
||||
assert config.chat_template_stage is explicit_chat_template
|
||||
assert config.chat_template_stage.enabled is False
|
||||
assert config.chat_template_stage.batch_size == 64
|
||||
|
||||
|
||||
def test_chat_template_fields_merged():
|
||||
"""apply_chat_template + chat_template → merged into stage config."""
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="test-model",
|
||||
apply_chat_template=True,
|
||||
chat_template="custom_template",
|
||||
)
|
||||
|
||||
assert isinstance(config.chat_template_stage, dict)
|
||||
assert config.chat_template_stage["enabled"] is True
|
||||
assert config.chat_template_stage["chat_template"] == "custom_template"
|
||||
|
||||
|
||||
def test_no_warnings_when_using_new_api():
|
||||
"""No warnings when only new API used."""
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
vLLMEngineProcessorConfig(
|
||||
model_source="test-model",
|
||||
chat_template_stage=ChatTemplateStageConfig(enabled=True),
|
||||
tokenize_stage=TokenizerStageConfig(enabled=True),
|
||||
detokenize_stage=DetokenizeStageConfig(enabled=True),
|
||||
prepare_multimodal_stage=PrepareMultimodalStageConfig(enabled=False),
|
||||
)
|
||||
# Filter out any non-UserWarning warnings
|
||||
deprecation_warnings = [
|
||||
warning for warning in w if issubclass(warning.category, UserWarning)
|
||||
]
|
||||
assert len(deprecation_warnings) == 0
|
||||
|
||||
|
||||
def test_legacy_dict_stage_config():
|
||||
"""Dict form stage configs work correctly."""
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="test-model",
|
||||
chat_template_stage={"enabled": False, "batch_size": 128},
|
||||
tokenize_stage={"enabled": True, "concurrency": 4},
|
||||
)
|
||||
|
||||
assert isinstance(config.chat_template_stage, dict)
|
||||
assert config.chat_template_stage["enabled"] is False
|
||||
assert config.chat_template_stage["batch_size"] == 128
|
||||
|
||||
assert isinstance(config.tokenize_stage, dict)
|
||||
assert config.tokenize_stage["enabled"] is True
|
||||
assert config.tokenize_stage["concurrency"] == 4
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,39 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ray.data.llm import HttpRequestStageConfig
|
||||
from ray.llm._internal.batch.processor import ProcessorBuilder
|
||||
from ray.llm._internal.batch.processor.http_request_proc import (
|
||||
HttpRequestProcessorConfig,
|
||||
)
|
||||
|
||||
|
||||
def test_http_request_processor():
|
||||
config = HttpRequestProcessorConfig(
|
||||
url="http://localhost:8000",
|
||||
headers={"Authorization": "Bearer 1234567890"},
|
||||
qps=2,
|
||||
concurrency=4,
|
||||
batch_size=64,
|
||||
http_request_stage=HttpRequestStageConfig(
|
||||
num_cpus=0.5,
|
||||
memory=100000,
|
||||
),
|
||||
)
|
||||
processor = ProcessorBuilder.build(config)
|
||||
assert processor.list_stage_names() == ["HttpRequestStage"]
|
||||
stage = processor.get_stage_by_name("HttpRequestStage")
|
||||
assert stage.map_batches_kwargs["num_cpus"] == 0.5
|
||||
assert stage.map_batches_kwargs["memory"] == 100000
|
||||
assert stage.map_batches_kwargs["compute"].min_size == 1
|
||||
assert stage.map_batches_kwargs["compute"].max_size == 4
|
||||
assert stage.fn_constructor_kwargs["url"] == "http://localhost:8000"
|
||||
assert stage.fn_constructor_kwargs["additional_header"] == {
|
||||
"Authorization": "Bearer 1234567890"
|
||||
}
|
||||
assert stage.fn_constructor_kwargs["qps"] == 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Regression tests for lazy imports in ``ray.llm._internal.batch``.
|
||||
|
||||
The ``stages`` and ``processor`` packages re-export classes whose defining
|
||||
modules pull in heavy optional dependencies (``transformers``, ``vllm``,
|
||||
``sglang``, ``mistral_common``). They are wired up via PEP 562
|
||||
``__getattr__`` so that, e.g., importing ``HttpRequestProcessorConfig`` does
|
||||
not drag the entire ML stack into ``sys.modules``.
|
||||
|
||||
These tests run in a fresh Python subprocess so that ``sys.modules`` is
|
||||
guaranteed to be clean -- otherwise the modules-under-test could already be
|
||||
loaded by an earlier test.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _run_in_subprocess(script: str) -> str:
|
||||
"""Run ``script`` in a clean Python subprocess and return stdout."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", textwrap.dedent(script)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
# Module names that the lightweight HTTP processor must NOT pull in.
|
||||
# Each one is loaded by a different stage / processor module, so seeing any
|
||||
# of them after importing the HTTP processor means the lazy wiring broke.
|
||||
_HEAVY_MODULES = (
|
||||
"transformers",
|
||||
"tokenizers",
|
||||
"huggingface_hub",
|
||||
"mistral_common",
|
||||
"vllm.transformers_utils",
|
||||
# Stage submodules that pull in the heavy deps above.
|
||||
"ray.llm._internal.batch.stages.tokenize_stage",
|
||||
"ray.llm._internal.batch.stages.chat_template_stage",
|
||||
"ray.llm._internal.batch.stages.vllm_engine_stage",
|
||||
"ray.llm._internal.batch.stages.sglang_engine_stage",
|
||||
"ray.llm._internal.batch.stages.prepare_multimodal_stage",
|
||||
"ray.llm._internal.batch.stages.serve_deployment_stage",
|
||||
# Processor submodules whose top-level statements import heavy deps
|
||||
# directly (e.g. ``import transformers`` in sglang_engine_proc.py).
|
||||
"ray.llm._internal.batch.processor.sglang_engine_proc",
|
||||
"ray.llm._internal.batch.processor.vllm_engine_proc",
|
||||
"ray.llm._internal.batch.processor.serve_deployment_proc",
|
||||
)
|
||||
|
||||
|
||||
def test_http_request_processor_does_not_import_heavy_deps():
|
||||
"""HTTP-only processor imports must not load transformers/vllm/etc."""
|
||||
out = _run_in_subprocess(
|
||||
f"""
|
||||
import sys
|
||||
from ray.llm._internal.batch import HttpRequestProcessorConfig # noqa: F401
|
||||
|
||||
heavy = {_HEAVY_MODULES!r}
|
||||
loaded = [m for m in heavy if m in sys.modules]
|
||||
print(','.join(loaded))
|
||||
"""
|
||||
)
|
||||
loaded = [m for m in out.strip().split(",") if m]
|
||||
assert loaded == [], (
|
||||
"Importing HttpRequestProcessorConfig must not pull in heavy ML "
|
||||
f"dependencies, but the following modules ended up loaded: {loaded}"
|
||||
)
|
||||
|
||||
|
||||
def test_http_request_stage_only_loads_its_own_submodule():
|
||||
"""``from ...stages import HttpRequestStage`` must only load that stage."""
|
||||
out = _run_in_subprocess(
|
||||
"""
|
||||
import sys
|
||||
from ray.llm._internal.batch.stages import HttpRequestStage # noqa: F401
|
||||
|
||||
stage_modules = sorted(
|
||||
m for m in sys.modules
|
||||
if m.startswith('ray.llm._internal.batch.stages.')
|
||||
and m != 'ray.llm._internal.batch.stages.base'
|
||||
and m != 'ray.llm._internal.batch.stages.configs'
|
||||
and m != 'ray.llm._internal.batch.stages.common'
|
||||
)
|
||||
print(','.join(stage_modules))
|
||||
"""
|
||||
)
|
||||
loaded = [m for m in out.strip().split(",") if m]
|
||||
assert loaded == [
|
||||
"ray.llm._internal.batch.stages.http_request_stage"
|
||||
], f"Expected only http_request_stage to load, got: {loaded}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,submodule",
|
||||
[
|
||||
("HttpRequestStage", "http_request_stage"),
|
||||
("TokenizeStage", "tokenize_stage"),
|
||||
("DetokenizeStage", "tokenize_stage"),
|
||||
("ChatTemplateStage", "chat_template_stage"),
|
||||
("PrepareMultimodalStage", "prepare_multimodal_stage"),
|
||||
("ServeDeploymentStage", "serve_deployment_stage"),
|
||||
("SGLangEngineStage", "sglang_engine_stage"),
|
||||
("vLLMEngineStage", "vllm_engine_stage"),
|
||||
],
|
||||
)
|
||||
def test_stage_lazy_attr_resolves(name, submodule):
|
||||
"""Each lazy stage attr resolves to the class from the right submodule."""
|
||||
import ray.llm._internal.batch.stages as stages
|
||||
|
||||
cls = getattr(stages, name)
|
||||
assert cls.__name__ == name
|
||||
assert cls.__module__ == f"ray.llm._internal.batch.stages.{submodule}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,submodule",
|
||||
[
|
||||
("HttpRequestProcessorConfig", "http_request_proc"),
|
||||
("ServeDeploymentProcessorConfig", "serve_deployment_proc"),
|
||||
("SGLangEngineProcessorConfig", "sglang_engine_proc"),
|
||||
("vLLMEngineProcessorConfig", "vllm_engine_proc"),
|
||||
],
|
||||
)
|
||||
def test_processor_lazy_attr_resolves(name, submodule):
|
||||
"""Each lazy processor-config attr resolves to the right class."""
|
||||
import ray.llm._internal.batch.processor as processor
|
||||
|
||||
cls = getattr(processor, name)
|
||||
assert cls.__name__ == name
|
||||
assert cls.__module__ == f"ray.llm._internal.batch.processor.{submodule}"
|
||||
|
||||
|
||||
def test_unknown_attr_raises_attribute_error():
|
||||
"""``__getattr__`` must raise ``AttributeError`` for unknown names so
|
||||
that ``hasattr`` and other attribute-introspection paths behave correctly.
|
||||
"""
|
||||
import ray.llm._internal.batch.processor as processor
|
||||
import ray.llm._internal.batch.stages as stages
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
processor.DefinitelyNotAProcessor # noqa: B018
|
||||
with pytest.raises(AttributeError):
|
||||
stages.DefinitelyNotAStage # noqa: B018
|
||||
|
||||
|
||||
def test_dir_lists_lazy_attrs():
|
||||
"""``dir(pkg)`` must list the lazy attributes (for IDE completion etc.)."""
|
||||
import ray.llm._internal.batch.processor as processor
|
||||
import ray.llm._internal.batch.stages as stages
|
||||
|
||||
for name in (
|
||||
"HttpRequestProcessorConfig",
|
||||
"ServeDeploymentProcessorConfig",
|
||||
"SGLangEngineProcessorConfig",
|
||||
"vLLMEngineProcessorConfig",
|
||||
):
|
||||
assert name in dir(processor)
|
||||
for name in (
|
||||
"HttpRequestStage",
|
||||
"TokenizeStage",
|
||||
"DetokenizeStage",
|
||||
"ChatTemplateStage",
|
||||
"PrepareMultimodalStage",
|
||||
"ServeDeploymentStage",
|
||||
"SGLangEngineStage",
|
||||
"vLLMEngineStage",
|
||||
):
|
||||
assert name in dir(stages)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-vv", __file__]))
|
||||
@@ -0,0 +1,595 @@
|
||||
import sys
|
||||
from typing import Any, AsyncIterator, Dict, List, Type
|
||||
from unittest.mock import patch
|
||||
|
||||
import pydantic
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.data.llm import build_processor
|
||||
from ray.llm._internal.batch.processor import (
|
||||
base as processor_base,
|
||||
vLLMEngineProcessorConfig,
|
||||
)
|
||||
from ray.llm._internal.batch.processor.base import (
|
||||
Processor,
|
||||
ProcessorBuilder,
|
||||
ProcessorConfig,
|
||||
)
|
||||
from ray.llm._internal.batch.stages.base import StatefulStage, StatefulStageUDF
|
||||
|
||||
|
||||
def test_empty_processor():
|
||||
"""Test processor with only preprocess and postprocess."""
|
||||
|
||||
processor = Processor(
|
||||
config=ProcessorConfig(
|
||||
batch_size=64,
|
||||
accelerator_type=None,
|
||||
concurrency=1,
|
||||
),
|
||||
stages=[],
|
||||
# {id} -> {__data: {id, val}}
|
||||
preprocess=lambda row: {"val": row["id"] + 5},
|
||||
# {__data: {id, val}} -> {id, result}
|
||||
postprocess=lambda row: {"result": row["val"], "id": row["id"]},
|
||||
)
|
||||
|
||||
ds = ray.data.range(5)
|
||||
ds = processor(ds).take_all()
|
||||
for row in ds:
|
||||
assert "val" not in row
|
||||
assert "id" in row
|
||||
assert "result" in row
|
||||
|
||||
|
||||
def test_processor_with_no_preprocess_or_postprocess():
|
||||
"""Test processor with no preprocess or postprocess."""
|
||||
|
||||
processor = Processor(
|
||||
config=ProcessorConfig(
|
||||
batch_size=64,
|
||||
accelerator_type=None,
|
||||
concurrency=1,
|
||||
),
|
||||
stages=[],
|
||||
)
|
||||
|
||||
ds = ray.data.range(5)
|
||||
ds = processor(ds).take_all()
|
||||
for row in ds:
|
||||
assert "id" in row
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_extra", [True, False])
|
||||
def test_processor_with_stages(has_extra: bool):
|
||||
"""Test processor with multiple stages."""
|
||||
|
||||
class DummyStatefulStageUDF(StatefulStageUDF):
|
||||
def __init__(
|
||||
self,
|
||||
data_column: str,
|
||||
expected_input_keys: List[str],
|
||||
factor: int,
|
||||
):
|
||||
super().__init__(data_column, expected_input_keys)
|
||||
self.factor = factor
|
||||
|
||||
async def udf(
|
||||
self, batch: List[Dict[str, Any]]
|
||||
) -> AsyncIterator[Dict[str, Any]]:
|
||||
for row in batch:
|
||||
answer = row["val"] * self.factor
|
||||
if "extra" in row: # Optional input column.
|
||||
answer += row["extra"]
|
||||
yield {
|
||||
# Use the same name to chain multiple dummy stages.
|
||||
"val": answer,
|
||||
self.IDX_IN_BATCH_COLUMN: row[self.IDX_IN_BATCH_COLUMN],
|
||||
}
|
||||
|
||||
class DummyStage(StatefulStage):
|
||||
fn: Type[StatefulStageUDF] = DummyStatefulStageUDF
|
||||
fn_constructor_kwargs: Dict[str, Any] = {}
|
||||
map_batches_kwargs: Dict[str, Any] = dict(concurrency=1)
|
||||
|
||||
def get_required_input_keys(self) -> Dict[str, str]:
|
||||
return {"val": "The value to multiply."}
|
||||
|
||||
stages = [
|
||||
DummyStage(fn_constructor_kwargs=dict(factor=2)),
|
||||
DummyStage(fn_constructor_kwargs=dict(factor=3)),
|
||||
]
|
||||
|
||||
processor = Processor(
|
||||
config=ProcessorConfig(
|
||||
accelerator_type=None,
|
||||
concurrency=1,
|
||||
batch_size=64,
|
||||
),
|
||||
stages=stages,
|
||||
preprocess=lambda row: {"val": row["id"]},
|
||||
postprocess=lambda row: {"result": row["val"], "id": row["id"]},
|
||||
)
|
||||
|
||||
# Check the stage names.
|
||||
stage_names = processor.list_stage_names()
|
||||
assert stage_names == [
|
||||
"DummyStage",
|
||||
"DummyStage_1",
|
||||
]
|
||||
|
||||
# Check the stages.
|
||||
for stage_name, stage in zip(stage_names, stages):
|
||||
assert processor.get_stage_by_name(stage_name) == stage
|
||||
|
||||
# Run the processor twice with different datasets to test
|
||||
# whether the processor is reusable.
|
||||
for _ in range(2):
|
||||
ds = ray.data.range(5)
|
||||
ds = ds.map(
|
||||
lambda row: {
|
||||
"id": row["id"],
|
||||
**({"extra": 1} if has_extra else {}),
|
||||
}
|
||||
)
|
||||
|
||||
ds = processor(ds).take_all()
|
||||
extra = 1 if has_extra else 0
|
||||
for row in ds:
|
||||
assert "id" in row
|
||||
assert "result" in row
|
||||
|
||||
# The final output should be the result of the last stage.
|
||||
assert row["result"] == (row["id"] * 2 + extra) * 3 + extra
|
||||
|
||||
|
||||
# Common dummy classes for testing
|
||||
class DummyStatefulStageUDF(StatefulStageUDF):
|
||||
async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]:
|
||||
for row in batch:
|
||||
yield row
|
||||
|
||||
|
||||
class DummyStage(StatefulStage):
|
||||
fn: Type[StatefulStageUDF] = DummyStatefulStageUDF
|
||||
fn_constructor_kwargs: Dict[str, Any] = {}
|
||||
map_batches_kwargs: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class DummyProcessorConfig(ProcessorConfig):
|
||||
pass
|
||||
|
||||
|
||||
def test_builder():
|
||||
def build_processor(config: ProcessorConfig) -> Processor:
|
||||
stages = [
|
||||
DummyStage(
|
||||
fn_constructor_kwargs=dict(),
|
||||
map_batches_kwargs=dict(concurrency=1),
|
||||
)
|
||||
]
|
||||
processor = Processor(config, stages)
|
||||
return processor
|
||||
|
||||
ProcessorBuilder.register(DummyProcessorConfig, build_processor)
|
||||
|
||||
processor = ProcessorBuilder.build(DummyProcessorConfig(batch_size=64))
|
||||
assert isinstance(processor.config, DummyProcessorConfig)
|
||||
assert processor.list_stage_names() == ["DummyStage"]
|
||||
assert (
|
||||
processor.get_stage_by_name("DummyStage").map_batches_kwargs["concurrency"] == 1
|
||||
)
|
||||
|
||||
def overrider(name: str, stage: StatefulStage):
|
||||
if name.startswith("DummyStage"):
|
||||
stage.map_batches_kwargs["concurrency"] = 2
|
||||
|
||||
processor = ProcessorBuilder.build(
|
||||
DummyProcessorConfig(batch_size=64),
|
||||
override_stage_config_fn=overrider,
|
||||
)
|
||||
assert processor.list_stage_names() == ["DummyStage"]
|
||||
assert (
|
||||
processor.get_stage_by_name("DummyStage").map_batches_kwargs["concurrency"] == 2
|
||||
)
|
||||
|
||||
|
||||
class TestBuilderKwargsValidation:
|
||||
@pytest.fixture
|
||||
def build_processor_with_kwargs(self):
|
||||
def build_processor_with_kwargs(
|
||||
config: ProcessorConfig,
|
||||
preprocess=None,
|
||||
postprocess=None,
|
||||
preprocess_map_kwargs=None,
|
||||
postprocess_map_kwargs=None,
|
||||
custom_kwarg=None,
|
||||
another_kwarg=None,
|
||||
) -> Processor:
|
||||
stages = [
|
||||
DummyStage(
|
||||
fn_constructor_kwargs=dict(
|
||||
custom_kwarg=custom_kwarg,
|
||||
another_kwarg=another_kwarg,
|
||||
),
|
||||
map_batches_kwargs=dict(concurrency=1),
|
||||
)
|
||||
]
|
||||
processor = Processor(
|
||||
config,
|
||||
stages,
|
||||
preprocess=preprocess,
|
||||
postprocess=postprocess,
|
||||
preprocess_map_kwargs=preprocess_map_kwargs,
|
||||
postprocess_map_kwargs=postprocess_map_kwargs,
|
||||
)
|
||||
return processor
|
||||
|
||||
return build_processor_with_kwargs
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_registry(self):
|
||||
ProcessorBuilder.clear_registry()
|
||||
|
||||
def test_builder_kwargs_passthrough(self, build_processor_with_kwargs):
|
||||
ProcessorBuilder.register(DummyProcessorConfig, build_processor_with_kwargs)
|
||||
|
||||
config = DummyProcessorConfig(batch_size=64)
|
||||
processor = build_processor(
|
||||
config,
|
||||
preprocess=lambda row: {"val": row["id"]},
|
||||
postprocess=lambda row: {"result": row["val"]},
|
||||
builder_kwargs=dict(
|
||||
custom_kwarg="test_value",
|
||||
another_kwarg=42,
|
||||
),
|
||||
)
|
||||
assert processor.list_stage_names() == ["DummyStage"]
|
||||
stage = processor.get_stage_by_name("DummyStage")
|
||||
assert stage.fn_constructor_kwargs["custom_kwarg"] == "test_value"
|
||||
assert stage.fn_constructor_kwargs["another_kwarg"] == 42
|
||||
|
||||
def test_unsupported_kwargs(self):
|
||||
def build_processor_no_kwargs(
|
||||
config: ProcessorConfig,
|
||||
preprocess=None,
|
||||
postprocess=None,
|
||||
) -> Processor:
|
||||
stages = []
|
||||
processor = Processor(
|
||||
config, stages, preprocess=preprocess, postprocess=postprocess
|
||||
)
|
||||
return processor
|
||||
|
||||
ProcessorBuilder.register(DummyProcessorConfig, build_processor_no_kwargs)
|
||||
|
||||
config = DummyProcessorConfig(batch_size=64)
|
||||
with pytest.raises(TypeError, match="unsupported_kwarg"):
|
||||
build_processor(
|
||||
config,
|
||||
builder_kwargs=dict(unsupported_kwarg="value"),
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("conflicting_key", ["preprocess", "postprocess"])
|
||||
def test_error_builder_kwargs_conflict(
|
||||
self, conflicting_key, build_processor_with_kwargs
|
||||
):
|
||||
ProcessorBuilder.register(DummyProcessorConfig, build_processor_with_kwargs)
|
||||
|
||||
config = DummyProcessorConfig(batch_size=64)
|
||||
with pytest.raises(ValueError, match="builder_kwargs cannot contain"):
|
||||
build_processor(
|
||||
config,
|
||||
preprocess=lambda row: {"val": row["id"]},
|
||||
builder_kwargs={conflicting_key: lambda row: {"other": row["id"]}},
|
||||
)
|
||||
|
||||
|
||||
class TestProcessorConfig:
|
||||
def test_valid_concurrency(self):
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
concurrency=(1, 2),
|
||||
)
|
||||
assert config.concurrency == (1, 2)
|
||||
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
)
|
||||
assert config.concurrency == 1
|
||||
|
||||
def test_invalid_concurrency(self):
|
||||
with pytest.raises(pydantic.ValidationError):
|
||||
vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
concurrency=1.1,
|
||||
)
|
||||
|
||||
with pytest.raises(pydantic.ValidationError):
|
||||
vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
concurrency=[1, 2, 3],
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("n", [1, 2, 10])
|
||||
def test_positive_int_not_fail(self, n):
|
||||
conf = ProcessorConfig(concurrency=n)
|
||||
assert conf.concurrency == n
|
||||
|
||||
def test_positive_int_unusual_not_fail(self):
|
||||
assert ProcessorConfig(concurrency="1").concurrency == 1
|
||||
assert ProcessorConfig(concurrency=1.0).concurrency == 1
|
||||
assert ProcessorConfig(concurrency="1.0").concurrency == 1
|
||||
|
||||
@pytest.mark.parametrize("pair", [(1, 1), (1, 2), (2, 8)])
|
||||
def test_valid_tuple_not_fail(self, pair):
|
||||
conf = ProcessorConfig(concurrency=pair)
|
||||
assert conf.concurrency == pair
|
||||
|
||||
def test_valid_tuple_unusual_not_fail(self):
|
||||
assert ProcessorConfig(concurrency=("1", 2)).concurrency == (1, 2)
|
||||
assert ProcessorConfig(concurrency=(1, "2")).concurrency == (1, 2)
|
||||
assert ProcessorConfig(concurrency=[1, "2"]).concurrency == (1, 2)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad,msg_part",
|
||||
[
|
||||
(0, "positive integer"),
|
||||
(-5, "positive integer"),
|
||||
((1, 2, 3), "at most 2 items"),
|
||||
((0, 1), "positive integers"),
|
||||
((1, 0), "positive integers"),
|
||||
((-1, 2), "positive integers"),
|
||||
((1, -2), "positive integers"),
|
||||
((1, 2.5), "a number with a fractional part"),
|
||||
("2.1", "unable to parse string"),
|
||||
((5, 2), "min > max"),
|
||||
],
|
||||
)
|
||||
def test_invalid_inputs_raise(self, bad, msg_part):
|
||||
with pytest.raises(pydantic.ValidationError) as e:
|
||||
ProcessorConfig(concurrency=bad)
|
||||
assert msg_part in str(e.value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"n,expected",
|
||||
[
|
||||
(1, {"min_size": 1, "max_size": 1}),
|
||||
(4, {"min_size": 1, "max_size": 4}),
|
||||
(10, {"min_size": 1, "max_size": 10}),
|
||||
("10", {"min_size": 1, "max_size": 10}),
|
||||
],
|
||||
)
|
||||
def test_with_int_concurrency_scaling(self, n, expected):
|
||||
conf = ProcessorConfig(concurrency=n)
|
||||
assert conf.get_concurrency() == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"n,expected",
|
||||
[
|
||||
(1, {"size": 1}),
|
||||
(4, {"size": 4}),
|
||||
(10, {"size": 10}),
|
||||
],
|
||||
)
|
||||
def test_with_int_concurrency_fixed(self, n, expected):
|
||||
conf = ProcessorConfig(concurrency=n)
|
||||
assert conf.get_concurrency(autoscaling_enabled=False) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pair,expected",
|
||||
[
|
||||
((1, 1), {"min_size": 1, "max_size": 1}),
|
||||
((1, 3), {"min_size": 1, "max_size": 3}),
|
||||
((2, 8), {"min_size": 2, "max_size": 8}),
|
||||
],
|
||||
)
|
||||
def test_with_tuple_concurrency(self, pair, expected):
|
||||
conf = ProcessorConfig(concurrency=pair)
|
||||
assert conf.get_concurrency() == expected
|
||||
|
||||
|
||||
class TestOfflineProcessorConfig:
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs, expected",
|
||||
[
|
||||
({"max_tasks_in_flight_per_actor": 10}, 10),
|
||||
({}, None),
|
||||
# Field stays None; the formula runs in Ray Data, not here.
|
||||
({"max_concurrent_batches": 4}, None),
|
||||
],
|
||||
)
|
||||
def test_max_tasks_in_flight_per_actor_passthrough(self, kwargs, expected):
|
||||
"""Field passes through to ActorPoolStrategy; None defers resolution."""
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
**kwargs,
|
||||
)
|
||||
assert config.max_tasks_in_flight_per_actor == expected
|
||||
assert config.max_concurrent_batches == kwargs.get("max_concurrent_batches", 8)
|
||||
|
||||
def test_experimental_max_tasks_in_flight_per_actor_deprecated(self):
|
||||
"""Setting `experimental['max_tasks_in_flight_per_actor']` migrates to
|
||||
the top-level field with a deprecation log; the explicit top-level
|
||||
field overrides it but the warning still fires."""
|
||||
|
||||
def has_deprecation_log(warning_mock):
|
||||
return any(
|
||||
"max_tasks_in_flight_per_actor" in call.args[0]
|
||||
and "deprecated" in call.args[0]
|
||||
for call in warning_mock.call_args_list
|
||||
)
|
||||
|
||||
# Migration: experimental → top-level field.
|
||||
with patch.object(processor_base.logger, "warning") as warning_mock:
|
||||
cfg = vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
experimental={"max_tasks_in_flight_per_actor": 10},
|
||||
)
|
||||
assert cfg.max_tasks_in_flight_per_actor == 10
|
||||
assert has_deprecation_log(warning_mock)
|
||||
|
||||
# Explicit top-level beats experimental, but warning still fires.
|
||||
with patch.object(processor_base.logger, "warning") as warning_mock:
|
||||
cfg = vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
max_tasks_in_flight_per_actor=20,
|
||||
experimental={"max_tasks_in_flight_per_actor": 10},
|
||||
)
|
||||
assert cfg.max_tasks_in_flight_per_actor == 20
|
||||
assert has_deprecation_log(warning_mock)
|
||||
|
||||
def test_max_tasks_in_flight_under_max_concurrent_batches_warns(self):
|
||||
with patch.object(processor_base.logger, "warning") as warning_mock:
|
||||
cfg = vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
max_tasks_in_flight_per_actor=1,
|
||||
max_concurrent_batches=8,
|
||||
)
|
||||
|
||||
assert cfg.max_tasks_in_flight_per_actor == 1
|
||||
assert cfg.max_concurrent_batches == 8
|
||||
warning_messages = [call.args[0] for call in warning_mock.call_args_list]
|
||||
assert any(
|
||||
"max_tasks_in_flight_per_actor" in message
|
||||
and "max_concurrent_batches" in message
|
||||
and "underutilize" in message
|
||||
for message in warning_messages
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs",
|
||||
[
|
||||
{},
|
||||
{"max_tasks_in_flight_per_actor": 8, "max_concurrent_batches": 8},
|
||||
{"max_tasks_in_flight_per_actor": 16, "max_concurrent_batches": 8},
|
||||
],
|
||||
)
|
||||
def test_max_tasks_in_flight_does_not_warn_when_not_underutilized(self, kwargs):
|
||||
with patch.object(processor_base.logger, "warning") as warning_mock:
|
||||
vLLMEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.2-1B-Instruct",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
warning_messages = [call.args[0] for call in warning_mock.call_args_list]
|
||||
assert not any("underutilize" in message for message in warning_messages)
|
||||
|
||||
|
||||
class TestMapKwargs:
|
||||
"""Tests for preprocess_map_kwargs and postprocess_map_kwargs."""
|
||||
|
||||
def test_map_kwargs_stored_in_processor(self):
|
||||
"""Test that map kwargs are correctly stored in Processor."""
|
||||
preprocess_kwargs = {"num_cpus": 0.5}
|
||||
postprocess_kwargs = {"num_cpus": 0.25, "memory": 1024}
|
||||
|
||||
processor = Processor(
|
||||
config=ProcessorConfig(batch_size=64),
|
||||
stages=[],
|
||||
preprocess=lambda row: {"val": row["id"]},
|
||||
postprocess=lambda row: {"result": row["val"]},
|
||||
preprocess_map_kwargs=preprocess_kwargs,
|
||||
postprocess_map_kwargs=postprocess_kwargs,
|
||||
)
|
||||
|
||||
assert processor.preprocess_map_kwargs == preprocess_kwargs
|
||||
assert processor.postprocess_map_kwargs == postprocess_kwargs
|
||||
|
||||
def test_map_kwargs_defaults_to_empty_dict(self):
|
||||
"""Test that map kwargs default to empty dict when None."""
|
||||
processor = Processor(
|
||||
config=ProcessorConfig(batch_size=64),
|
||||
stages=[],
|
||||
)
|
||||
|
||||
assert processor.preprocess_map_kwargs == {}
|
||||
assert processor.postprocess_map_kwargs == {}
|
||||
|
||||
def test_map_kwargs_passthrough_via_builder(self):
|
||||
"""Test that map kwargs are passed through ProcessorBuilder."""
|
||||
|
||||
def build_processor_simple(
|
||||
config: ProcessorConfig,
|
||||
preprocess=None,
|
||||
postprocess=None,
|
||||
preprocess_map_kwargs=None,
|
||||
postprocess_map_kwargs=None,
|
||||
) -> Processor:
|
||||
return Processor(
|
||||
config,
|
||||
[],
|
||||
preprocess=preprocess,
|
||||
postprocess=postprocess,
|
||||
preprocess_map_kwargs=preprocess_map_kwargs,
|
||||
postprocess_map_kwargs=postprocess_map_kwargs,
|
||||
)
|
||||
|
||||
ProcessorBuilder.clear_registry()
|
||||
ProcessorBuilder.register(DummyProcessorConfig, build_processor_simple)
|
||||
|
||||
config = DummyProcessorConfig(batch_size=64)
|
||||
# Test through ProcessorBuilder which is called by build_processor
|
||||
processor = ProcessorBuilder.build(
|
||||
config,
|
||||
preprocess=lambda row: {"val": row["id"]},
|
||||
postprocess=lambda row: {"result": row["val"]},
|
||||
preprocess_map_kwargs={"num_cpus": 0.5},
|
||||
postprocess_map_kwargs={"num_cpus": 0.25},
|
||||
)
|
||||
|
||||
assert processor.preprocess_map_kwargs == {"num_cpus": 0.5}
|
||||
assert processor.postprocess_map_kwargs == {"num_cpus": 0.25}
|
||||
|
||||
def test_builder_kwargs_conflict_with_map_kwargs(self):
|
||||
"""Test that builder_kwargs validation rejects map kwargs."""
|
||||
# Test the validation that build_processor calls
|
||||
with pytest.raises(ValueError, match="builder_kwargs cannot contain"):
|
||||
ProcessorBuilder.validate_builder_kwargs(
|
||||
{"preprocess_map_kwargs": {"num_cpus": 0.5}}
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="builder_kwargs cannot contain"):
|
||||
ProcessorBuilder.validate_builder_kwargs(
|
||||
{"postprocess_map_kwargs": {"num_cpus": 0.5}}
|
||||
)
|
||||
|
||||
def test_end_to_end_with_map_kwargs(self):
|
||||
"""Test end-to-end execution with map kwargs."""
|
||||
processor = Processor(
|
||||
config=ProcessorConfig(batch_size=64),
|
||||
stages=[],
|
||||
preprocess=lambda row: {"val": row["id"] * 2},
|
||||
postprocess=lambda row: {"result": row["val"] + 1, "id": row["id"]},
|
||||
preprocess_map_kwargs={"num_cpus": 0.5},
|
||||
postprocess_map_kwargs={"num_cpus": 0.25},
|
||||
)
|
||||
|
||||
ds = ray.data.range(5)
|
||||
result = processor(ds).take_all()
|
||||
|
||||
for row in result:
|
||||
# Verify the computation: val = id * 2, result = val + 1
|
||||
assert row["result"] == row["id"] * 2 + 1
|
||||
|
||||
def test_backward_compatibility_without_map_kwargs(self):
|
||||
"""Test that existing code without map kwargs still works."""
|
||||
processor = Processor(
|
||||
config=ProcessorConfig(batch_size=64),
|
||||
stages=[],
|
||||
preprocess=lambda row: {"val": row["id"]},
|
||||
postprocess=lambda row: {"result": row["val"]},
|
||||
)
|
||||
|
||||
ds = ray.data.range(5)
|
||||
result = processor(ds).take_all()
|
||||
# Sort results by result value since order is not guaranteed
|
||||
result = sorted(result, key=lambda x: x["result"])
|
||||
|
||||
for i, row in enumerate(result):
|
||||
assert row["result"] == i
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Test that Ray Data LLM does not override wait_for_min_actors_s.
|
||||
|
||||
With default settings (wait_for_min_actors_s <= 0), processing starts
|
||||
as soon as any actor is ready, regardless of concurrency config.
|
||||
"""
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ray.data import DataContext
|
||||
from ray.llm._internal.batch.processor import ProcessorBuilder
|
||||
from ray.llm._internal.batch.processor.vllm_engine_proc import vLLMEngineProcessorConfig
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_data_context():
|
||||
"""Reset DataContext before and after each test."""
|
||||
ctx = DataContext.get_current()
|
||||
original_value = ctx.wait_for_min_actors_s
|
||||
ctx.wait_for_min_actors_s = -1
|
||||
yield
|
||||
ctx.wait_for_min_actors_s = original_value
|
||||
|
||||
|
||||
class TestWaitForMinActorsNotOverridden:
|
||||
"""Test that Processor does not override wait_for_min_actors_s."""
|
||||
|
||||
def test_processor_does_not_override_default(self):
|
||||
"""Processor should not change wait_for_min_actors_s from default."""
|
||||
ctx = DataContext.get_current()
|
||||
ctx.wait_for_min_actors_s = -1
|
||||
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="facebook/opt-125m",
|
||||
concurrency=4,
|
||||
)
|
||||
ProcessorBuilder.build(config)
|
||||
|
||||
assert ctx.wait_for_min_actors_s == -1
|
||||
|
||||
@pytest.mark.parametrize("user_value", [60, 600, 1800])
|
||||
def test_processor_preserves_user_setting(self, user_value):
|
||||
"""Processor should preserve user-set wait_for_min_actors_s."""
|
||||
ctx = DataContext.get_current()
|
||||
ctx.wait_for_min_actors_s = user_value
|
||||
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="facebook/opt-125m",
|
||||
concurrency=4,
|
||||
)
|
||||
ProcessorBuilder.build(config)
|
||||
|
||||
assert ctx.wait_for_min_actors_s == user_value
|
||||
|
||||
|
||||
class TestConcurrencyConfigPassthrough:
|
||||
"""
|
||||
Test that concurrency config correctly sets ActorPoolStrategy.
|
||||
|
||||
This determines blocking behavior when wait_for_min_actors_s > 0:
|
||||
- concurrency=N → min_size=1, max_size=N → blocks for 1 actor (autoscaling)
|
||||
- concurrency=(m, N) → min_size=m, max_size=N → blocks for m actors
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"concurrency,expected_min_size,expected_max_size",
|
||||
[
|
||||
(4, 1, 4), # int: autoscaling pool with min_size=1
|
||||
((1, 4), 1, 4), # tuple: autoscaling pool
|
||||
((2, 8), 2, 8), # tuple: custom min
|
||||
],
|
||||
ids=["int_concurrency", "tuple_1_to_n", "tuple_custom_min"],
|
||||
)
|
||||
def test_concurrency_to_actor_pool_strategy(
|
||||
self, concurrency, expected_min_size, expected_max_size
|
||||
):
|
||||
"""Verify concurrency config maps to correct ActorPoolStrategy."""
|
||||
config = vLLMEngineProcessorConfig(
|
||||
model_source="facebook/opt-125m",
|
||||
concurrency=concurrency,
|
||||
)
|
||||
processor = ProcessorBuilder.build(config)
|
||||
|
||||
# Get the vLLM stage and check its compute strategy
|
||||
stage = processor.get_stage_by_name("vLLMEngineStage")
|
||||
compute = stage.map_batches_kwargs.get("compute")
|
||||
|
||||
assert (
|
||||
compute.min_size == expected_min_size
|
||||
), f"Expected min_size={expected_min_size}, got {compute.min_size}"
|
||||
assert (
|
||||
compute.max_size == expected_max_size
|
||||
), f"Expected max_size={expected_max_size}, got {compute.max_size}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
Reference in New Issue
Block a user