chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
build/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
@@ -0,0 +1,20 @@
|
||||
# config.yaml
|
||||
applications:
|
||||
- args:
|
||||
llm_configs:
|
||||
- model_loading_config:
|
||||
model_id: qwen3-reward
|
||||
model_source: Qwen/Qwen3-0.6B
|
||||
# Register the plugin architecture in the Ray Serve LLM processes.
|
||||
server_cls: qwen3_reward_plugin.serve_hook:RewardModelServer
|
||||
engine_kwargs:
|
||||
runner: pooling
|
||||
hf_overrides:
|
||||
architectures:
|
||||
- Qwen3CustomRewardModel
|
||||
num_labels: 1
|
||||
problem_type: regression
|
||||
max_model_len: 4096
|
||||
import_path: ray.serve.llm:build_openai_app
|
||||
name: custom_vllm_app
|
||||
route_prefix: "/"
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Documentation example and CI test: custom vLLM model with Ray Serve LLM.
|
||||
|
||||
Structure:
|
||||
1. Test-only setup: force serve.run non-blocking. The plugin is baked into the
|
||||
cluster image, and direct streaming is enabled through the cluster
|
||||
environment.
|
||||
2. Docs example (between __custom_vllm_example_start/end__): embedded in the
|
||||
guide via literalinclude.
|
||||
3. Test validation (deployment status polling + reward-head assertion + cleanup).
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from ray import serve
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
|
||||
_original_serve_run = serve.run
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing."""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
|
||||
# __custom_vllm_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
|
||||
# Serve Qwen3-0.6B with a scalar reward head from the `qwen3_reward_plugin`
|
||||
# vLLM plugin (pip install it into your cluster image).
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config=dict(
|
||||
model_id="qwen3-reward",
|
||||
model_source="Qwen/Qwen3-0.6B",
|
||||
),
|
||||
# vLLM's entry point registers the architecture in the engine and worker
|
||||
# processes. Ray Serve LLM also resolves it while building the engine config.
|
||||
server_cls="qwen3_reward_plugin.serve_hook:RewardModelServer",
|
||||
engine_kwargs=dict(
|
||||
runner="pooling",
|
||||
hf_overrides=dict(
|
||||
architectures=["Qwen3CustomRewardModel"],
|
||||
num_labels=1,
|
||||
problem_type="regression",
|
||||
),
|
||||
max_model_len=4096,
|
||||
),
|
||||
)
|
||||
|
||||
# Requires direct streaming so vLLM's native /classify route is exposed.
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app, blocking=True)
|
||||
# __custom_vllm_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 300
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. "
|
||||
f"Current status: {status}"
|
||||
)
|
||||
|
||||
# Verify the reward head ran end to end: /classify returns a single scalar score
|
||||
# per input (num_labels=1), read from data[0].probs.
|
||||
body = json.dumps(
|
||||
{"model": "qwen3-reward", "input": "The capital of France is Paris."}
|
||||
).encode()
|
||||
request = urllib.request.Request(
|
||||
"http://localhost:8000/classify",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
response = json.load(urllib.request.urlopen(request, timeout=60))
|
||||
reward = response["data"][0]["probs"]
|
||||
assert len(reward) == 1, f"Expected a scalar reward, got {len(reward)} values"
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Documentation example and CI test: custom vLLM model via a YAML config.
|
||||
|
||||
Structure:
|
||||
1. Test-only setup: force serve.run non-blocking and strip accelerator
|
||||
requirements for CI. The plugin is baked into the cluster image, and direct
|
||||
streaming is enabled through the cluster environment.
|
||||
2. Load the YAML config and deploy it with build_openai_app.
|
||||
3. Test validation (deployment status polling + reward-head assertion + cleanup).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import yaml
|
||||
|
||||
from ray import serve
|
||||
from ray.serve import llm
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
|
||||
config_path = os.path.join(os.path.dirname(__file__), "custom_vllm_config.yaml")
|
||||
with open(config_path, "r") as f:
|
||||
config_dict = yaml.safe_load(f)
|
||||
|
||||
llm_configs = config_dict["applications"][0]["args"]["llm_configs"]
|
||||
app = llm.build_openai_app({"llm_configs": llm_configs})
|
||||
serve.run(app, blocking=False)
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 300
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. "
|
||||
f"Current status: {status}"
|
||||
)
|
||||
|
||||
# Verify the reward head ran end to end: /classify returns a single scalar score
|
||||
# per input (num_labels=1), read from data[0].probs.
|
||||
body = json.dumps(
|
||||
{"model": "qwen3-reward", "input": "The capital of France is Paris."}
|
||||
).encode()
|
||||
request = urllib.request.Request(
|
||||
"http://localhost:8000/classify",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
response = json.load(urllib.request.urlopen(request, timeout=60))
|
||||
reward = response["data"][0]["probs"]
|
||||
assert len(reward) == 1, f"Expected a scalar reward, got {len(reward)} values"
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,11 @@
|
||||
# __register_start__
|
||||
"""vLLM plugin entry point: register the custom architecture."""
|
||||
def register() -> None:
|
||||
from vllm import ModelRegistry
|
||||
|
||||
if "Qwen3CustomRewardModel" not in ModelRegistry.get_supported_archs():
|
||||
ModelRegistry.register_model(
|
||||
"Qwen3CustomRewardModel",
|
||||
"qwen3_reward_plugin.qwen3_rm:Qwen3CustomRewardModel",
|
||||
)
|
||||
# __register_end__
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Qwen3 with the LM head replaced by a scalar reward head, as a vLLM plugin."""
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.linear import ReplicatedLinear
|
||||
from vllm.model_executor.layers.pooler import DispatchPooler
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.model_executor.models.interfaces_base import default_pooling_type
|
||||
from vllm.model_executor.models.qwen3 import Qwen3ForCausalLM
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader, maybe_prefix
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
REWARD_HEAD_FILE = "reward_head.pt"
|
||||
REWARD_HEAD_KEYS = ("linear.weight", "weight", "score.weight")
|
||||
|
||||
|
||||
@default_pooling_type(seq_pooling_type="LAST")
|
||||
class Qwen3CustomRewardModel(Qwen3ForCausalLM):
|
||||
"""Qwen3 backbone + scalar reward head, served as a pooling model."""
|
||||
|
||||
is_pooling_model = True
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
# A single scalar output (set problem_type="regression" in the config
|
||||
# for an identity activation, so /classify returns the raw reward).
|
||||
vllm_config.model_config.hf_config.num_labels = 1
|
||||
super().__init__(vllm_config=vllm_config, prefix=prefix)
|
||||
|
||||
model_config = vllm_config.model_config
|
||||
|
||||
# Scalar reward head. Replicated because the output dim is 1.
|
||||
self.score = ReplicatedLinear(
|
||||
model_config.hf_config.hidden_size,
|
||||
1,
|
||||
bias=False,
|
||||
params_dtype=model_config.head_dtype,
|
||||
return_bias=False,
|
||||
prefix=maybe_prefix(prefix, "score"),
|
||||
)
|
||||
|
||||
# LAST-token pooling -> reward head -> raw score. Advertising the
|
||||
# "classify" task mounts vLLM's /classify route.
|
||||
pooler_config = model_config.pooler_config
|
||||
assert pooler_config is not None
|
||||
self.pooler = DispatchPooler.for_seq_cls(pooler_config, classifier=self.score)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
skip_prefixes = ["score."] # reward head loads separately, below
|
||||
if self.config.tie_word_embeddings:
|
||||
skip_prefixes.append("lm_head.") # tied: no lm_head weight to load
|
||||
loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
|
||||
loaded = loader.load_weights(weights)
|
||||
if self._load_reward_head():
|
||||
loaded.add("score.weight")
|
||||
return loaded
|
||||
|
||||
def _load_reward_head(self) -> bool:
|
||||
"""Load ``reward_head.pt``; return False (keep init values) if absent."""
|
||||
env_path = os.environ.get("RM_REWARD_HEAD_PATH")
|
||||
model_dir = self.vllm_config.model_config.model
|
||||
head_path = (
|
||||
env_path
|
||||
if env_path and os.path.isfile(env_path)
|
||||
else os.path.join(model_dir, REWARD_HEAD_FILE)
|
||||
)
|
||||
if not os.path.isfile(head_path):
|
||||
logger.warning(
|
||||
"Reward head %r not found; scores are not meaningful.", head_path
|
||||
)
|
||||
return False
|
||||
|
||||
state = torch.load(head_path, map_location="cpu", weights_only=True)
|
||||
weight = (
|
||||
state
|
||||
if isinstance(state, torch.Tensor)
|
||||
else next((state[k] for k in REWARD_HEAD_KEYS if k in state), None)
|
||||
)
|
||||
if weight is None:
|
||||
raise KeyError(f"{REWARD_HEAD_FILE} has none of {REWARD_HEAD_KEYS}")
|
||||
|
||||
param = self.score.weight
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, weight.reshape(1, -1).to(param.dtype))
|
||||
logger.info("Loaded reward head from %s.", head_path)
|
||||
return True
|
||||
@@ -0,0 +1,13 @@
|
||||
# __serve_hook_start__
|
||||
"""LLMServer subclass for ``LLMConfig.server_cls``: importing it registers the plugin."""
|
||||
|
||||
from ray.llm._internal.serve.core.server.llm_server import LLMServer
|
||||
|
||||
from qwen3_reward_plugin import register
|
||||
|
||||
register()
|
||||
|
||||
|
||||
class RewardModelServer(LLMServer):
|
||||
pass
|
||||
# __serve_hook_end__
|
||||
@@ -0,0 +1,16 @@
|
||||
# __setup_start__
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="qwen3-reward-plugin",
|
||||
version="0.1.0",
|
||||
packages=["qwen3_reward_plugin"],
|
||||
# vLLM discovers and runs this entry point in every process (the engine core
|
||||
# and each rank worker) via load_general_plugins().
|
||||
entry_points={
|
||||
"vllm.general_plugins": [
|
||||
"qwen3_reward = qwen3_reward_plugin:register",
|
||||
],
|
||||
},
|
||||
)
|
||||
# __setup_end__
|
||||
@@ -0,0 +1,20 @@
|
||||
# config.yaml
|
||||
applications:
|
||||
- name: llm-direct-streaming
|
||||
route_prefix: /
|
||||
import_path: ray.serve.llm:build_openai_app
|
||||
args:
|
||||
llm_configs:
|
||||
- model_loading_config:
|
||||
model_id: qwen3.5-0.8b
|
||||
model_source: Qwen/Qwen3.5-0.8B
|
||||
deployment_config:
|
||||
autoscaling_config:
|
||||
min_replicas: 1
|
||||
max_replicas: 4
|
||||
engine_kwargs:
|
||||
trust_remote_code: true
|
||||
tensor_parallel_size: 1
|
||||
enable_auto_tool_choice: true
|
||||
tool_call_parser: qwen3_coder
|
||||
reasoning_parser: qwen3
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __direct_streaming_custom_router_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
|
||||
Scope: validates the documented request_router_config snippet (config and the
|
||||
build_openai_app call signature) for direct streaming.
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_openai_app = llm.build_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_openai_app(llm_serving_args):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
for config in llm_serving_args["llm_configs"]:
|
||||
config.accelerator_type = None
|
||||
|
||||
return _original_build_openai_app(llm_serving_args)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_openai_app = _testing_build_openai_app
|
||||
|
||||
# __direct_streaming_custom_router_example_start__
|
||||
from ray.serve.config import RequestRouterConfig
|
||||
from ray.serve.llm import LLMConfig
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "qwen3.5-0.8b",
|
||||
"model_source": "Qwen/Qwen3.5-0.8B",
|
||||
},
|
||||
deployment_config={
|
||||
"request_router_config": RequestRouterConfig(
|
||||
request_router_class="ray.serve.experimental.consistent_hash_router.ConsistentHashRouter",
|
||||
),
|
||||
},
|
||||
engine_kwargs={
|
||||
"trust_remote_code": True,
|
||||
"tensor_parallel_size": 1,
|
||||
"enable_auto_tool_choice": True,
|
||||
"tool_call_parser": "qwen3_coder",
|
||||
"reasoning_parser": "qwen3",
|
||||
},
|
||||
)
|
||||
# __direct_streaming_custom_router_example_end__
|
||||
|
||||
from ray.serve.llm import build_openai_app
|
||||
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app)
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 180
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __direct_streaming_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
|
||||
Scope: builds and deploys the documented snippet with direct streaming enabled.
|
||||
The bazel target sets ``RAY_SERVE_ENABLE_HA_PROXY`` and
|
||||
``RAY_SERVE_LLM_ENABLE_DIRECT_STREAMING``, so build_openai_app returns the
|
||||
direct streaming deployment and the app reaches RUNNING on the HAProxy ingress.
|
||||
The end-to-end streaming data path is covered by the openai-compatibility
|
||||
direct-streaming test suite.
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_openai_app = llm.build_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_openai_app(llm_serving_args):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
for config in llm_serving_args["llm_configs"]:
|
||||
config.accelerator_type = None
|
||||
|
||||
return _original_build_openai_app(llm_serving_args)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_openai_app = _testing_build_openai_app
|
||||
|
||||
# __direct_streaming_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "qwen3.5-0.8b",
|
||||
"model_source": "Qwen/Qwen3.5-0.8B",
|
||||
},
|
||||
deployment_config={
|
||||
"autoscaling_config": {"min_replicas": 1, "max_replicas": 4},
|
||||
},
|
||||
engine_kwargs={
|
||||
"trust_remote_code": True,
|
||||
"tensor_parallel_size": 1,
|
||||
"enable_auto_tool_choice": True,
|
||||
"tool_call_parser": "qwen3_coder",
|
||||
"reasoning_parser": "qwen3",
|
||||
},
|
||||
)
|
||||
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app)
|
||||
# __direct_streaming_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 180
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test for the direct
|
||||
streaming YAML config.
|
||||
|
||||
Structure:
|
||||
1. Load the YAML config shown in the guide and convert it to an app with
|
||||
build_openai_app, removing accelerator requirements for CI testing.
|
||||
2. Test validation (deployment status polling + cleanup).
|
||||
|
||||
Scope: validates that the documented config.yaml parses into a valid
|
||||
LLMServingArgs and deploys with direct streaming enabled. The bazel target sets
|
||||
RAY_SERVE_ENABLE_HA_PROXY and RAY_SERVE_LLM_ENABLE_DIRECT_STREAMING, so the app
|
||||
reaches RUNNING on the HAProxy ingress. The end-to-end streaming data path is
|
||||
covered by the openai-compatibility direct-streaming test suite.
|
||||
"""
|
||||
|
||||
import time
|
||||
import os
|
||||
import yaml
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
|
||||
config_path = os.path.join(os.path.dirname(__file__), "direct_streaming_config.yaml")
|
||||
with open(config_path, "r") as f:
|
||||
config_dict = yaml.safe_load(f)
|
||||
|
||||
llm_configs = config_dict["applications"][0]["args"]["llm_configs"]
|
||||
for config in llm_configs:
|
||||
config.pop("accelerator_type", None)
|
||||
# Disable compile cache to avoid cache corruption in CI
|
||||
if "runtime_env" not in config:
|
||||
config["runtime_env"] = {}
|
||||
if "env_vars" not in config["runtime_env"]:
|
||||
config["runtime_env"]["env_vars"] = {}
|
||||
config["runtime_env"]["env_vars"]["VLLM_DISABLE_COMPILE_CACHE"] = "1"
|
||||
|
||||
app = llm.build_openai_app({"llm_configs": llm_configs})
|
||||
serve.run(app, blocking=False)
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 180
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,91 @@
|
||||
# flake8: noqa
|
||||
"""
|
||||
This file serves as a documentation example and CI test for bundle_per_worker placement group config.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __bundle_per_worker_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_openai_app = llm.build_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_openai_app(config, **kwargs):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
llm_configs = config.get("llm_configs", [])
|
||||
for llm_config in llm_configs:
|
||||
if hasattr(llm_config, "accelerator_type") and llm_config.accelerator_type is not None:
|
||||
llm_config.accelerator_type = None
|
||||
return _original_build_openai_app(config, **kwargs)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_openai_app = _testing_build_openai_app
|
||||
|
||||
# __bundle_per_worker_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
|
||||
# Configure a model with bundle_per_worker for simple resource specification
|
||||
# Ray automatically replicates this bundle based on tp * pp (2 bundles total)
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config=dict(
|
||||
model_id="qwen-0.5b",
|
||||
model_source="Qwen/Qwen2.5-0.5B-Instruct",
|
||||
),
|
||||
deployment_config=dict(
|
||||
autoscaling_config=dict(
|
||||
min_replicas=1,
|
||||
max_replicas=1,
|
||||
)
|
||||
),
|
||||
engine_kwargs=dict(
|
||||
tensor_parallel_size=2,
|
||||
max_model_len=1024,
|
||||
max_num_seqs=32,
|
||||
),
|
||||
# Simple: specify resources per worker, auto-replicated by tp*pp
|
||||
placement_group_config=dict(
|
||||
bundle_per_worker={"GPU": 1, "CPU": 1},
|
||||
),
|
||||
)
|
||||
|
||||
# Deploy the application
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app, blocking=True)
|
||||
# __bundle_per_worker_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 300
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test for autoscaling data parallel attention deployment.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __dp_autoscaling_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_dp_openai_app = llm.build_dp_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_dp_openai_app(builder_config, **kwargs):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
if "llm_config" in builder_config:
|
||||
config = builder_config["llm_config"]
|
||||
if hasattr(config, "accelerator_type") and config.accelerator_type is not None:
|
||||
config.accelerator_type = None
|
||||
return _original_build_dp_openai_app(builder_config, **kwargs)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_dp_openai_app = _testing_build_dp_openai_app
|
||||
|
||||
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_dp_openai_app
|
||||
|
||||
# __dp_autoscaling_example_start__
|
||||
config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "microsoft/Phi-tiny-MoE-instruct"
|
||||
},
|
||||
deployment_config={
|
||||
"num_replicas": "auto",
|
||||
"autoscaling_config": {
|
||||
"min_replicas": 1, # Min number of DP groups
|
||||
"max_replicas": 2, # Max number of DP groups
|
||||
"initial_replicas": 1, # Initial number of DP groups
|
||||
# Other Ray Serve autoscaling knobs still apply
|
||||
"upscale_delay_s": 0.1,
|
||||
"downscale_delay_s": 2,
|
||||
},
|
||||
},
|
||||
engine_kwargs={
|
||||
"data_parallel_size": 2, # Number of DP replicas
|
||||
"tensor_parallel_size": 1, # TP size per replica
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": 32,
|
||||
},
|
||||
)
|
||||
|
||||
app = build_dp_openai_app({
|
||||
"llm_config": config
|
||||
})
|
||||
# __dp_autoscaling_example_end__
|
||||
|
||||
serve.run(app, blocking=True)
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 300
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test for basic data parallel attention deployment.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __dp_basic_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_dp_openai_app = llm.build_dp_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_dp_openai_app(builder_config, **kwargs):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
if "llm_config" in builder_config:
|
||||
config = builder_config["llm_config"]
|
||||
if hasattr(config, "accelerator_type") and config.accelerator_type is not None:
|
||||
config.accelerator_type = None
|
||||
return _original_build_dp_openai_app(builder_config, **kwargs)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_dp_openai_app = _testing_build_dp_openai_app
|
||||
|
||||
# __dp_basic_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_dp_openai_app
|
||||
|
||||
# Configure the model with data parallel settings
|
||||
config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "microsoft/Phi-tiny-MoE-instruct"
|
||||
},
|
||||
deployment_config={
|
||||
"num_replicas": 2
|
||||
},
|
||||
engine_kwargs={
|
||||
"data_parallel_size": 2, # Number of DP replicas
|
||||
"tensor_parallel_size": 1, # TP size per replica
|
||||
# Reduced for CI compatibility
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": 32,
|
||||
},
|
||||
)
|
||||
|
||||
app = build_dp_openai_app({
|
||||
"llm_config": config
|
||||
})
|
||||
|
||||
serve.run(app, blocking=True)
|
||||
# __dp_basic_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 300
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test for data parallel + prefill-decode disaggregation.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __dp_pd_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
# Check if NIXL is available (required for NixlConnector)
|
||||
try:
|
||||
import nixl # noqa: F401
|
||||
NIXL_AVAILABLE = True
|
||||
except ImportError:
|
||||
NIXL_AVAILABLE = False
|
||||
|
||||
if not NIXL_AVAILABLE:
|
||||
raise ImportError(
|
||||
"NIXL is required for this example but is not installed. "
|
||||
"Install it with: pip install nixl or uv pip install nixl"
|
||||
)
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_dp_deployment = llm.build_dp_deployment
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_dp_deployment(llm_config, **kwargs):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
if llm_config.accelerator_type is not None:
|
||||
llm_config.accelerator_type = None
|
||||
return _original_build_dp_deployment(llm_config, **kwargs)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_dp_deployment = _testing_build_dp_deployment
|
||||
|
||||
# __dp_pd_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_pd_openai_app
|
||||
|
||||
# Configure prefill with data parallel attention
|
||||
prefill_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "microsoft/Phi-tiny-MoE-instruct"
|
||||
},
|
||||
engine_kwargs={
|
||||
"data_parallel_size": 2, # 2 DP replicas for prefill
|
||||
"tensor_parallel_size": 1,
|
||||
"kv_transfer_config": {
|
||||
"kv_connector": "NixlConnector",
|
||||
"kv_role": "kv_both",
|
||||
},
|
||||
# Reduced for CI compatibility
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": 32,
|
||||
},
|
||||
)
|
||||
|
||||
# Configure decode with data parallel attention
|
||||
decode_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "microsoft/Phi-tiny-MoE-instruct"
|
||||
},
|
||||
engine_kwargs={
|
||||
"data_parallel_size": 2, # 2 DP replicas for decode
|
||||
"tensor_parallel_size": 1,
|
||||
"kv_transfer_config": {
|
||||
"kv_connector": "NixlConnector",
|
||||
"kv_role": "kv_both",
|
||||
},
|
||||
# Reduced for CI compatibility
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": 32,
|
||||
},
|
||||
)
|
||||
|
||||
# Build and deploy the PD application (3-tier: ingress -> decode -> prefill)
|
||||
# PDDecodeServer orchestrates remote prefill then runs local decode.
|
||||
app = build_pd_openai_app(
|
||||
{
|
||||
"prefill_config": prefill_config,
|
||||
"decode_config": decode_config,
|
||||
}
|
||||
)
|
||||
serve.run(app, blocking=True)
|
||||
# __dp_pd_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 300 # Longer timeout for DP+PD setup
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __prefix_aware_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_openai_app = llm.build_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_openai_app(llm_serving_args):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
for config in llm_serving_args["llm_configs"]:
|
||||
config.accelerator_type = None
|
||||
|
||||
return _original_build_openai_app(llm_serving_args)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_openai_app = _testing_build_openai_app
|
||||
|
||||
# __prefix_aware_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
from ray.serve.llm.request_router import PrefixCacheAffinityRouter
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "qwen-0.5b",
|
||||
"model_source": "Qwen/Qwen2.5-0.5B-Instruct",
|
||||
},
|
||||
deployment_config={
|
||||
"autoscaling_config": {
|
||||
"min_replicas": 4,
|
||||
"max_replicas": 4,
|
||||
},
|
||||
"request_router_config": {
|
||||
"request_router_class": PrefixCacheAffinityRouter,
|
||||
"request_router_kwargs": {
|
||||
"imbalanced_threshold": 5, # More aggressive load balancing
|
||||
"match_rate_threshold": 0.15, # Require 15% match rate
|
||||
"do_eviction": True, # Enable memory management
|
||||
"eviction_threshold_chars": 500_000,
|
||||
"eviction_target_chars": 400_000,
|
||||
"eviction_interval_secs": 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app, blocking=True)
|
||||
# __prefix_aware_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 180
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,23 @@
|
||||
# config.yaml
|
||||
applications:
|
||||
- args:
|
||||
llm_configs:
|
||||
- model_loading_config:
|
||||
model_id: qwen-0.5b
|
||||
model_source: Qwen/Qwen2.5-0.5B-Instruct
|
||||
accelerator_type: A10G
|
||||
deployment_config:
|
||||
autoscaling_config:
|
||||
min_replicas: 1
|
||||
max_replicas: 2
|
||||
- model_loading_config:
|
||||
model_id: qwen-1.5b
|
||||
model_source: Qwen/Qwen2.5-1.5B-Instruct
|
||||
accelerator_type: A10G
|
||||
deployment_config:
|
||||
autoscaling_config:
|
||||
min_replicas: 1
|
||||
max_replicas: 2
|
||||
import_path: ray.serve.llm:build_openai_app
|
||||
name: llm_app
|
||||
route_prefix: "/"
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test for YAML config deployment.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Load YAML config and convert to Python using build_openai_app
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
import os
|
||||
import yaml
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
|
||||
config_path = os.path.join(os.path.dirname(__file__), "llm_config_example.yaml")
|
||||
with open(config_path, "r") as f:
|
||||
config_dict = yaml.safe_load(f)
|
||||
|
||||
llm_configs = config_dict["applications"][0]["args"]["llm_configs"]
|
||||
for config in llm_configs:
|
||||
config.pop("accelerator_type", None)
|
||||
# Disable compile cache to avoid cache corruption in CI
|
||||
if "runtime_env" not in config:
|
||||
config["runtime_env"] = {}
|
||||
if "env_vars" not in config["runtime_env"]:
|
||||
config["runtime_env"]["env_vars"] = {}
|
||||
config["runtime_env"]["env_vars"]["VLLM_DISABLE_COMPILE_CACHE"] = "1"
|
||||
|
||||
app = llm.build_openai_app({"llm_configs": llm_configs})
|
||||
serve.run(app, blocking=False)
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 180
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __qwen_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_openai_app = llm.build_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_openai_app(llm_serving_args):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
for config in llm_serving_args["llm_configs"]:
|
||||
config.accelerator_type = None
|
||||
# Disable compile cache to avoid cache corruption in CI
|
||||
if not config.runtime_env:
|
||||
config.runtime_env = {}
|
||||
if "env_vars" not in config.runtime_env:
|
||||
config.runtime_env["env_vars"] = {}
|
||||
config.runtime_env["env_vars"]["VLLM_DISABLE_COMPILE_CACHE"] = "1"
|
||||
|
||||
return _original_build_openai_app(llm_serving_args)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_openai_app = _testing_build_openai_app
|
||||
|
||||
# __qwen_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "qwen-0.5b",
|
||||
"model_source": "Qwen/Qwen2.5-0.5B-Instruct",
|
||||
},
|
||||
deployment_config={
|
||||
"autoscaling_config": {
|
||||
"min_replicas": 1,
|
||||
"max_replicas": 2,
|
||||
}
|
||||
},
|
||||
# Pass the desired accelerator type (e.g. A10G, L4, etc.)
|
||||
accelerator_type="A10G",
|
||||
# You can customize the engine arguments (e.g. vLLM engine kwargs)
|
||||
engine_kwargs={
|
||||
"tensor_parallel_size": 2,
|
||||
},
|
||||
)
|
||||
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app, blocking=True)
|
||||
# __qwen_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 180
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
@@ -0,0 +1,45 @@
|
||||
# __sglang_batch_start__
|
||||
import ray
|
||||
from ray.data.llm import SGLangEngineProcessorConfig, build_processor
|
||||
|
||||
config = SGLangEngineProcessorConfig(
|
||||
model_source="unsloth/Llama-3.1-8B-Instruct",
|
||||
engine_kwargs=dict(
|
||||
dtype="half",
|
||||
mem_fraction_static=0.8,
|
||||
),
|
||||
batch_size=32,
|
||||
concurrency=1,
|
||||
)
|
||||
|
||||
processor = build_processor(
|
||||
config,
|
||||
preprocess=lambda row: dict(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": row["prompt"]},
|
||||
],
|
||||
sampling_params=dict(
|
||||
temperature=0.7,
|
||||
max_new_tokens=256,
|
||||
),
|
||||
),
|
||||
postprocess=lambda row: dict(
|
||||
prompt=row["prompt"],
|
||||
response=row["generated_text"],
|
||||
),
|
||||
)
|
||||
|
||||
ds = ray.data.from_items(
|
||||
[
|
||||
{"prompt": "What is the capital of France?"},
|
||||
{"prompt": "Explain photosynthesis in one sentence."},
|
||||
{"prompt": "Write a haiku about programming."},
|
||||
]
|
||||
)
|
||||
ds = processor(ds)
|
||||
|
||||
for row in ds.take_all():
|
||||
print(f"Prompt: {row['prompt']}")
|
||||
print(f"Response: {row['response']}\n")
|
||||
# __sglang_batch_end__
|
||||
@@ -0,0 +1,36 @@
|
||||
# __sglang_multinode_start__
|
||||
from ray.llm._internal.serve.engines.sglang import SGLangServer
|
||||
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "Llama-3.1-70B-Instruct",
|
||||
"model_source": "meta-llama/Llama-3.1-70B-Instruct",
|
||||
},
|
||||
deployment_config={
|
||||
"autoscaling_config": {
|
||||
"min_replicas": 1,
|
||||
"max_replicas": 2,
|
||||
"target_ongoing_requests": 4,
|
||||
}
|
||||
},
|
||||
# PACK fills GPUs on each node before moving to the next.
|
||||
# With 8 bundles across 2 nodes (4 GPUs each), each node gets 4 bundles.
|
||||
placement_group_config={
|
||||
"placement_group_bundles": [{"CPU": 1, "GPU": 1}] + [{"GPU": 1}] * 7,
|
||||
"placement_group_strategy": "PACK",
|
||||
},
|
||||
server_cls=SGLangServer,
|
||||
engine_kwargs={
|
||||
"model_path": "meta-llama/Llama-3.1-70B-Instruct",
|
||||
"tp_size": 4,
|
||||
"pp_size": 2,
|
||||
"mem_fraction_static": 0.8,
|
||||
},
|
||||
)
|
||||
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app, blocking=True)
|
||||
# __sglang_multinode_end__
|
||||
@@ -0,0 +1,27 @@
|
||||
# __sglang_query_start__
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="http://localhost:8000/v1", api_key="fake-key")
|
||||
|
||||
# Chat completions
|
||||
print("=== Chat Completions ===")
|
||||
chat_response = client.chat.completions.create(
|
||||
model="Llama-3.1-8B-Instruct",
|
||||
messages=[
|
||||
{"role": "user", "content": "List 3 countries and their capitals."},
|
||||
],
|
||||
temperature=0,
|
||||
max_tokens=64,
|
||||
)
|
||||
print(chat_response.choices[0].message.content)
|
||||
|
||||
# Text completions
|
||||
print("\n=== Text Completions ===")
|
||||
completion_response = client.completions.create(
|
||||
model="Llama-3.1-8B-Instruct",
|
||||
prompt="San Francisco is a",
|
||||
temperature=0,
|
||||
max_tokens=30,
|
||||
)
|
||||
print(completion_response.choices[0].text)
|
||||
# __sglang_query_end__
|
||||
@@ -0,0 +1,29 @@
|
||||
# __sglang_single_node_start__
|
||||
from ray.llm._internal.serve.engines.sglang import SGLangServer
|
||||
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "Llama-3.1-8B-Instruct",
|
||||
"model_source": "unsloth/Llama-3.1-8B-Instruct",
|
||||
},
|
||||
deployment_config={
|
||||
"autoscaling_config": {
|
||||
"min_replicas": 1,
|
||||
"max_replicas": 2,
|
||||
}
|
||||
},
|
||||
server_cls=SGLangServer,
|
||||
engine_kwargs={
|
||||
"trust_remote_code": True,
|
||||
"model_path": "unsloth/Llama-3.1-8B-Instruct",
|
||||
"tp_size": 1,
|
||||
"mem_fraction_static": 0.8,
|
||||
},
|
||||
)
|
||||
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app, blocking=True)
|
||||
# __sglang_single_node_end__
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
This file serves as a documentation example and CI test.
|
||||
|
||||
Structure:
|
||||
1. Monkeypatch setup: Ensures serve.run is non-blocking and removes accelerator requirements for CI testing.
|
||||
2. Docs example (between __transcription_example_start/end__): Embedded in Sphinx docs via literalinclude.
|
||||
3. Test validation (deployment status polling + cleanup)
|
||||
"""
|
||||
|
||||
import time
|
||||
import openai
|
||||
import requests
|
||||
from ray import serve
|
||||
from ray.serve.schema import ApplicationStatus
|
||||
from ray.serve._private.constants import SERVE_DEFAULT_APP_NAME
|
||||
from ray.serve import llm
|
||||
|
||||
_original_serve_run = serve.run
|
||||
_original_build_openai_app = llm.build_openai_app
|
||||
|
||||
|
||||
def _non_blocking_serve_run(app, **kwargs):
|
||||
"""Forces blocking=False for testing"""
|
||||
kwargs["blocking"] = False
|
||||
return _original_serve_run(app, **kwargs)
|
||||
|
||||
|
||||
def _testing_build_openai_app(llm_serving_args):
|
||||
"""Removes accelerator requirements for testing"""
|
||||
for config in llm_serving_args["llm_configs"]:
|
||||
config.accelerator_type = None
|
||||
|
||||
return _original_build_openai_app(llm_serving_args)
|
||||
|
||||
|
||||
serve.run = _non_blocking_serve_run
|
||||
llm.build_openai_app = _testing_build_openai_app
|
||||
|
||||
# __transcription_example_start__
|
||||
from ray import serve
|
||||
from ray.serve.llm import LLMConfig, build_openai_app
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model_loading_config={
|
||||
"model_id": "whisper-small",
|
||||
"model_source": "openai/whisper-small",
|
||||
},
|
||||
deployment_config={
|
||||
"autoscaling_config": {
|
||||
"min_replicas": 1,
|
||||
"max_replicas": 4,
|
||||
}
|
||||
},
|
||||
accelerator_type="A10G",
|
||||
log_engine_metrics=True,
|
||||
)
|
||||
|
||||
app = build_openai_app({"llm_configs": [llm_config]})
|
||||
serve.run(app, blocking=True)
|
||||
# __transcription_example_end__
|
||||
|
||||
status = ApplicationStatus.NOT_STARTED
|
||||
timeout_seconds = 300
|
||||
start_time = time.time()
|
||||
|
||||
while (
|
||||
status != ApplicationStatus.RUNNING and time.time() - start_time < timeout_seconds
|
||||
):
|
||||
status = serve.status().applications[SERVE_DEFAULT_APP_NAME].status
|
||||
|
||||
if status in [ApplicationStatus.DEPLOY_FAILED, ApplicationStatus.UNHEALTHY]:
|
||||
raise AssertionError(f"Deployment failed with status: {status}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if status != ApplicationStatus.RUNNING:
|
||||
raise AssertionError(
|
||||
f"Deployment failed to reach RUNNING status within {timeout_seconds}s. Current status: {status}"
|
||||
)
|
||||
|
||||
response = requests.get("https://voiceage.com/wbsamples/in_stereo/Sports.wav")
|
||||
with open("audio.wav", "wb") as f:
|
||||
f.write(response.content)
|
||||
|
||||
client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="fake-key")
|
||||
|
||||
with open("audio.wav", "rb") as f:
|
||||
try:
|
||||
response = client.audio.transcriptions.create(
|
||||
model="whisper-small",
|
||||
file=f,
|
||||
temperature=0.0,
|
||||
language="en",
|
||||
)
|
||||
except Exception as e:
|
||||
raise AssertionError(
|
||||
f"Error while querying models: {e}. Check the logs for more details."
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
Reference in New Issue
Block a user