chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
from typing import List, Tuple # noqa: UP035
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve.sync_engine import EngineConfig, SyncMLCEngine
|
||||
|
||||
|
||||
def _parse_args():
|
||||
args = argparse.ArgumentParser()
|
||||
args.add_argument("--model-lib", type=str)
|
||||
args.add_argument("--device", type=str, default="auto")
|
||||
args.add_argument("--batch-size", type=int, default=80)
|
||||
args.add_argument("--max-total-seq-length", type=int)
|
||||
args.add_argument("--seed", type=int, default=0)
|
||||
|
||||
parsed = args.parse_args()
|
||||
parsed.model = os.path.dirname(parsed.model_lib)
|
||||
assert parsed.batch_size % 16 == 0
|
||||
return parsed
|
||||
|
||||
|
||||
def generate_requests(
|
||||
num_requests: int, input_length: int, output_length: int
|
||||
) -> Tuple[List[List[int]], List[GenerationConfig]]: # noqa: UP006
|
||||
prompt_ids = []
|
||||
for _ in range(num_requests):
|
||||
token_ids = []
|
||||
for _ in range(input_length):
|
||||
token_ids.append(random.randint(0, 30000))
|
||||
prompt_ids.append(token_ids)
|
||||
generation_config_list = [
|
||||
GenerationConfig(temperature=1.0, top_p=1.0, max_tokens=output_length)
|
||||
] * num_requests
|
||||
return prompt_ids, generation_config_list
|
||||
|
||||
|
||||
def benchmark(args: argparse.Namespace):
|
||||
random.seed(args.seed)
|
||||
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=args.model,
|
||||
device=args.device,
|
||||
model_lib=args.model_lib,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_num_sequence=args.batch_size,
|
||||
max_total_sequence_length=args.max_total_seq_length,
|
||||
),
|
||||
)
|
||||
|
||||
print(args)
|
||||
for num_requests in [1, 2, 4, 8, 16, 32, 64]:
|
||||
if num_requests > args.batch_size:
|
||||
continue
|
||||
for input_length in [64, 128, 256, 512, 1024]:
|
||||
if num_requests * input_length >= 16384:
|
||||
continue
|
||||
for output_length in [4]:
|
||||
print(f"nreq={num_requests}\tin={input_length}\tout={output_length}")
|
||||
prompt_ids, generation_config = generate_requests(
|
||||
num_requests, input_length, output_length
|
||||
)
|
||||
engine.reset()
|
||||
engine.generate(prompt_ids, generation_config)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ARGS = _parse_args()
|
||||
benchmark(ARGS)
|
||||
@@ -0,0 +1,34 @@
|
||||
import os
|
||||
from typing import Tuple # noqa: UP035
|
||||
|
||||
import pytest
|
||||
|
||||
from mlc_llm.serve import PopenServer
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def served_model() -> Tuple[str, str]: # noqa: UP006
|
||||
model_lib = os.environ.get("MLC_SERVE_MODEL_LIB")
|
||||
if model_lib is None:
|
||||
raise ValueError(
|
||||
'Environment variable "MLC_SERVE_MODEL_LIB" not found. '
|
||||
"Please set it to model lib compiled by MLC LLM "
|
||||
"(e.g., `dist/Llama-2-7b-chat-hf-q0f16-MLC/Llama-2-7b-chat-hf-q0f16-MLC-cuda.so`)."
|
||||
)
|
||||
model = os.path.dirname(model_lib)
|
||||
return model, model_lib
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def launch_server(served_model):
|
||||
"""A pytest session-level fixture which launches the server in a subprocess."""
|
||||
server = PopenServer(
|
||||
model=served_model[0],
|
||||
model_lib=served_model[1],
|
||||
enable_tracing=True,
|
||||
enable_debug=True,
|
||||
port=8000,
|
||||
)
|
||||
|
||||
with server:
|
||||
yield
|
||||
@@ -0,0 +1,369 @@
|
||||
"""Embedding server endpoint tests in MLC LLM.
|
||||
|
||||
Tests the /v1/embeddings endpoint via HTTP using the OpenAI client,
|
||||
following the same patterns as test_server.py.
|
||||
|
||||
Reuses MLC LLM test infrastructure:
|
||||
- Pytest markers (endpoint)
|
||||
- expect_error() response validation pattern from test_server.py
|
||||
- OpenAI client usage pattern from test_server.py
|
||||
- Session-scoped server fixture pattern from conftest.py
|
||||
|
||||
Run (launches its own embedding-only server):
|
||||
MLC_SERVE_EMBEDDING_MODEL_LIB="path/to/model.dylib" \
|
||||
pytest -m endpoint tests/python/serve/server/test_embedding_server.py -v
|
||||
|
||||
Environment variables:
|
||||
MLC_SERVE_EMBEDDING_MODEL_LIB Path to compiled embedding model library (required)
|
||||
MLC_SERVE_EMBEDDING_MODEL Path to embedding model weight directory
|
||||
(optional, defaults to dirname of model lib)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional # noqa: UP035
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import requests
|
||||
from openai import OpenAI
|
||||
|
||||
# Reuse MLC LLM marker system
|
||||
pytestmark = [pytest.mark.endpoint]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
EMBEDDING_MODEL_LIB = os.environ.get("MLC_SERVE_EMBEDDING_MODEL_LIB")
|
||||
EMBEDDING_MODEL_DIR = os.environ.get(
|
||||
"MLC_SERVE_EMBEDDING_MODEL",
|
||||
os.path.dirname(EMBEDDING_MODEL_LIB) if EMBEDDING_MODEL_LIB else None,
|
||||
)
|
||||
EMBEDDING_SERVER_HOST = "127.0.0.1"
|
||||
EMBEDDING_SERVER_PORT = 8321
|
||||
EMBEDDING_BASE_URL = f"http://{EMBEDDING_SERVER_HOST}:{EMBEDDING_SERVER_PORT}/v1"
|
||||
EMBEDDING_MODEL_NAME = "embedding"
|
||||
|
||||
|
||||
def _skip_if_no_model():
|
||||
if EMBEDDING_MODEL_LIB is None:
|
||||
pytest.skip(
|
||||
'Environment variable "MLC_SERVE_EMBEDDING_MODEL_LIB" not found. '
|
||||
"Set it to a compiled embedding model library."
|
||||
)
|
||||
if not os.path.isfile(EMBEDDING_MODEL_LIB):
|
||||
pytest.skip(f"Embedding model library not found at: {EMBEDDING_MODEL_LIB}")
|
||||
if EMBEDDING_MODEL_DIR is None or not os.path.isdir(EMBEDDING_MODEL_DIR):
|
||||
pytest.skip(f"Embedding model directory not found at: {EMBEDDING_MODEL_DIR}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Response validation helpers — adapted from test_server.py patterns
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def check_embedding_response(
|
||||
response: Dict, # noqa: UP006
|
||||
*,
|
||||
model: str,
|
||||
num_embeddings: int,
|
||||
expected_dim: Optional[int] = None,
|
||||
check_unit_norm: bool = True,
|
||||
):
|
||||
"""Validate an OpenAI-compatible embedding response.
|
||||
|
||||
Adapted from check_openai_nonstream_response() in test_server.py,
|
||||
specialized for embedding responses.
|
||||
"""
|
||||
assert response["object"] == "list"
|
||||
assert response["model"] == model
|
||||
|
||||
data = response["data"]
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == num_embeddings
|
||||
|
||||
for item in data:
|
||||
assert item["object"] == "embedding"
|
||||
assert isinstance(item["index"], int)
|
||||
emb = item["embedding"]
|
||||
assert isinstance(emb, list)
|
||||
assert len(emb) > 0
|
||||
|
||||
if expected_dim is not None:
|
||||
assert len(emb) == expected_dim, f"Expected dim={expected_dim}, got {len(emb)}"
|
||||
|
||||
if check_unit_norm:
|
||||
norm = float(np.linalg.norm(emb))
|
||||
assert abs(norm - 1.0) < 1e-3, f"Expected unit norm, got {norm}"
|
||||
|
||||
# Usage validation — same pattern as test_server.py
|
||||
usage = response["usage"]
|
||||
assert isinstance(usage, dict)
|
||||
assert usage["prompt_tokens"] > 0
|
||||
assert usage["total_tokens"] == usage["prompt_tokens"]
|
||||
|
||||
|
||||
def expect_error(response_str: str, msg_prefix: Optional[str] = None):
|
||||
"""Validate error response — reused directly from test_server.py."""
|
||||
response = json.loads(response_str)
|
||||
assert response["object"] == "error"
|
||||
assert isinstance(response["message"], str)
|
||||
if msg_prefix is not None:
|
||||
assert response["message"].startswith(msg_prefix)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server fixture — follows PopenServer/launch_server pattern from conftest.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def launch_embedding_server():
|
||||
"""Launch an embedding-only server as a subprocess.
|
||||
|
||||
Follows the same lifecycle pattern as the launch_server fixture
|
||||
in serve/server/conftest.py, but uses a lightweight embedding-only
|
||||
server since PopenServer doesn't support --embedding-model yet.
|
||||
"""
|
||||
_skip_if_no_model()
|
||||
|
||||
mlc_llm_path = str(Path(__file__).resolve().parents[4] / "python")
|
||||
server_code = f"""
|
||||
import sys
|
||||
sys.path.insert(0, "{mlc_llm_path}")
|
||||
|
||||
import fastapi
|
||||
import uvicorn
|
||||
from mlc_llm.serve.embedding_engine import AsyncEmbeddingEngine
|
||||
from mlc_llm.serve.server import ServerContext
|
||||
from mlc_llm.serve.entrypoints import openai_entrypoints
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
app.include_router(openai_entrypoints.app)
|
||||
|
||||
engine = AsyncEmbeddingEngine(
|
||||
model="{EMBEDDING_MODEL_DIR}",
|
||||
model_lib="{EMBEDDING_MODEL_LIB}",
|
||||
device="auto",
|
||||
)
|
||||
ctx = ServerContext()
|
||||
ServerContext.server_context = ctx
|
||||
ctx.add_embedding_engine("{EMBEDDING_MODEL_NAME}", engine)
|
||||
|
||||
uvicorn.run(app, host="{EMBEDDING_SERVER_HOST}", port={EMBEDDING_SERVER_PORT}, log_level="info")
|
||||
"""
|
||||
with subprocess.Popen(
|
||||
[sys.executable, "-c", server_code],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
) as proc:
|
||||
# Wait for server readiness — same polling pattern as PopenServer.start()
|
||||
timeout = 120
|
||||
attempts = 0.0
|
||||
ready = False
|
||||
while attempts < timeout:
|
||||
try:
|
||||
response = requests.get(f"{EMBEDDING_BASE_URL}/models", timeout=2)
|
||||
if response.status_code == 200:
|
||||
ready = True
|
||||
break
|
||||
except requests.RequestException:
|
||||
pass
|
||||
attempts += 0.5
|
||||
time.sleep(0.5)
|
||||
|
||||
if not ready:
|
||||
stderr = proc.stderr.read().decode() if proc.stderr else ""
|
||||
proc.kill()
|
||||
raise RuntimeError(f"Embedding server failed to start in {timeout}s.\nStderr: {stderr}")
|
||||
|
||||
yield proc
|
||||
|
||||
# Cleanup — same pattern as PopenServer.terminate()
|
||||
proc.send_signal(signal.SIGINT)
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client(launch_embedding_server):
|
||||
"""OpenAI client connected to the embedding server."""
|
||||
assert launch_embedding_server is not None
|
||||
return OpenAI(base_url=EMBEDDING_BASE_URL, api_key="none")
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# /v1/models
|
||||
# ===================================================================
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
def test_models_endpoint():
|
||||
"""The /v1/models endpoint lists the embedding model."""
|
||||
resp = requests.get(f"{EMBEDDING_BASE_URL}/models", timeout=5)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Single input
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_single_string_input(client):
|
||||
"""Single string input returns one embedding."""
|
||||
resp = client.embeddings.create(input="What is machine learning?", model=EMBEDDING_MODEL_NAME)
|
||||
raw = resp.model_dump()
|
||||
check_embedding_response(raw, model=EMBEDDING_MODEL_NAME, num_embeddings=1)
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Batch input
|
||||
# ===================================================================
|
||||
|
||||
BATCH_INPUTS = [
|
||||
"What is machine learning?",
|
||||
"How to brew coffee?",
|
||||
"ML is a subset of AI.",
|
||||
]
|
||||
|
||||
|
||||
def test_batch_string_input(client):
|
||||
"""List of strings returns one embedding per input."""
|
||||
resp = client.embeddings.create(input=BATCH_INPUTS, model=EMBEDDING_MODEL_NAME)
|
||||
raw = resp.model_dump()
|
||||
check_embedding_response(raw, model=EMBEDDING_MODEL_NAME, num_embeddings=len(BATCH_INPUTS))
|
||||
|
||||
|
||||
def test_batch_index_ordering(client):
|
||||
"""Embedding indices are sequential."""
|
||||
resp = client.embeddings.create(input=BATCH_INPUTS, model=EMBEDDING_MODEL_NAME)
|
||||
indices = [d.index for d in resp.data]
|
||||
assert indices == list(range(len(BATCH_INPUTS)))
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Cosine similarity — semantic quality via endpoint
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_cosine_similarity_via_endpoint(client):
|
||||
"""Related texts have higher similarity than unrelated (end-to-end)."""
|
||||
resp = client.embeddings.create(
|
||||
input=[
|
||||
"What is machine learning?",
|
||||
"Explain deep learning",
|
||||
"Order a pizza",
|
||||
],
|
||||
model=EMBEDDING_MODEL_NAME,
|
||||
)
|
||||
e0, e1, e2 = [np.array(d.embedding) for d in resp.data]
|
||||
sim_related = float(np.dot(e0, e1))
|
||||
sim_unrelated = float(np.dot(e0, e2))
|
||||
assert sim_related > sim_unrelated, (
|
||||
f"Related ({sim_related:.4f}) should > unrelated ({sim_unrelated:.4f})"
|
||||
)
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Dimension truncation (Matryoshka)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_dimension_truncation(client):
|
||||
"""dimensions parameter truncates and re-normalizes output."""
|
||||
target_dim = 256
|
||||
resp = client.embeddings.create(
|
||||
input="Hello world", model=EMBEDDING_MODEL_NAME, dimensions=target_dim
|
||||
)
|
||||
raw = resp.model_dump()
|
||||
check_embedding_response(
|
||||
raw,
|
||||
model=EMBEDDING_MODEL_NAME,
|
||||
num_embeddings=1,
|
||||
expected_dim=target_dim,
|
||||
)
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Encoding format
|
||||
# ===================================================================
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("launch_embedding_server")
|
||||
def test_base64_encoding():
|
||||
"""base64 encoding format returns base64-encoded embeddings."""
|
||||
resp = requests.post(
|
||||
f"{EMBEDDING_BASE_URL}/embeddings",
|
||||
json={
|
||||
"input": "Hello world",
|
||||
"model": EMBEDDING_MODEL_NAME,
|
||||
"encoding_format": "base64",
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["data"][0]["object"] == "embedding"
|
||||
# base64 string should be a non-empty string (not a list)
|
||||
emb = data["data"][0]["embedding"]
|
||||
assert isinstance(emb, str) and len(emb) > 0
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Error handling — reuses expect_error() pattern from test_server.py
|
||||
# ===================================================================
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("launch_embedding_server")
|
||||
def test_any_model_name_works_with_single_engine():
|
||||
"""When only one embedding engine is served, any model name works.
|
||||
|
||||
This mirrors ServerContext.get_engine() behavior: a single served
|
||||
model is returned regardless of the requested model name.
|
||||
"""
|
||||
resp = requests.post(
|
||||
f"{EMBEDDING_BASE_URL}/embeddings",
|
||||
json={"input": "test", "model": "any-name-works"},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Standalone runner (same pattern as test_server.py __main__)
|
||||
# ===================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
_skip_if_no_model()
|
||||
|
||||
print(f"Using model: {EMBEDDING_MODEL_DIR}")
|
||||
print(f"Using model lib: {EMBEDDING_MODEL_LIB}")
|
||||
print(f"Server URL: {EMBEDDING_BASE_URL}")
|
||||
print(
|
||||
"\nMake sure the embedding server is running, or set env vars "
|
||||
"and use pytest to auto-launch."
|
||||
)
|
||||
|
||||
# Allow running against an already-running server
|
||||
c = OpenAI(base_url=EMBEDDING_BASE_URL, api_key="none")
|
||||
test_models_endpoint()
|
||||
test_single_string_input(c)
|
||||
test_batch_string_input(c)
|
||||
test_batch_index_ordering(c)
|
||||
test_cosine_similarity_via_endpoint(c)
|
||||
test_dimension_truncation(c)
|
||||
test_base64_encoding()
|
||||
test_any_model_name_works_with_single_engine()
|
||||
print("\nAll embedding server tests passed!")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Test script for function call in chat completion. To run this script, use the following command:
|
||||
MLC_SERVE_MODEL_LIB=dist/gorilla-openfunctions-v1-q4f16_1_MLC/gorilla-openfunctions-v1-q4f16_1-cuda.so
|
||||
MLC_SERVE_MODEL_LIB=${MLC_SERVE_MODEL_LIB} python -m pytest -x tests/python/serve/server/test_server_function_call.py
|
||||
""" # noqa: E501
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple # noqa: UP035
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
OPENAI_V1_CHAT_COMPLETION_URL = "http://127.0.0.1:8000/v1/chat/completions"
|
||||
|
||||
|
||||
def check_openai_nonstream_response(
|
||||
response: Dict, # noqa: UP006
|
||||
*,
|
||||
model: str,
|
||||
object_str: str,
|
||||
num_choices: int,
|
||||
finish_reason: List[str], # noqa: UP006
|
||||
completion_tokens: Optional[int] = None,
|
||||
):
|
||||
print(response)
|
||||
assert response["model"] == model
|
||||
assert response["object"] == object_str
|
||||
|
||||
choices = response["choices"]
|
||||
assert isinstance(choices, list)
|
||||
assert len(choices) == num_choices
|
||||
for idx, choice in enumerate(choices):
|
||||
assert choice["index"] == idx
|
||||
assert choice["finish_reason"] in finish_reason
|
||||
|
||||
# text: str
|
||||
message = choice["message"]
|
||||
assert message["role"] == "assistant"
|
||||
if choice["finish_reason"] == "tool_calls":
|
||||
assert message["content"] is None
|
||||
assert isinstance(message["tool_calls"], list)
|
||||
else:
|
||||
assert message["tool_calls"] is None
|
||||
assert message["content"] is not None
|
||||
|
||||
usage = response["usage"]
|
||||
assert isinstance(usage, dict)
|
||||
assert usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"]
|
||||
assert usage["prompt_tokens"] > 0
|
||||
|
||||
if completion_tokens is not None:
|
||||
assert usage["completion_tokens"] == completion_tokens
|
||||
|
||||
|
||||
def check_openai_stream_response(
|
||||
responses: List[Dict], # noqa: UP006
|
||||
*,
|
||||
model: str,
|
||||
object_str: str,
|
||||
num_choices: int,
|
||||
finish_reason: str,
|
||||
echo_prompt: Optional[str] = None,
|
||||
suffix: Optional[str] = None,
|
||||
stop: Optional[List[str]] = None, # noqa: UP006
|
||||
require_substr: Optional[List[str]] = None, # noqa: UP006
|
||||
):
|
||||
assert len(responses) > 0
|
||||
|
||||
finished = [False for _ in range(num_choices)]
|
||||
outputs = ["" for _ in range(num_choices)]
|
||||
for response in responses:
|
||||
assert response["model"] == model
|
||||
assert response["object"] == object_str
|
||||
|
||||
choices = response["choices"]
|
||||
assert isinstance(choices, list)
|
||||
assert len(choices) == num_choices
|
||||
for idx, choice in enumerate(choices):
|
||||
assert choice["index"] == idx
|
||||
|
||||
delta = choice["delta"]
|
||||
assert delta["role"] == "assistant"
|
||||
assert isinstance(delta["content"], str)
|
||||
outputs[idx] += delta["content"]
|
||||
|
||||
if finished[idx]:
|
||||
assert choice["finish_reason"] == finish_reason
|
||||
elif choice["finish_reason"] is not None:
|
||||
assert choice["finish_reason"] == finish_reason
|
||||
finished[idx] = True
|
||||
|
||||
for output in outputs:
|
||||
if echo_prompt is not None:
|
||||
assert output.startswith(echo_prompt)
|
||||
if suffix is not None:
|
||||
assert output.endswith(suffix)
|
||||
if stop is not None:
|
||||
for stop_str in stop:
|
||||
assert stop_str not in output
|
||||
if require_substr is not None:
|
||||
for substr in require_substr:
|
||||
assert substr in output
|
||||
|
||||
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_weather",
|
||||
"description": "Get the current weather in a given location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
},
|
||||
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
CHAT_COMPLETION_MESSAGES = [
|
||||
# messages #0
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the current weather in Pittsburgh, PA?",
|
||||
}
|
||||
],
|
||||
# messages #1
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the current weather in Pittsburgh, PA and Tokyo, JP?",
|
||||
}
|
||||
],
|
||||
# messages #2
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the current weather in Pittsburgh, PA in fahrenheit?",
|
||||
}
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream", [False, True])
|
||||
@pytest.mark.parametrize("messages", CHAT_COMPLETION_MESSAGES)
|
||||
def test_openai_v1_chat_completion_function_call(
|
||||
served_model: Tuple[str, str], # noqa: UP006
|
||||
launch_server,
|
||||
stream: bool,
|
||||
messages: List[Dict[str, str]], # noqa: UP006
|
||||
):
|
||||
# `served_model` and `launch_server` are pytest fixtures
|
||||
# defined in conftest.py.
|
||||
|
||||
payload = {
|
||||
"model": served_model[0],
|
||||
"messages": messages,
|
||||
"stream": stream,
|
||||
"tools": tools,
|
||||
}
|
||||
|
||||
response = requests.post(OPENAI_V1_CHAT_COMPLETION_URL, json=payload, timeout=60)
|
||||
if not stream:
|
||||
check_openai_nonstream_response(
|
||||
response.json(),
|
||||
model=served_model[0],
|
||||
object_str="chat.completion",
|
||||
num_choices=1,
|
||||
finish_reason=["tool_calls", "error"],
|
||||
)
|
||||
else:
|
||||
responses = []
|
||||
for chunk in response.iter_lines(chunk_size=512):
|
||||
if not chunk or chunk == b"data: [DONE]":
|
||||
continue
|
||||
responses.append(json.loads(chunk.decode("utf-8")[6:]))
|
||||
check_openai_stream_response(
|
||||
responses,
|
||||
model=served_model[0],
|
||||
object_str="chat.completion.chunk",
|
||||
num_choices=1,
|
||||
finish_reason="tool_calls",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
model_lib = os.environ.get("MLC_SERVE_MODEL_LIB")
|
||||
if model_lib is None:
|
||||
raise ValueError(
|
||||
'Environment variable "MLC_SERVE_MODEL_LIB" not found. '
|
||||
"Please set it to model lib compiled by MLC LLM "
|
||||
"(e.g., `./dist/gorilla-openfunctions-v1-q4f16_1_MLC/gorilla-openfunctions-v1-q4f16_1-cuda.so`) " # noqa: E501
|
||||
"which supports function calls."
|
||||
)
|
||||
MODEL = (os.path.dirname(model_lib), model_lib)
|
||||
|
||||
for msg in CHAT_COMPLETION_MESSAGES:
|
||||
test_openai_v1_chat_completion_function_call(MODEL, None, stream=False, messages=msg)
|
||||
test_openai_v1_chat_completion_function_call(MODEL, None, stream=True, messages=msg)
|
||||
@@ -0,0 +1,257 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple # noqa: UP035
|
||||
|
||||
import pytest
|
||||
import regex
|
||||
import requests
|
||||
|
||||
OPENAI_V1_CHAT_COMPLETION_URL = "http://127.0.0.1:8001/v1/chat/completions"
|
||||
|
||||
JSON_TOKEN_PATTERN = (
|
||||
r"((-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?)|null|true|false|"
|
||||
r'("((\\["\\\/bfnrt])|(\\u[0-9a-fA-F]{4})|[^"\\\x00-\x1f])*")'
|
||||
)
|
||||
JSON_TOKEN_RE = regex.compile(JSON_TOKEN_PATTERN)
|
||||
|
||||
|
||||
def is_json_or_json_prefix(s: str) -> bool:
|
||||
try:
|
||||
json.loads(s)
|
||||
return True
|
||||
except json.JSONDecodeError as e:
|
||||
# If the JSON decoder reaches the end of s, it is a prefix of a JSON string.
|
||||
if e.pos == len(s):
|
||||
return True
|
||||
# Since json.loads is token-based instead of char-based, there may remain half a token after
|
||||
# the matching position.
|
||||
# If the left part is a prefix of a valid JSON token, the output is also valid
|
||||
regex_match = JSON_TOKEN_RE.fullmatch(s[e.pos :], partial=True)
|
||||
return regex_match is not None
|
||||
|
||||
|
||||
def check_openai_nonstream_response(
|
||||
response: Dict, # noqa: UP006
|
||||
*,
|
||||
is_chat_completion: bool,
|
||||
model: str,
|
||||
object_str: str,
|
||||
num_choices: int,
|
||||
finish_reasons: List[str], # noqa: UP006
|
||||
completion_tokens: Optional[int] = None,
|
||||
echo_prompt: Optional[str] = None,
|
||||
suffix: Optional[str] = None,
|
||||
stop: Optional[List[str]] = None, # noqa: UP006
|
||||
require_substr: Optional[List[str]] = None, # noqa: UP006
|
||||
json_mode: bool = False,
|
||||
):
|
||||
assert response["model"] == model
|
||||
assert response["object"] == object_str
|
||||
|
||||
choices = response["choices"]
|
||||
assert isinstance(choices, list)
|
||||
assert len(choices) <= num_choices
|
||||
texts: List[str] = ["" for _ in range(num_choices)] # noqa: UP006
|
||||
for choice in choices:
|
||||
idx = choice["index"]
|
||||
assert choice["finish_reason"] in finish_reasons
|
||||
|
||||
if not is_chat_completion:
|
||||
assert isinstance(choice["text"], str)
|
||||
texts[idx] = choice["text"]
|
||||
if echo_prompt is not None:
|
||||
assert texts[idx]
|
||||
if suffix is not None:
|
||||
assert texts[idx]
|
||||
else:
|
||||
message = choice["message"]
|
||||
assert message["role"] == "assistant"
|
||||
assert isinstance(message["content"], str)
|
||||
texts[idx] = message["content"]
|
||||
|
||||
if stop is not None:
|
||||
for stop_str in stop:
|
||||
assert stop_str not in texts[idx]
|
||||
if require_substr is not None:
|
||||
for substr in require_substr:
|
||||
assert substr in texts[idx]
|
||||
if json_mode:
|
||||
assert is_json_or_json_prefix(texts[idx])
|
||||
|
||||
usage = response["usage"]
|
||||
assert isinstance(usage, dict)
|
||||
assert usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"]
|
||||
assert usage["prompt_tokens"] > 0
|
||||
if completion_tokens is not None:
|
||||
assert usage["completion_tokens"] == completion_tokens
|
||||
|
||||
|
||||
def check_openai_stream_response(
|
||||
responses: List[Dict], # noqa: UP006
|
||||
*,
|
||||
is_chat_completion: bool,
|
||||
model: str,
|
||||
object_str: str,
|
||||
num_choices: int,
|
||||
finish_reasons: List[str], # noqa: UP006
|
||||
completion_tokens: Optional[int] = None,
|
||||
echo_prompt: Optional[str] = None,
|
||||
suffix: Optional[str] = None,
|
||||
stop: Optional[List[str]] = None, # noqa: UP006
|
||||
require_substr: Optional[List[str]] = None, # noqa: UP006
|
||||
json_mode: bool = False,
|
||||
):
|
||||
assert len(responses) > 0
|
||||
|
||||
finished = [False for _ in range(num_choices)]
|
||||
outputs = ["" for _ in range(num_choices)]
|
||||
for response in responses:
|
||||
assert response["model"] == model
|
||||
assert response["object"] == object_str
|
||||
|
||||
choices = response["choices"]
|
||||
assert isinstance(choices, list)
|
||||
assert len(choices) <= num_choices
|
||||
for choice in choices:
|
||||
idx = choice["index"]
|
||||
|
||||
if not is_chat_completion:
|
||||
assert isinstance(choice["text"], str)
|
||||
outputs[idx] += choice["text"]
|
||||
else:
|
||||
delta = choice["delta"]
|
||||
assert delta["role"] == "assistant"
|
||||
assert isinstance(delta["content"], str)
|
||||
outputs[idx] += delta["content"]
|
||||
|
||||
if finished[idx]:
|
||||
assert choice["finish_reason"] in finish_reasons
|
||||
elif choice["finish_reason"] is not None:
|
||||
assert choice["finish_reason"] in finish_reasons
|
||||
finished[idx] = True
|
||||
|
||||
if not is_chat_completion:
|
||||
usage = response["usage"]
|
||||
assert isinstance(usage, dict)
|
||||
assert usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"]
|
||||
assert usage["prompt_tokens"] > 0
|
||||
if completion_tokens is not None:
|
||||
assert usage["completion_tokens"] <= completion_tokens
|
||||
|
||||
if not is_chat_completion:
|
||||
if completion_tokens is not None:
|
||||
assert responses[-1]["usage"]["completion_tokens"] == completion_tokens
|
||||
|
||||
for i, output in enumerate(outputs):
|
||||
if echo_prompt is not None:
|
||||
assert output.startswith(echo_prompt)
|
||||
if suffix is not None:
|
||||
assert output.endswith(suffix)
|
||||
if stop is not None:
|
||||
for stop_str in stop:
|
||||
assert stop_str not in output
|
||||
if require_substr is not None:
|
||||
for substr in require_substr:
|
||||
assert substr in output
|
||||
if json_mode:
|
||||
assert is_json_or_json_prefix(output)
|
||||
|
||||
|
||||
CHAT_COMPLETION_MESSAGES = [
|
||||
# messages #0
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": "https://llava-vl.github.io/static/images/view.jpg",
|
||||
},
|
||||
{"type": "text", "text": "What does this image represent?"},
|
||||
],
|
||||
},
|
||||
],
|
||||
# messages #1
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": "https://llava-vl.github.io/static/images/view.jpg",
|
||||
},
|
||||
{"type": "text", "text": "What does this image represent?"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The image represents a serene and peaceful scene of a pier extending over a body of water, such as a lake or a river.er. The pier is made of wood and has a bench on it, providing a place for people to sit and enjoy the view. The pier is situated in a natural environment, surrounded by trees and mountains in the background. This setting creates a tranquil atmosphere, inviting visitors to relax and appreciate the beauty of the landscape.", # noqa: E501
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What country is the image set in? Give me 10 ranked guesses and reasons why.", # noqa: E501
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream", [False, True])
|
||||
@pytest.mark.parametrize("messages", CHAT_COMPLETION_MESSAGES)
|
||||
def test_openai_v1_chat_completions(
|
||||
served_model: Tuple[str, str], # noqa: UP006
|
||||
launch_server,
|
||||
stream: bool,
|
||||
messages: List[Dict[str, str]], # noqa: UP006
|
||||
):
|
||||
# `served_model` and `launch_server` are pytest fixtures
|
||||
# defined in conftest.py.
|
||||
|
||||
payload = {
|
||||
"model": served_model[0],
|
||||
"messages": messages,
|
||||
"stream": stream,
|
||||
}
|
||||
response = requests.post(OPENAI_V1_CHAT_COMPLETION_URL, json=payload, timeout=180)
|
||||
if not stream:
|
||||
check_openai_nonstream_response(
|
||||
response.json(),
|
||||
is_chat_completion=True,
|
||||
model=served_model[0],
|
||||
object_str="chat.completion",
|
||||
num_choices=1,
|
||||
finish_reasons=["stop"],
|
||||
)
|
||||
else:
|
||||
responses = []
|
||||
for chunk in response.iter_lines(chunk_size=512):
|
||||
if not chunk or chunk == b"data: [DONE]":
|
||||
continue
|
||||
responses.append(json.loads(chunk.decode("utf-8")[6:]))
|
||||
check_openai_stream_response(
|
||||
responses,
|
||||
is_chat_completion=True,
|
||||
model=served_model[0],
|
||||
object_str="chat.completion.chunk",
|
||||
num_choices=1,
|
||||
finish_reasons=["stop"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
model_lib = os.environ.get("MLC_SERVE_MODEL_LIB")
|
||||
if model_lib is None:
|
||||
raise ValueError(
|
||||
'Environment variable "MLC_SERVE_MODEL_LIB" not found. '
|
||||
"Please set it to model lib compiled by MLC LLM "
|
||||
"(e.g., `dist/Llama-2-7b-chat-hf-q0f16-MLC/Llama-2-7b-chat-hf-q0f16-MLC-cuda.so`)."
|
||||
)
|
||||
|
||||
model = os.environ.get("MLC_SERVE_MODEL")
|
||||
if model is None:
|
||||
MODEL = (os.path.dirname(model_lib), model_lib)
|
||||
else:
|
||||
MODEL = (model, model_lib)
|
||||
|
||||
for msg in CHAT_COMPLETION_MESSAGES:
|
||||
test_openai_v1_chat_completions(MODEL, None, stream=False, messages=msg)
|
||||
test_openai_v1_chat_completions(MODEL, None, stream=True, messages=msg)
|
||||
@@ -0,0 +1,365 @@
|
||||
"""Embedding engine tests in MLC LLM.
|
||||
|
||||
Tests AsyncEmbeddingEngine for both direct (sync) and async embedding inference.
|
||||
Reuses MLC LLM test infrastructure: markers, require_test_model pattern,
|
||||
and conventions from test_serve_engine.py.
|
||||
|
||||
Run with real model (requires GPU + compiled embedding model):
|
||||
MLC_SERVE_EMBEDDING_MODEL_LIB="path/to/model.dylib" \
|
||||
pytest -m engine tests/python/serve/test_embedding_engine.py -v
|
||||
|
||||
Environment variables:
|
||||
MLC_SERVE_EMBEDDING_MODEL_LIB Path to compiled embedding model library (required)
|
||||
MLC_SERVE_EMBEDDING_MODEL Path to embedding model weight directory
|
||||
(optional, defaults to dirname of model lib)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Reuse MLC LLM marker system (registered in tests/python/conftest.py)
|
||||
pytestmark = [pytest.mark.engine]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures — follows pattern from serve/server/conftest.py (served_model)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
EMBEDDING_MODEL_LIB = os.environ.get("MLC_SERVE_EMBEDDING_MODEL_LIB")
|
||||
EMBEDDING_MODEL_DIR = os.environ.get(
|
||||
"MLC_SERVE_EMBEDDING_MODEL",
|
||||
os.path.dirname(EMBEDDING_MODEL_LIB) if EMBEDDING_MODEL_LIB else None,
|
||||
)
|
||||
|
||||
|
||||
def _skip_if_no_model():
|
||||
if EMBEDDING_MODEL_LIB is None:
|
||||
pytest.skip(
|
||||
'Environment variable "MLC_SERVE_EMBEDDING_MODEL_LIB" not found. '
|
||||
"Set it to a compiled embedding model library "
|
||||
"(e.g., Qwen3-Embedding-0.6B-q0f32-MLC.dylib)."
|
||||
)
|
||||
if not os.path.isfile(EMBEDDING_MODEL_LIB):
|
||||
pytest.skip(f"Embedding model library not found at: {EMBEDDING_MODEL_LIB}")
|
||||
if EMBEDDING_MODEL_DIR is None or not os.path.isdir(EMBEDDING_MODEL_DIR):
|
||||
pytest.skip(f"Embedding model directory not found at: {EMBEDDING_MODEL_DIR}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def embedding_engine():
|
||||
"""Module-scoped AsyncEmbeddingEngine — loaded once, shared across tests."""
|
||||
_skip_if_no_model()
|
||||
from mlc_llm.serve.embedding_engine import AsyncEmbeddingEngine
|
||||
|
||||
engine = AsyncEmbeddingEngine(
|
||||
model=EMBEDDING_MODEL_DIR,
|
||||
model_lib=EMBEDDING_MODEL_LIB,
|
||||
device="auto",
|
||||
)
|
||||
yield engine
|
||||
engine.terminate()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers — reuse cosine_similarity pattern from test_serve_engine.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def cosine_similarity(a, b):
|
||||
"""Return cosine similarity between two vectors."""
|
||||
a, b = np.array(a), np.array(b)
|
||||
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Engine initialization tests
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_engine_model_type(embedding_engine):
|
||||
"""Engine reports a valid model type."""
|
||||
assert embedding_engine.model_type in ("encoder", "decoder")
|
||||
|
||||
|
||||
def test_engine_pooling_strategy(embedding_engine):
|
||||
"""Engine selects appropriate default pooling strategy."""
|
||||
if embedding_engine.model_type == "encoder":
|
||||
assert embedding_engine.pooling_strategy == "cls"
|
||||
else:
|
||||
assert embedding_engine.pooling_strategy == "last"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Single-text embedding
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_single_text_shape(embedding_engine):
|
||||
"""Single text returns exactly one embedding vector."""
|
||||
embeddings, tokens = embedding_engine.embed(["Hello world"])
|
||||
assert len(embeddings) == 1
|
||||
assert len(embeddings[0]) > 0
|
||||
assert tokens > 0
|
||||
|
||||
|
||||
def test_single_text_unit_norm(embedding_engine):
|
||||
"""Embedding output is L2-normalized."""
|
||||
embeddings, _ = embedding_engine.embed(["Hello world"])
|
||||
norm = float(np.linalg.norm(embeddings[0]))
|
||||
assert abs(norm - 1.0) < 1e-4, f"Expected unit norm, got {norm}"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Batch embedding
|
||||
# ===================================================================
|
||||
|
||||
BATCH_TEXTS = [
|
||||
"Machine learning is fascinating",
|
||||
"I love pizza",
|
||||
"Deep learning uses neural networks",
|
||||
]
|
||||
|
||||
|
||||
def test_batch_count(embedding_engine):
|
||||
"""Batch embedding returns one vector per input."""
|
||||
embeddings, tokens = embedding_engine.embed(BATCH_TEXTS)
|
||||
assert len(embeddings) == len(BATCH_TEXTS)
|
||||
assert tokens > 0
|
||||
|
||||
|
||||
def test_batch_all_normalized(embedding_engine):
|
||||
"""Every vector in a batch is L2-normalized."""
|
||||
embeddings, _ = embedding_engine.embed(BATCH_TEXTS)
|
||||
for i, emb in enumerate(embeddings):
|
||||
norm = float(np.linalg.norm(emb))
|
||||
assert abs(norm - 1.0) < 1e-4, f"Embedding [{i}] norm={norm}"
|
||||
|
||||
|
||||
def test_batch_consistent_dimension(embedding_engine):
|
||||
"""All embeddings in a batch have the same dimension."""
|
||||
embeddings, _ = embedding_engine.embed(BATCH_TEXTS)
|
||||
dims = {len(emb) for emb in embeddings}
|
||||
assert len(dims) == 1, f"Inconsistent dimensions: {dims}"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Semantic quality — cosine similarity ranking
|
||||
# ===================================================================
|
||||
|
||||
SIMILARITY_TEXTS = [
|
||||
"What is machine learning?",
|
||||
"Explain deep learning algorithms",
|
||||
"I want to order pizza",
|
||||
]
|
||||
|
||||
|
||||
def test_cosine_similarity_ranking(embedding_engine):
|
||||
"""Related texts have higher cosine similarity than unrelated texts."""
|
||||
embeddings, _ = embedding_engine.embed(SIMILARITY_TEXTS)
|
||||
e_ml, e_dl, e_pizza = [np.array(e) for e in embeddings]
|
||||
sim_related = float(np.dot(e_ml, e_dl))
|
||||
sim_unrelated = float(np.dot(e_ml, e_pizza))
|
||||
assert sim_related > sim_unrelated, (
|
||||
f"Related sim ({sim_related:.4f}) should > unrelated sim ({sim_unrelated:.4f})"
|
||||
)
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Determinism
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_deterministic_output(embedding_engine):
|
||||
"""Same input produces identical output across calls."""
|
||||
text = ["Deterministic test"]
|
||||
emb1, _ = embedding_engine.embed(text)
|
||||
emb2, _ = embedding_engine.embed(text)
|
||||
cos = cosine_similarity(emb1[0], emb2[0])
|
||||
assert cos > 0.9999, f"Expected deterministic output, cosine={cos}"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Async embedding
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_async_embed(embedding_engine):
|
||||
"""async_embed produces same result as sync embed."""
|
||||
text = ["Async test"]
|
||||
sync_emb, sync_tokens = embedding_engine.embed(text)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
async_emb, async_tokens = loop.run_until_complete(embedding_engine.async_embed(text))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
assert sync_tokens == async_tokens
|
||||
cos = cosine_similarity(sync_emb[0], async_emb[0])
|
||||
assert cos > 0.9999, f"Async vs sync mismatch, cosine={cos}"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Edge cases
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_empty_string(embedding_engine):
|
||||
"""Empty string should still produce a valid embedding for supported models."""
|
||||
embeddings, tokens = embedding_engine.embed([""])
|
||||
if embedding_engine.model_type == "encoder":
|
||||
assert len(embeddings) == 1
|
||||
assert len(embeddings[0]) > 0
|
||||
assert tokens > 0
|
||||
else:
|
||||
assert len(embeddings) == 1
|
||||
assert len(embeddings[0]) > 0
|
||||
assert tokens > 0
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Long text handling (model-type dependent)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
def test_long_text_decoder_chunked_prefill(embedding_engine):
|
||||
"""[Decoder only] Text >prefill_chunk_size triggers chunked prefill.
|
||||
~5000 tokens processed in 3 chunks. Result is unit-norm embedding."""
|
||||
if embedding_engine.model_type != "decoder":
|
||||
pytest.skip("Chunked prefill is decoder-only")
|
||||
long_text = "word " * 5000
|
||||
embeddings, tokens = embedding_engine.embed([long_text])
|
||||
assert tokens > 2048, f"Expected >2048 tokens to trigger chunking, got {tokens}"
|
||||
norm = float(np.linalg.norm(embeddings[0]))
|
||||
assert abs(norm - 1.0) < 1e-3
|
||||
|
||||
|
||||
def _get_encoder_tokens(embedding_engine, text):
|
||||
"""Replicate encoder preprocessing: tokenize and add [CLS]/[SEP]."""
|
||||
tokens = list(embedding_engine.tokenizer.encode(text))
|
||||
if embedding_engine._cls_token_id is not None and (
|
||||
len(tokens) == 0 or tokens[0] != embedding_engine._cls_token_id
|
||||
):
|
||||
tokens = [embedding_engine._cls_token_id, *tokens]
|
||||
if embedding_engine._sep_token_id is not None and (
|
||||
len(tokens) == 0 or tokens[-1] != embedding_engine._sep_token_id
|
||||
):
|
||||
tokens = [*tokens, embedding_engine._sep_token_id]
|
||||
return tokens
|
||||
|
||||
|
||||
def test_long_text_encoder_truncation(embedding_engine):
|
||||
"""[Encoder only] Text exceeding prefill_chunk_size is truncated.
|
||||
Two texts with the same shared prefix but different suffixes beyond the
|
||||
limit should produce identical embeddings, since the suffix is truncated
|
||||
and the retained token prefixes are verified to be identical."""
|
||||
if embedding_engine.model_type != "encoder":
|
||||
pytest.skip("Truncation test is encoder-only")
|
||||
prefill_chunk = embedding_engine._metadata.get("prefill_chunk_size", 512)
|
||||
|
||||
# Dynamically construct input that exceeds prefill_chunk_size.
|
||||
unit = "machine learning is great "
|
||||
suffix_a = " alpha beta gamma " * 200
|
||||
suffix_b = " totally different ending " * 200
|
||||
unit_tokens = len(list(embedding_engine.tokenizer.encode(unit)))
|
||||
repeats = max(1, prefill_chunk // max(unit_tokens, 1) + 64)
|
||||
|
||||
# Increase prefix length until both inputs exceed prefill_chunk_size
|
||||
# and their truncated token prefixes are identical.
|
||||
while True:
|
||||
shared_prefix = unit * repeats
|
||||
full_tokens_a = _get_encoder_tokens(embedding_engine, shared_prefix + suffix_a)
|
||||
full_tokens_b = _get_encoder_tokens(embedding_engine, shared_prefix + suffix_b)
|
||||
if (
|
||||
len(full_tokens_a) > prefill_chunk
|
||||
and len(full_tokens_b) > prefill_chunk
|
||||
and full_tokens_a[:prefill_chunk] == full_tokens_b[:prefill_chunk]
|
||||
):
|
||||
break
|
||||
repeats += 64
|
||||
assert repeats < 200000, "Failed to construct truncation test inputs"
|
||||
|
||||
text_a = shared_prefix + suffix_a
|
||||
text_b = shared_prefix + suffix_b
|
||||
|
||||
emb_a, tokens_a = embedding_engine.embed([text_a])
|
||||
emb_b, tokens_b = embedding_engine.embed([text_b])
|
||||
|
||||
# Verify truncation happened
|
||||
assert tokens_a <= prefill_chunk, (
|
||||
f"Encoder should truncate to {prefill_chunk}, got {tokens_a} tokens"
|
||||
)
|
||||
assert tokens_b <= prefill_chunk
|
||||
# Both should be valid unit-norm embeddings
|
||||
assert abs(float(np.linalg.norm(emb_a[0])) - 1.0) < 1e-3
|
||||
assert abs(float(np.linalg.norm(emb_b[0])) - 1.0) < 1e-3
|
||||
|
||||
# Both truncated to identical token sequences → embeddings must match
|
||||
cos = cosine_similarity(emb_a[0], emb_b[0])
|
||||
assert cos > 0.999, f"Same truncated tokens should match, cosine={cos:.6f}"
|
||||
|
||||
|
||||
def test_long_vs_short_semantic_quality(embedding_engine):
|
||||
"""Long text should still capture semantic meaning correctly.
|
||||
Decoder: chunked prefill preserves full context.
|
||||
Encoder: truncation keeps most relevant prefix."""
|
||||
short_ml = "Machine learning enables systems to learn from data"
|
||||
long_ml = (
|
||||
"Machine learning is a fascinating field of study. " * 200
|
||||
+ "It enables systems to learn from data."
|
||||
)
|
||||
pizza = "I want to order a pepperoni pizza for dinner"
|
||||
|
||||
embs, _ = embedding_engine.embed([short_ml, long_ml, pizza])
|
||||
e_short, e_long, e_pizza = [np.array(e) for e in embs]
|
||||
|
||||
sim_same_topic = float(np.dot(e_short, e_long))
|
||||
sim_different = float(np.dot(e_short, e_pizza))
|
||||
assert sim_same_topic > sim_different, (
|
||||
f"Same topic ({sim_same_topic:.4f}) should > different ({sim_different:.4f})"
|
||||
)
|
||||
|
||||
|
||||
def test_unicode_text(embedding_engine):
|
||||
"""Unicode input is handled correctly."""
|
||||
texts = ["Привет мир", "你好世界", "こんにちは世界"]
|
||||
embeddings, _ = embedding_engine.embed(texts)
|
||||
assert len(embeddings) == 3
|
||||
for emb in embeddings:
|
||||
assert abs(float(np.linalg.norm(emb)) - 1.0) < 1e-4
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Standalone runner (like test_serve_engine.py)
|
||||
# ===================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
_skip_if_no_model()
|
||||
from mlc_llm.serve.embedding_engine import AsyncEmbeddingEngine
|
||||
|
||||
engine = AsyncEmbeddingEngine(
|
||||
model=EMBEDDING_MODEL_DIR,
|
||||
model_lib=EMBEDDING_MODEL_LIB,
|
||||
device="auto",
|
||||
)
|
||||
try:
|
||||
test_engine_model_type(engine)
|
||||
test_engine_pooling_strategy(engine)
|
||||
test_single_text_shape(engine)
|
||||
test_single_text_unit_norm(engine)
|
||||
test_batch_count(engine)
|
||||
test_batch_all_normalized(engine)
|
||||
test_batch_consistent_dimension(engine)
|
||||
test_cosine_similarity_ranking(engine)
|
||||
test_deterministic_output(engine)
|
||||
test_async_embed(engine)
|
||||
test_empty_string(engine)
|
||||
test_long_text_decoder_chunked_prefill(engine)
|
||||
test_long_text_encoder_truncation(engine)
|
||||
test_long_vs_short_semantic_quality(engine)
|
||||
test_unicode_text(engine)
|
||||
print("\nAll embedding engine tests passed!")
|
||||
finally:
|
||||
engine.terminate()
|
||||
@@ -0,0 +1,48 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from mlc_llm.serve.event_trace_recorder import EventTraceRecorder
|
||||
|
||||
# test category "unittest"
|
||||
pytestmark = [pytest.mark.unittest]
|
||||
|
||||
|
||||
def test_event_trace_recorder():
|
||||
trace_recorder = EventTraceRecorder()
|
||||
request_ids = ["x", "y"]
|
||||
num_decode = 5
|
||||
|
||||
for request_id in request_ids:
|
||||
trace_recorder.add_event(request_id, event="start tokenization")
|
||||
trace_recorder.add_event(request_id, event="finish tokenization")
|
||||
trace_recorder.add_event(request_id, event="add request")
|
||||
trace_recorder.add_event(request_id, event="start embed")
|
||||
trace_recorder.add_event(request_id, event="finish embed")
|
||||
trace_recorder.add_event(request_id, event="start prefill")
|
||||
trace_recorder.add_event(request_id, event="finish prefill")
|
||||
|
||||
for _ in range(num_decode):
|
||||
for request_id in request_ids:
|
||||
trace_recorder.add_event(request_id, event="start decode")
|
||||
trace_recorder.add_event(request_id, event="finish decode")
|
||||
for request_id in request_ids:
|
||||
trace_recorder.add_event(request_id, event="start detokenization")
|
||||
trace_recorder.add_event(request_id, event="finish detokenization")
|
||||
|
||||
events = json.loads(trace_recorder.dump_json())
|
||||
decode_count = {}
|
||||
for event in events:
|
||||
request_id = event["tid"]
|
||||
if event["name"].startswith("decode"):
|
||||
if request_id not in decode_count:
|
||||
decode_count[request_id] = 1
|
||||
else:
|
||||
decode_count[request_id] += 1
|
||||
|
||||
for _, decode_cnt in decode_count.items():
|
||||
assert decode_cnt == num_decode * 2, decode_cnt
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_event_trace_recorder()
|
||||
@@ -0,0 +1,138 @@
|
||||
import pytest
|
||||
|
||||
from mlc_llm.serve import PagedRadixTree
|
||||
|
||||
# category "runtime_module"
|
||||
pytestmark = [pytest.mark.unittest]
|
||||
|
||||
|
||||
def test_add():
|
||||
prt = PagedRadixTree()
|
||||
prt.add(0)
|
||||
assert list(prt.get(0)) == []
|
||||
prt.add(1)
|
||||
assert list(prt.get(1)) == []
|
||||
|
||||
|
||||
def test_remove():
|
||||
prt = PagedRadixTree()
|
||||
capacity = prt.free_capacity()
|
||||
prt.add(0)
|
||||
prt.remove(0)
|
||||
prt.add(0)
|
||||
prt.extend(0, [1 for _ in range(200)])
|
||||
prt.remove(0)
|
||||
assert prt.free_capacity() == capacity
|
||||
|
||||
prt.add(1)
|
||||
prt.extend(1, [1 for _ in range(200)])
|
||||
capacity = prt.free_capacity()
|
||||
prt.add(2)
|
||||
prt.extend(2, [1 for _ in range(100)] + [2 for _ in range(100)])
|
||||
prt.remove(2)
|
||||
assert prt.free_capacity() == capacity
|
||||
|
||||
prt.add(3)
|
||||
prt.extend(3, [1 for _ in range(200)])
|
||||
prt.remove(3)
|
||||
assert prt.free_capacity() == capacity
|
||||
|
||||
prt.add(4)
|
||||
prt.add(5)
|
||||
prt.add(6)
|
||||
assert prt.free_capacity() == capacity
|
||||
prt.remove(4)
|
||||
assert prt.free_capacity() == capacity
|
||||
prt.remove(5)
|
||||
assert prt.free_capacity() == capacity
|
||||
prt.remove(6)
|
||||
assert prt.free_capacity() == capacity
|
||||
|
||||
|
||||
def test_extend():
|
||||
prt = PagedRadixTree()
|
||||
L = prt.free_capacity() // 64
|
||||
H = L // 2
|
||||
Q = L // 4
|
||||
seq_id = 0
|
||||
for start_pos in [0, H, L, L + H]:
|
||||
for length in [Q, L - H, L, 2 * L - H, 2 * L]:
|
||||
prt.add(seq_id)
|
||||
if start_pos:
|
||||
tokens_1 = [seq_id for _ in range(start_pos)]
|
||||
prt.extend(seq_id, tokens_1)
|
||||
assert list(prt.get(seq_id)) == tokens_1
|
||||
else:
|
||||
tokens_1 = []
|
||||
tokens_2 = [seq_id for _ in range(length)]
|
||||
prt.extend(seq_id, tokens_2)
|
||||
assert list(prt.get(seq_id)) == tokens_1 + tokens_2
|
||||
seq_id += 1
|
||||
|
||||
|
||||
def test_fork():
|
||||
prt = PagedRadixTree()
|
||||
L = prt.free_capacity() // 64
|
||||
H = L // 2
|
||||
Q = L // 4
|
||||
seq_id = 0
|
||||
length_list = [Q, H, L, L + Q, L + H, L * 2]
|
||||
for p_idx in range(1, len(length_list)):
|
||||
for c_idx in range(0, p_idx + 1):
|
||||
prt.add(seq_id)
|
||||
tokens = [seq_id for _ in range(length_list[p_idx])]
|
||||
prt.extend(seq_id, tokens)
|
||||
prt.fork(seq_id + 1, seq_id, length_list[c_idx])
|
||||
assert list(prt.get(seq_id + 1)) == tokens[: length_list[c_idx]]
|
||||
seq_id += 2
|
||||
|
||||
|
||||
def test_fork_2():
|
||||
prt = PagedRadixTree()
|
||||
prt.add(0)
|
||||
prt.extend(0, [0, 1, 2, 3])
|
||||
prt.fork(1, 0, 3)
|
||||
prt.extend(1, [4])
|
||||
prt.fork(2, 0, 3)
|
||||
prt.extend(2, [5])
|
||||
assert prt.match([0, 1, 2, 4]) == (4, (1,))
|
||||
assert prt.match([0, 1, 2, 5]) == (4, (2,))
|
||||
|
||||
|
||||
def test_rollback():
|
||||
prt = PagedRadixTree()
|
||||
L = prt.free_capacity() // 64
|
||||
H = L // 2
|
||||
Q = L // 4
|
||||
seq_id = 0
|
||||
for start_pos in [H, L, L + H, 2 * L, 3 * L + H]:
|
||||
for length in [Q, H, L + Q, 2 * L, 2 * L + Q]:
|
||||
if length > start_pos:
|
||||
continue
|
||||
prt.add(seq_id)
|
||||
tokens = [seq_id for _ in range(start_pos)]
|
||||
prt.extend(seq_id, tokens)
|
||||
prt.rollback(seq_id, length)
|
||||
assert list(prt.get(seq_id)) == tokens[:-length]
|
||||
seq_id += 1
|
||||
|
||||
for start_pos in [H, L, L + H, 2 * L, 3 * L + H]:
|
||||
for length in [Q, H, L + Q, 2 * L, 2 * L + Q]:
|
||||
if length > start_pos:
|
||||
continue
|
||||
prt.add(seq_id)
|
||||
tokens = [seq_id for _ in range(start_pos)]
|
||||
prt.extend(seq_id, tokens)
|
||||
prt.fork(seq_id + 1, seq_id, start_pos)
|
||||
prt.rollback(seq_id + 1, length)
|
||||
assert list(prt.get(seq_id + 1)) == tokens[:-length]
|
||||
seq_id += 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_add()
|
||||
test_remove()
|
||||
test_extend()
|
||||
test_fork()
|
||||
test_fork_2()
|
||||
test_rollback()
|
||||
@@ -0,0 +1,285 @@
|
||||
import asyncio
|
||||
from typing import List # noqa: UP035
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve import AsyncMLCEngine, EngineConfig
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
prompts = [
|
||||
"What is the meaning of life?",
|
||||
"Introduce the history of Pittsburgh to me. Please elaborate in detail.",
|
||||
"Write a three-day Seattle travel plan. Please elaborate in detail.",
|
||||
"What is Alaska famous of? Please elaborate in detail.",
|
||||
"What is the difference between Lambda calculus and Turing machine? Please elaborate in detail.", # noqa: E501
|
||||
"What are the necessary components to assemble a desktop computer? Please elaborate in detail.",
|
||||
"Why is Vitamin D important to human beings? Please elaborate in detail.",
|
||||
"Where is milk tea originated from? Please elaborate in detail.",
|
||||
"Where is the southernmost place in United States? Please elaborate in detail.",
|
||||
"Do you know AlphaGo? What capabilities does it have, and what achievements has it got? Please elaborate in detail.", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q4f16_1-MLC")
|
||||
async def test_engine_generate(model: str):
|
||||
# Create engine
|
||||
async_engine = AsyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
|
||||
num_requests = 10
|
||||
max_tokens = 256
|
||||
generation_cfg = GenerationConfig(max_tokens=max_tokens, n=7)
|
||||
|
||||
output_texts: List[List[str]] = [ # noqa: UP006
|
||||
["" for _ in range(generation_cfg.n)] for _ in range(num_requests)
|
||||
]
|
||||
|
||||
async def generate_task(
|
||||
async_engine: AsyncMLCEngine,
|
||||
prompt: str,
|
||||
generation_cfg: GenerationConfig,
|
||||
request_id: str,
|
||||
):
|
||||
print(f"generate task for request {request_id}")
|
||||
rid = int(request_id)
|
||||
async for delta_outputs in async_engine._generate(
|
||||
prompt, generation_cfg, request_id=request_id
|
||||
):
|
||||
if len(delta_outputs) == generation_cfg.n:
|
||||
for i, delta_output in enumerate(delta_outputs):
|
||||
output_texts[rid][i] += delta_output.delta_text
|
||||
else:
|
||||
assert len(delta_outputs) == 1
|
||||
assert len(delta_outputs[0].request_final_usage_json_str) != 0
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(
|
||||
generate_task(async_engine, prompts[i], generation_cfg, request_id=str(i))
|
||||
)
|
||||
for i in range(num_requests)
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Print output.
|
||||
print("All finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
async_engine.terminate()
|
||||
del async_engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q4f16_1-MLC")
|
||||
async def test_chat_completion(model: str):
|
||||
# Create engine
|
||||
async_engine = AsyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 32
|
||||
n = 1
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
async def generate_task(prompt: str, request_id: str):
|
||||
print(f"generate chat completion task for request {request_id}")
|
||||
rid = int(request_id)
|
||||
async for response in await async_engine.chat.completions.create( # noqa: F821
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=request_id,
|
||||
stream=True,
|
||||
):
|
||||
for choice in response.choices:
|
||||
assert choice.delta.role == "assistant"
|
||||
assert isinstance(choice.delta.content, str)
|
||||
output_texts[rid][choice.index] += choice.delta.content
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(generate_task(prompts[i], request_id=str(i)))
|
||||
for i in range(num_requests)
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Print output.
|
||||
print("Chat completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
async_engine.terminate()
|
||||
del async_engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q4f16_1-MLC")
|
||||
async def test_chat_completion_non_stream(model: str):
|
||||
# Create engine
|
||||
async_engine = AsyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 32
|
||||
n = 1
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
async def generate_task(prompt: str, request_id: str):
|
||||
print(f"generate chat completion task for request {request_id}")
|
||||
rid = int(request_id)
|
||||
response = await async_engine.chat.completions.create( # noqa: F821
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=request_id,
|
||||
)
|
||||
for choice in response.choices:
|
||||
assert choice.message.role == "assistant"
|
||||
assert isinstance(choice.message.content, str)
|
||||
output_texts[rid][choice.index] += choice.message.content
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(generate_task(prompts[i], request_id=str(i)))
|
||||
for i in range(num_requests)
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Print output.
|
||||
print("Chat completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
async_engine.terminate()
|
||||
del async_engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
async def test_completion(model: str):
|
||||
# Create engine
|
||||
async_engine = AsyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 128
|
||||
n = 1
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
async def generate_task(prompt: str, request_id: str):
|
||||
print(f"generate completion task for request {request_id}")
|
||||
rid = int(request_id)
|
||||
async for response in await async_engine.completions.create( # noqa: F821
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=request_id,
|
||||
stream=True,
|
||||
extra_body={"debug_config": {"ignore_eos": True}},
|
||||
):
|
||||
for choice in response.choices:
|
||||
output_texts[rid][choice.index] += choice.text
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(generate_task(prompts[i], request_id=str(i)))
|
||||
for i in range(num_requests)
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Print output.
|
||||
print("Completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
async_engine.terminate()
|
||||
del async_engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
async def test_completion_non_stream(model: str):
|
||||
# Create engine
|
||||
async_engine = AsyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 128
|
||||
n = 1
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
async def generate_task(prompt: str, request_id: str):
|
||||
print(f"generate completion task for request {request_id}")
|
||||
rid = int(request_id)
|
||||
response = await async_engine.completions.create( # noqa: F821
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=request_id,
|
||||
extra_body={"debug_config": {"ignore_eos": True}},
|
||||
)
|
||||
for choice in response.choices:
|
||||
output_texts[rid][choice.index] += choice.text
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(generate_task(prompts[i], request_id=str(i)))
|
||||
for i in range(num_requests)
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Print output.
|
||||
print("Completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
async_engine.terminate()
|
||||
del async_engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_engine_generate())
|
||||
asyncio.run(test_chat_completion())
|
||||
asyncio.run(test_chat_completion_non_stream())
|
||||
asyncio.run(test_completion())
|
||||
asyncio.run(test_completion_non_stream())
|
||||
@@ -0,0 +1,84 @@
|
||||
import asyncio
|
||||
from typing import List # noqa: UP035
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve import AsyncMLCEngine, EngineConfig
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
prompts = [
|
||||
"What is the meaning of life?",
|
||||
"Introduce the history of Pittsburgh to me. Please elaborate in detail.",
|
||||
"Write a three-day Seattle travel plan. Please elaborate in detail.",
|
||||
"What is Alaska famous of? Please elaborate in detail.",
|
||||
"What is the difference between Lambda calculus and Turing machine? Please elaborate in detail.", # noqa: E501
|
||||
"What are the necessary components to assemble a desktop computer? Please elaborate in detail.",
|
||||
"Why is Vitamin D important to human beings? Please elaborate in detail.",
|
||||
"Where is milk tea originated from? Please elaborate in detail.",
|
||||
"Where is the southernmost place in United States? Please elaborate in detail.",
|
||||
"Do you know AlphaGo? What capabilities does it have, and what achievements has it got? Please elaborate in detail.", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
@require_test_model(
|
||||
"Llama-2-7b-chat-hf-q0f16-MLC",
|
||||
"Llama-2-7b-chat-hf-q4f16_1-MLC",
|
||||
)
|
||||
async def test_engine_generate(model: str, small_model: str):
|
||||
# Create engine
|
||||
async_engine = AsyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
additional_models=[small_model],
|
||||
speculative_mode="small_draft",
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 10
|
||||
max_tokens = 256
|
||||
generation_cfg = GenerationConfig(max_tokens=max_tokens)
|
||||
|
||||
output_texts: List[List[str]] = [ # noqa: UP006
|
||||
["" for _ in range(generation_cfg.n)] for _ in range(num_requests)
|
||||
]
|
||||
|
||||
async def generate_task(
|
||||
async_engine: AsyncMLCEngine,
|
||||
prompt: str,
|
||||
generation_cfg: GenerationConfig,
|
||||
request_id: str,
|
||||
):
|
||||
print(f"generate task for request {request_id}")
|
||||
rid = int(request_id)
|
||||
async for delta_outputs in async_engine._generate(
|
||||
prompt, generation_cfg, request_id=request_id
|
||||
):
|
||||
assert len(delta_outputs) == generation_cfg.n
|
||||
for i, delta_output in enumerate(delta_outputs):
|
||||
output_texts[rid][i] += delta_output.delta_text
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(
|
||||
generate_task(async_engine, prompts[i], generation_cfg, request_id=str(i))
|
||||
)
|
||||
for i in range(num_requests)
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Print output.
|
||||
print("All finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
async_engine.terminate()
|
||||
del async_engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_engine_generate())
|
||||
@@ -0,0 +1,241 @@
|
||||
from typing import List # noqa: UP035
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve import EngineConfig, MLCEngine
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
prompts = [
|
||||
"What is the meaning of life?",
|
||||
"Introduce the history of Pittsburgh to me. Please elaborate in detail.",
|
||||
"Write a three-day Seattle travel plan. Please elaborate in detail.",
|
||||
"What is Alaska famous of? Please elaborate in detail.",
|
||||
"What is the difference between Lambda calculus and Turing machine? Please elaborate in detail.", # noqa: E501
|
||||
"What are the necessary components to assemble a desktop computer? Please elaborate in detail.",
|
||||
"Why is Vitamin D important to human beings? Please elaborate in detail.",
|
||||
"Where is milk tea originated from? Please elaborate in detail.",
|
||||
"Where is the southernmost place in United States? Please elaborate in detail.",
|
||||
"Do you know AlphaGo? What capabilities does it have, and what achievements has it got? Please elaborate in detail.", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_generate(model: str):
|
||||
# Create engine
|
||||
engine = MLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 10
|
||||
max_tokens = 256
|
||||
generation_cfg = GenerationConfig(max_tokens=max_tokens, n=7)
|
||||
|
||||
output_texts: List[List[str]] = [ # noqa: UP006
|
||||
["" for _ in range(generation_cfg.n)] for _ in range(num_requests)
|
||||
]
|
||||
for rid in range(num_requests):
|
||||
print(f"generating for request {rid}")
|
||||
for delta_outputs in engine._generate(prompts[rid], generation_cfg, request_id=str(rid)):
|
||||
assert len(delta_outputs) == generation_cfg.n
|
||||
for i, delta_output in enumerate(delta_outputs):
|
||||
output_texts[rid][i] += delta_output.delta_text
|
||||
|
||||
# Print output.
|
||||
print("All finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
engine.terminate()
|
||||
del engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_chat_completion(model: str):
|
||||
# Create engine
|
||||
engine = MLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 64
|
||||
n = 2
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
for rid in range(num_requests):
|
||||
print(f"chat completion for request {rid}")
|
||||
for response in engine.chat.completions.create(
|
||||
messages=[{"role": "user", "content": prompts[rid]}],
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=str(rid),
|
||||
stream=True,
|
||||
):
|
||||
for choice in response.choices:
|
||||
assert choice.delta.role == "assistant"
|
||||
assert isinstance(choice.delta.content, str)
|
||||
output_texts[rid][choice.index] += choice.delta.content
|
||||
|
||||
# Print output.
|
||||
print("Chat completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
engine.terminate()
|
||||
del engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_chat_completion_non_stream(model: str):
|
||||
# Create engine
|
||||
engine = MLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 64
|
||||
n = 2
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
for rid in range(num_requests):
|
||||
print(f"chat completion for request {rid}")
|
||||
response = engine.chat.completions.create(
|
||||
messages=[{"role": "user", "content": prompts[rid]}],
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=str(rid),
|
||||
)
|
||||
for choice in response.choices:
|
||||
assert choice.message.role == "assistant"
|
||||
assert isinstance(choice.message.content, str)
|
||||
output_texts[rid][choice.index] += choice.message.content
|
||||
|
||||
# Print output.
|
||||
print("Chat completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
engine.terminate()
|
||||
del engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_completion(model: str):
|
||||
# Create engine
|
||||
engine = MLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 128
|
||||
n = 1
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
for rid in range(num_requests):
|
||||
print(f"completion for request {rid}")
|
||||
for response in engine.completions.create(
|
||||
prompt=prompts[rid],
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=str(rid),
|
||||
stream=True,
|
||||
extra_body={"debug_config": {"ignore_eos": True}},
|
||||
):
|
||||
for choice in response.choices:
|
||||
output_texts[rid][choice.index] += choice.text
|
||||
|
||||
# Print output.
|
||||
print("Completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
engine.terminate()
|
||||
del engine
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_completion_non_stream(model: str):
|
||||
# Create engine
|
||||
engine = MLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 2
|
||||
max_tokens = 128
|
||||
n = 1
|
||||
output_texts: List[List[str]] = [["" for _ in range(n)] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
for rid in range(num_requests):
|
||||
print(f"completion for request {rid}")
|
||||
response = engine.completions.create(
|
||||
prompt=prompts[rid],
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
request_id=str(rid),
|
||||
extra_body={"debug_config": {"ignore_eos": True}},
|
||||
)
|
||||
for choice in response.choices:
|
||||
output_texts[rid][choice.index] += choice.text
|
||||
|
||||
# Print output.
|
||||
print("Completion all finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
engine.terminate()
|
||||
del engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_engine_generate()
|
||||
test_chat_completion()
|
||||
test_chat_completion_non_stream()
|
||||
test_completion()
|
||||
test_completion_non_stream()
|
||||
@@ -0,0 +1,356 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
from typing import Dict, List, Literal # noqa: UP035
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mlc_llm.protocol.debug_protocol import DebugConfig
|
||||
from mlc_llm.protocol.openai_api_protocol import ChatCompletionResponse
|
||||
from mlc_llm.serve import AsyncMLCEngine, MLCEngine
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
LLAMA_2_MODEL = "Llama-2-7b-chat-hf-q4f16_1-MLC"
|
||||
LLAMA_3_MODEL = "Meta-Llama-3-8B-Instruct-q4f16_1-MLC"
|
||||
|
||||
|
||||
@require_test_model(LLAMA_3_MODEL)
|
||||
def test_batch_generation_with_grammar(model: str):
|
||||
# Engine
|
||||
engine = MLCEngine(model=model, mode="server")
|
||||
|
||||
# Inputs
|
||||
system_prompt = "You are a helpful assistant. Always respond only with json."
|
||||
prompts_list = [
|
||||
"Generate a JSON string containing 20 objects:",
|
||||
"Generate a JSON containing a non-empty list:",
|
||||
"Generate a JSON with 5 elements:",
|
||||
"Generate a JSON with a number list, counting from 1 to 20:",
|
||||
]
|
||||
|
||||
repeat = 3
|
||||
top_p = 0.9
|
||||
temperature = 0.6
|
||||
max_tokens = 4096
|
||||
|
||||
# non-json output
|
||||
responses_text: List[ChatCompletionResponse] = [] # noqa: UP006
|
||||
for _ in range(repeat):
|
||||
for p in prompts_list:
|
||||
print(f"Start generation task for request {len(responses_text)}")
|
||||
responses_text.append(
|
||||
engine.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": p},
|
||||
],
|
||||
response_format={"type": "text"},
|
||||
top_p=top_p,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
seed=random.randint(0, 1 << 30),
|
||||
extra_body={"debug_config": DebugConfig(grammar_execution_mode="constraint")},
|
||||
)
|
||||
)
|
||||
|
||||
print("Text output")
|
||||
for req_id, response in enumerate(responses_text):
|
||||
prompt = prompts_list[req_id % len(prompts_list)]
|
||||
output = response.choices[0].message.content
|
||||
print(f"Prompt {req_id}: {prompt}")
|
||||
print(f"Output {req_id}: {output}\n")
|
||||
|
||||
# json output
|
||||
responses_json: List[ChatCompletionResponse] = [] # noqa: UP006
|
||||
for _ in range(repeat):
|
||||
for p in prompts_list:
|
||||
print(f"Start generation task for request {len(responses_json)}")
|
||||
responses_json.append(
|
||||
engine.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": p},
|
||||
],
|
||||
response_format={"type": "json_object"},
|
||||
top_p=top_p,
|
||||
temperature=temperature,
|
||||
seed=random.randint(0, 1 << 30),
|
||||
)
|
||||
)
|
||||
|
||||
print("JSON output")
|
||||
for req_id, response in enumerate(responses_json):
|
||||
prompt = prompts_list[req_id % len(prompts_list)]
|
||||
output = str(response.choices[0].message.content)
|
||||
print(f"Prompt {req_id}: {prompt}")
|
||||
print(f"Output {req_id}: {output}\n")
|
||||
json.loads(output)
|
||||
|
||||
print("Engine metrics:", engine.metrics())
|
||||
|
||||
engine.terminate()
|
||||
|
||||
|
||||
@require_test_model(LLAMA_3_MODEL)
|
||||
def test_batch_generation_with_schema(model: str):
|
||||
# Create engine
|
||||
engine = MLCEngine(model=model, mode="server")
|
||||
|
||||
class Product(BaseModel):
|
||||
product_id: int
|
||||
is_available: bool
|
||||
price: float
|
||||
is_featured: Literal[True]
|
||||
category: Literal["Electronics", "Clothing", "Food"]
|
||||
tags: List[str] # noqa: UP006
|
||||
stock: Dict[str, int] # noqa: UP006
|
||||
|
||||
schema_str = json.dumps(Product.model_json_schema())
|
||||
|
||||
system_prompt = (
|
||||
"You are a helpful assistant. Always respond only with JSON based on the "
|
||||
f"following JSON schema: {schema_str}."
|
||||
)
|
||||
prompt = "Generate a JSON that describes the product according to the given JSON schema."
|
||||
|
||||
repeat = 8
|
||||
top_p = 0.9
|
||||
temperature = 0.6
|
||||
max_tokens = 4096
|
||||
|
||||
# non-json output
|
||||
responses_text: List[ChatCompletionResponse] = [] # noqa: UP006
|
||||
for i in range(repeat):
|
||||
print(f"Start generation task for request {i}")
|
||||
responses_text.append(
|
||||
engine.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
response_format={"type": "text"},
|
||||
top_p=top_p,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
seed=random.randint(0, 1 << 30),
|
||||
extra_body={"debug_config": DebugConfig(grammar_execution_mode="constraint")},
|
||||
)
|
||||
)
|
||||
|
||||
print("Text output")
|
||||
for req_id, response in enumerate(responses_text):
|
||||
output = response.choices[0].message.content
|
||||
print(f"Prompt {req_id}: {prompt}")
|
||||
print(f"Output {req_id}: {output}\n")
|
||||
|
||||
# json output without schema
|
||||
responses_json: List[ChatCompletionResponse] = [] # noqa: UP006
|
||||
for i in range(repeat):
|
||||
print(f"Start generation task for request {i}")
|
||||
responses_json.append(
|
||||
engine.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
response_format={"type": "json_object"},
|
||||
top_p=top_p,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
seed=random.randint(0, 1 << 30),
|
||||
extra_body={"debug_config": DebugConfig(grammar_execution_mode="constraint")},
|
||||
)
|
||||
)
|
||||
|
||||
print("JSON output")
|
||||
for req_id, response in enumerate(responses_json):
|
||||
output = response.choices[0].message.content
|
||||
print(f"Prompt {req_id}: {prompt}")
|
||||
print(f"Output {req_id}: {output}\n")
|
||||
|
||||
# json output with schema
|
||||
responses_schema: List[ChatCompletionResponse] = [] # noqa: UP006
|
||||
for i in range(repeat):
|
||||
print(f"Start generation task for request {i}")
|
||||
responses_schema.append(
|
||||
engine.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
response_format={"type": "json_object", "schema": schema_str},
|
||||
top_p=top_p,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
seed=random.randint(0, 1 << 30),
|
||||
extra_body={"debug_config": DebugConfig(grammar_execution_mode="constraint")},
|
||||
)
|
||||
)
|
||||
|
||||
print("JSON Schema output")
|
||||
for req_id, response in enumerate(responses_schema):
|
||||
output = response.choices[0].message.content
|
||||
print(f"Prompt {req_id}: {prompt}")
|
||||
print(f"Output {req_id}: {output}\n")
|
||||
|
||||
print("Engine metrics:", engine.metrics())
|
||||
|
||||
engine.terminate()
|
||||
|
||||
|
||||
@require_test_model(LLAMA_3_MODEL)
|
||||
def test_batch_generation_jump_forward(model: str, jump_forward: bool = True, repeat: int = 1):
|
||||
# Create engine
|
||||
engine = MLCEngine(model=model, mode="server")
|
||||
|
||||
class Product(BaseModel):
|
||||
product_id: int
|
||||
is_available: bool
|
||||
price: float
|
||||
is_featured: Literal[True]
|
||||
category: Literal["Electronics", "Clothing", "Food"]
|
||||
tags: List[str] # noqa: UP006
|
||||
stock: Dict[str, int] # noqa: UP006
|
||||
|
||||
schema_str = json.dumps(Product.model_json_schema())
|
||||
|
||||
system_prompt = (
|
||||
"You are a helpful assistant. Always respond only with JSON based on the "
|
||||
f"following JSON schema: {schema_str}."
|
||||
)
|
||||
prompt = "Generate a JSON that describes the product according to the given JSON schema."
|
||||
|
||||
top_p = 0.9
|
||||
temperature = 0.6
|
||||
max_tokens = 4096
|
||||
grammar_execution_mode = "jump_forward" if jump_forward else "constraint"
|
||||
|
||||
# json output with schema
|
||||
responses: List[ChatCompletionResponse] = [] # noqa: UP006
|
||||
for i in range(repeat):
|
||||
print(f"Start generation task for request {i}")
|
||||
responses.append(
|
||||
engine.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
response_format={"type": "json_object", "schema": schema_str},
|
||||
top_p=top_p,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
seed=random.randint(0, 1 << 30),
|
||||
extra_body={
|
||||
"debug_config": DebugConfig(grammar_execution_mode=grammar_execution_mode)
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
print(f"Jump forward: {jump_forward}, Repeat: {repeat}")
|
||||
for req_id, response in enumerate(responses):
|
||||
output = response.choices[0].message.content
|
||||
print(f"Prompt {req_id}: {prompt}")
|
||||
print(f"Output {req_id}: {output}\n")
|
||||
|
||||
print("Engine metrics:", engine.metrics())
|
||||
|
||||
engine.terminate()
|
||||
|
||||
|
||||
@require_test_model(LLAMA_3_MODEL)
|
||||
async def run_async_engine(
|
||||
model: str,
|
||||
mode: Literal["text", "json", "schema"] = "schema",
|
||||
jump_forward: bool = True,
|
||||
num_requests: int = 8,
|
||||
):
|
||||
# Create engine
|
||||
async_engine = AsyncMLCEngine(model=model, mode="server")
|
||||
|
||||
class Product(BaseModel):
|
||||
product_id: int
|
||||
is_available: bool
|
||||
price: float
|
||||
is_featured: Literal[True]
|
||||
category: Literal["Electronics", "Clothing", "Food"]
|
||||
tags: List[str] # noqa: UP006
|
||||
stock: Dict[str, int] # noqa: UP006
|
||||
|
||||
schema_str = json.dumps(Product.model_json_schema())
|
||||
|
||||
if mode == "text":
|
||||
response_format = {"type": "text"}
|
||||
elif mode == "json":
|
||||
response_format = {"type": "json_object"}
|
||||
elif mode == "schema":
|
||||
response_format = {"type": "json_object", "schema": schema_str}
|
||||
|
||||
system_prompt = (
|
||||
"You are a helpful assistant. Always respond only with JSON based on the "
|
||||
f"following JSON schema: {schema_str}."
|
||||
)
|
||||
prompt = "Generate a JSON that describes the product according to the given JSON schema."
|
||||
|
||||
top_p = 0.9
|
||||
temperature = 0.6
|
||||
max_tokens = 4096
|
||||
grammar_execution_mode = "jump_forward" if jump_forward else "constraint"
|
||||
|
||||
responses = ["" for _ in range(num_requests)]
|
||||
|
||||
async def generate_task(prompt: str, request_id: str):
|
||||
print(f"Start generation task for request {request_id}")
|
||||
rid = int(request_id)
|
||||
async for response in await async_engine.chat.completions.create( # noqa: F821
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
response_format=response_format,
|
||||
top_p=top_p,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
seed=random.randint(0, 1 << 30),
|
||||
stream=True,
|
||||
extra_body={"debug_config": DebugConfig(grammar_execution_mode=grammar_execution_mode)},
|
||||
):
|
||||
assert len(response.choices) == 1
|
||||
choice = response.choices[0]
|
||||
assert choice.delta.role == "assistant"
|
||||
assert isinstance(choice.delta.content, str)
|
||||
responses[rid] += choice.delta.content
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(generate_task(prompt, request_id=str(i))) for i in range(num_requests)
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
print(f"Mode: {mode}, Jump forward: {jump_forward}, Num requests: {num_requests}")
|
||||
for req_id, output in enumerate(responses):
|
||||
print(f"Prompt {req_id}: {prompt}")
|
||||
print(f"Output {req_id}: {output}\n")
|
||||
|
||||
print("Engine metrics:", await async_engine.metrics())
|
||||
|
||||
async_engine.terminate()
|
||||
del async_engine
|
||||
|
||||
|
||||
def test_async_engine(
|
||||
mode: Literal["text", "json", "schema"] = "schema",
|
||||
jump_forward: bool = True,
|
||||
num_requests: int = 8,
|
||||
):
|
||||
asyncio.run(run_async_engine(mode, jump_forward, num_requests))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_batch_generation_with_grammar()
|
||||
test_batch_generation_with_schema()
|
||||
test_batch_generation_jump_forward(False)
|
||||
test_batch_generation_jump_forward(True)
|
||||
test_async_engine("schema", False, 1)
|
||||
test_async_engine("schema", True, 1)
|
||||
test_async_engine("schema", False, 8)
|
||||
test_async_engine("schema", True, 8)
|
||||
@@ -0,0 +1,56 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve import data
|
||||
from mlc_llm.serve.sync_engine import EngineConfig, SyncMLCEngine
|
||||
|
||||
|
||||
def get_test_image(config) -> data.ImageData:
|
||||
return data.ImageData.from_url("https://llava-vl.github.io/static/images/view.jpg", config)
|
||||
|
||||
|
||||
def test_engine_generate():
|
||||
# Create engine
|
||||
model = "dist/llava-1.5-7b-hf-q4f16_1-MLC/params"
|
||||
model_lib = "dist/llava-1.5-7b-hf-q4f16_1-MLC/llava-1.5-7b-hf-q4f16_1-MLC.so"
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
model_lib=model_lib,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
max_tokens = 256
|
||||
|
||||
with open(Path(model) / "mlc-chat-config.json", encoding="utf-8") as file:
|
||||
model_config = json.load(file)
|
||||
|
||||
prompts = [
|
||||
[
|
||||
data.TextData("USER: "),
|
||||
get_test_image(model_config),
|
||||
data.TextData("\nWhat does this image represent? ASSISTANT:"),
|
||||
],
|
||||
[
|
||||
data.TextData("USER: "),
|
||||
get_test_image(model_config),
|
||||
data.TextData("\nIs there a dog in this image? ASSISTANT:"),
|
||||
],
|
||||
[data.TextData("USER: What is the meaning of life? ASSISTANT:")],
|
||||
]
|
||||
|
||||
output_texts, _ = engine.generate(
|
||||
prompts, GenerationConfig(max_tokens=max_tokens, stop_token_ids=[2])
|
||||
)
|
||||
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_engine_generate()
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Mock testing engine I/O conventions
|
||||
|
||||
Mock test only can help checking the overall input
|
||||
output processing options are passed correctly
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tvm
|
||||
|
||||
from mlc_llm.serve import MLCEngine
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
# test category "unittest"
|
||||
pytestmark = [pytest.mark.unittest]
|
||||
|
||||
|
||||
# NOTE: we only need tokenizers in folder
|
||||
# launch time of mock test is fast so we can put it in unittest
|
||||
@require_test_model("Llama-3-8B-Instruct-q4f16_1-MLC")
|
||||
def test_completion_api(model: str):
|
||||
engine = MLCEngine(model, tvm.cpu(), model_lib="mock://echo")
|
||||
param_dict = {
|
||||
"top_p": 0.6,
|
||||
"temperature": 0.9,
|
||||
"frequency_penalty": 0.1,
|
||||
"presence_penalty": 0.1,
|
||||
"n": 2,
|
||||
}
|
||||
response = engine.chat.completions.create(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
**param_dict,
|
||||
)
|
||||
# echo mock will echo back the generation config
|
||||
for k, v in param_dict.items():
|
||||
assert response.usage.extra[k] == v
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_completion_api()
|
||||
@@ -0,0 +1,141 @@
|
||||
from mlc_llm.protocol.debug_protocol import DebugConfig
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve.sync_engine import EngineConfig, SyncMLCEngine
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
prompts = [
|
||||
"The meaning of life is",
|
||||
"According to the history of Pittsburgh,",
|
||||
"I have a three-day Seattle travel plan. On the first day,",
|
||||
"Undoubtedly, Alaska is one of the most beautiful places on Earth,",
|
||||
"Explain difference between Lambda calculus and Turing machine is",
|
||||
"To assemble a desktop computer, we need the necessary components of",
|
||||
"Vitamin D is important to human beings, because",
|
||||
"Refer to history, the milk tea is originated from",
|
||||
"In the southernmost place in United States,",
|
||||
"AlphaGo has the capabilities of",
|
||||
]
|
||||
|
||||
|
||||
def test_engine_system_prompt(engine):
|
||||
system_prompt = "This is a system prompt"
|
||||
system_prompt_tokens = len(engine.tokenizer.encode(system_prompt))
|
||||
max_tokens = 8
|
||||
_, _ = engine.generate(
|
||||
system_prompt,
|
||||
GenerationConfig(
|
||||
temperature=0,
|
||||
max_tokens=max_tokens,
|
||||
debug_config=DebugConfig(pinned_system_prompt=True),
|
||||
),
|
||||
)
|
||||
metrics = engine.metrics()
|
||||
assert metrics["prefill_tokens_sum"] == system_prompt_tokens
|
||||
sum_prefill_tokens = system_prompt_tokens
|
||||
|
||||
input_token_lens = [len(engine.tokenizer.encode(prompt)) for prompt in prompts]
|
||||
|
||||
generation_config = GenerationConfig(temperature=0, max_tokens=max_tokens)
|
||||
_, _ = engine.generate(prompts, generation_config)
|
||||
metrics = engine.metrics()
|
||||
assert metrics["prefill_tokens_sum"] == sum_prefill_tokens + sum(input_token_lens)
|
||||
sum_prefill_tokens = metrics["prefill_tokens_sum"]
|
||||
|
||||
_, _ = engine.generate(system_prompt + " and why ?", generation_config)
|
||||
metrics = engine.metrics()
|
||||
# system prompt is reused entirely
|
||||
assert metrics["prefill_tokens_sum"] == sum_prefill_tokens + 3
|
||||
sum_prefill_tokens = metrics["prefill_tokens_sum"]
|
||||
|
||||
_, _ = engine.generate(prompts[:4], generation_config)
|
||||
metrics = engine.metrics()
|
||||
# first 4 prompts are removed and need to prefill again
|
||||
assert metrics["prefill_tokens_sum"] == sum_prefill_tokens + sum(input_token_lens[:4])
|
||||
|
||||
|
||||
def test_engine_multi_round(engine):
|
||||
num_requests = 10
|
||||
max_tokens = 8
|
||||
generation_config = GenerationConfig(temperature=0, max_tokens=max_tokens)
|
||||
input_token_lens = [len(engine.tokenizer.encode(prompt)) for prompt in prompts[:num_requests]]
|
||||
|
||||
output_texts, _ = engine.generate(prompts[:num_requests], generation_config)
|
||||
metrics = engine.metrics()
|
||||
assert metrics["prefill_tokens_sum"] == sum(input_token_lens)
|
||||
sum_prefill_tokens = metrics["prefill_tokens_sum"]
|
||||
concat_prompt = []
|
||||
for i, output in enumerate(output_texts):
|
||||
concat_prompt.append(prompts[i] + " " + output[0] + " ?")
|
||||
output_texts, _ = engine.generate(concat_prompt[:num_requests], generation_config)
|
||||
metrics = engine.metrics()
|
||||
assert metrics["prefill_tokens_sum"] == sum_prefill_tokens + 2 * num_requests
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_basic_engine_system_prompt(model: str):
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="local",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
prefix_cache_max_num_recycling_seqs=5,
|
||||
),
|
||||
)
|
||||
test_engine_system_prompt(engine)
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_basic_engine_multi_round(model: str):
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
test_engine_multi_round(engine)
|
||||
|
||||
|
||||
@require_test_model(
|
||||
"Llama-2-7b-chat-hf-q0f16-MLC",
|
||||
"Llama-2-7b-chat-hf-q4f16_1-MLC",
|
||||
)
|
||||
def test_engine_spec_multi_round(model: str, small_model: str):
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[small_model],
|
||||
speculative_mode="small_draft",
|
||||
),
|
||||
)
|
||||
|
||||
test_engine_multi_round(engine)
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_eagle_multi_round(model: str):
|
||||
# Create engine
|
||||
small_model = "dist/Eagle-llama2-7b-chat-q0f16-MLC"
|
||||
small_model_lib = "dist/Eagle-llama2-7b-chat-q0f16-MLC/Eagle-llama2-7b-chat-q0f16-MLC-cuda.so"
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[(small_model, small_model_lib)],
|
||||
speculative_mode="eagle",
|
||||
max_num_sequence=80,
|
||||
),
|
||||
)
|
||||
|
||||
test_engine_multi_round(engine)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_basic_engine_system_prompt()
|
||||
test_basic_engine_multi_round()
|
||||
test_engine_spec_multi_round()
|
||||
test_engine_eagle_multi_round()
|
||||
@@ -0,0 +1,60 @@
|
||||
from typing import List # noqa: UP035
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve import EngineConfig, MLCEngine
|
||||
|
||||
prompts = [
|
||||
"What is the meaning of life?",
|
||||
"Introduce the history of Pittsburgh to me. Please elaborate in detail.",
|
||||
"Write a three-day Seattle travel plan. Please elaborate in detail.",
|
||||
"What is Alaska famous of? Please elaborate in detail.",
|
||||
"What is the difference between Lambda calculus and Turing machine? Please elaborate in detail.", # noqa: E501
|
||||
"What are the necessary components to assemble a desktop computer? Please elaborate in detail.",
|
||||
"Why is Vitamin D important to human beings? Please elaborate in detail.",
|
||||
"Where is milk tea originated from? Please elaborate in detail.",
|
||||
"Where is the southernmost place in United States? Please elaborate in detail.",
|
||||
"Do you know AlphaGo? What capabilities does it have, and what achievements has it got? Please elaborate in detail.", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
def test_engine_generate() -> None:
|
||||
engine = MLCEngine(
|
||||
model="dist/rwkv-6-world-1b6-q0f16-MLC",
|
||||
model_lib="dist/rwkv-6-world-1b6-q0f16-MLC/rwkv-6-world-1b6-q0f16-MLC-cuda.so",
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_num_sequence=8,
|
||||
max_history_size=1,
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 10
|
||||
max_tokens = 256
|
||||
generation_cfg = GenerationConfig(max_tokens=max_tokens, n=7)
|
||||
|
||||
output_texts: List[List[str]] = [ # noqa: UP006
|
||||
["" for _ in range(generation_cfg.n)] for _ in range(num_requests)
|
||||
]
|
||||
for rid in range(num_requests):
|
||||
print(f"generating for request {rid}")
|
||||
for delta_outputs in engine._generate(prompts[rid], generation_cfg, request_id=str(rid)):
|
||||
assert len(delta_outputs) == generation_cfg.n
|
||||
for i, delta_output in enumerate(delta_outputs):
|
||||
output_texts[rid][i] += delta_output.delta_text
|
||||
|
||||
# Print output.
|
||||
print("All finished")
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
engine.terminate()
|
||||
del engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_engine_generate()
|
||||
@@ -0,0 +1,660 @@
|
||||
from typing import Callable, List, Optional # noqa: UP035
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve import Request, RequestStreamOutput, data
|
||||
from mlc_llm.serve.sync_engine import EngineConfig, SyncMLCEngine
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
prompts = [
|
||||
"What is the meaning of life?",
|
||||
"Introduce the history of Pittsburgh to me. Please elaborate in detail.",
|
||||
"Write a three-day Seattle travel plan. Please elaborate in detail.",
|
||||
"What is Alaska famous of? Please elaborate in detail.",
|
||||
"What is the difference between Lambda calculus and Turing machine? Please elaborate in detail.", # noqa: E501
|
||||
"What are the necessary components to assemble a desktop computer? Please elaborate in detail.",
|
||||
"Why is Vitamin D important to human beings? Please elaborate in detail.",
|
||||
"Where is milk tea originated from? Please elaborate in detail.",
|
||||
"Where is the southernmost place in United States? Please elaborate in detail.",
|
||||
"Do you know AlphaGo? What capabilities does it have, and what achievements has it got? Please elaborate in detail.", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
def create_requests(
|
||||
num_requests: int,
|
||||
stop_token_id: Optional[int] = None,
|
||||
temperature: float = 0.8,
|
||||
repetition_penalty: float = 1.0,
|
||||
max_tokens_low: int = 256,
|
||||
max_tokens_high: int = 257,
|
||||
) -> List[Request]: # noqa: UP006
|
||||
assert num_requests >= 0 and num_requests <= len(prompts)
|
||||
|
||||
stop_token_ids = [stop_token_id] if stop_token_id is not None else []
|
||||
requests = []
|
||||
for req_id, prompt in zip(range(num_requests), prompts):
|
||||
max_tokens = np.random.randint(max_tokens_low, max_tokens_high)
|
||||
requests.append(
|
||||
Request(
|
||||
request_id=str(req_id),
|
||||
inputs=data.TextData(prompt),
|
||||
generation_config=GenerationConfig(
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens=max_tokens,
|
||||
stop_token_ids=stop_token_ids,
|
||||
),
|
||||
)
|
||||
)
|
||||
return requests
|
||||
|
||||
|
||||
@require_test_model(
|
||||
"Llama-2-7b-chat-hf-q0f16-MLC",
|
||||
"Llama-2-7b-chat-hf-q4f16_1-MLC",
|
||||
)
|
||||
def test_engine_basic(model: str, small_model: str):
|
||||
"""Test engine **without continuous batching**.
|
||||
|
||||
- Add all requests to the engine altogether in the beginning.
|
||||
- All requests have the same max_tokens. This means all requests
|
||||
will end together.
|
||||
- Engine keeps running `step` for estimated number of steps (number of
|
||||
requests + max_tokens - 1). Then check the output of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations).
|
||||
num_requests = len(prompts) # [4, 8, 10]
|
||||
temperature = 0.9 # [0, 0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.0 # [1.0, 1.01]
|
||||
max_tokens: int = 256 # [32, 128, 256]
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
# Define the callback function for request generation results
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[small_model],
|
||||
speculative_mode="small_draft",
|
||||
),
|
||||
request_stream_callback=fcallback,
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max_tokens - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
engine.step()
|
||||
|
||||
for req_id, output in enumerate(outputs):
|
||||
print(f"Prompt {req_id}: {requests[req_id].inputs[0]}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_eagle_basic(model: str):
|
||||
"""Test engine **without continuous batching**.
|
||||
|
||||
- Add all requests to the engine altogether in the beginning.
|
||||
- All requests have the same max_tokens. This means all requests
|
||||
will end together.
|
||||
- Engine keeps running `step` for estimated number of steps (number of
|
||||
requests + max_tokens - 1). Then check the output of each request.
|
||||
- Use Eagle model as speculative model
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations).
|
||||
num_requests = len(prompts) # [4, 8, 10]
|
||||
temperature = 0.9 # [0, 0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.0 # [1.0, 1.01]
|
||||
max_tokens: int = 256 # [32, 128, 256]
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
# Define the callback function for request generation results
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
|
||||
# Create engine
|
||||
small_model = "dist/Eagle-llama2-7b-chat-q0f16-MLC"
|
||||
small_model_lib = "dist/Eagle-llama2-7b-chat-q0f16-MLC/Eagle-llama2-7b-chat-q0f16-MLC-cuda.so"
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[(small_model, small_model_lib)],
|
||||
speculative_mode="eagle",
|
||||
spec_draft_length=2,
|
||||
),
|
||||
request_stream_callback=fcallback,
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max_tokens - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
engine.step()
|
||||
|
||||
for req_id, output in enumerate(outputs):
|
||||
print(f"Prompt {req_id}: {requests[req_id].inputs[0]}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
|
||||
|
||||
@require_test_model(
|
||||
"Llama-2-7b-chat-hf-q0f16-MLC",
|
||||
"Llama-2-7b-chat-hf-q4f16_1-MLC",
|
||||
)
|
||||
def test_engine_continuous_batching_1(model: str, small_model: str):
|
||||
"""Test engine **with continuous batching**.
|
||||
|
||||
- Add all requests to the engine altogether in the beginning.
|
||||
- All requests have a random maximum generation length. So each
|
||||
request keeps generating until reaching the maximum length.
|
||||
- Engine keeps running `step` for estimated number of steps (number of
|
||||
requests + the maximum max_tokens - 1). Then check the output
|
||||
of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations)
|
||||
num_requests = len(prompts) # [4, 8, 10]
|
||||
temperature = 0.9 # [0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.00 # [1.0, 1.01]
|
||||
max_tokens_low = 128
|
||||
max_tokens_high = 384
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006
|
||||
|
||||
# Define the callback class for request generation results
|
||||
class CallbackTimer:
|
||||
timer: int = -1
|
||||
|
||||
def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
if stream_outputs[0].finish_reason is not None:
|
||||
print(f"Request {request_id} finished at step {self.timer}.")
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
finish_time[int(request_id)] = self.timer
|
||||
|
||||
return fcallback
|
||||
|
||||
def step(self) -> None:
|
||||
self.timer += 1
|
||||
|
||||
# Create engine
|
||||
timer = CallbackTimer()
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[small_model],
|
||||
speculative_mode="small_draft",
|
||||
),
|
||||
request_stream_callback=timer.callback_getter(),
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens_low,
|
||||
max_tokens_high=max_tokens_high,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max(request.generation_config.max_tokens for request in requests) - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
timer.step()
|
||||
assert timer.timer == step
|
||||
engine.step()
|
||||
|
||||
for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)):
|
||||
print(f"Prompt {req_id}: {request.inputs[0]}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
# assert fin_time == request.generation_config.max_tokens - 1
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q4f16_1-MLC")
|
||||
def test_engine_eagle_continuous_batching_1(model: str):
|
||||
"""Test engine **with continuous batching**.
|
||||
|
||||
- Add all requests to the engine altogether in the beginning.
|
||||
- All requests have a random maximum generation length. So each
|
||||
request keeps generating until reaching the maximum length.
|
||||
- Engine keeps running `step` for estimated number of steps (number of
|
||||
requests + the maximum max_tokens - 1). Then check the output
|
||||
of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations)
|
||||
num_requests = len(prompts) # [4, 8, 10]
|
||||
temperature = 0.9 # [0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.00 # [1.0, 1.01]
|
||||
max_tokens_low = 128
|
||||
max_tokens_high = 384
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006
|
||||
|
||||
# Define the callback class for request generation results
|
||||
class CallbackTimer:
|
||||
timer: int = -1
|
||||
|
||||
def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
if stream_outputs[0].finish_reason is not None:
|
||||
print(f"Request {request_id} finished at step {self.timer}.")
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
finish_time[int(request_id)] = self.timer
|
||||
|
||||
return fcallback
|
||||
|
||||
def step(self) -> None:
|
||||
self.timer += 1
|
||||
|
||||
# Create engine
|
||||
small_model = "dist/Eagle-llama2-7b-chat-q4f16_1-MLC"
|
||||
small_model_lib = (
|
||||
"dist/Eagle-llama2-7b-chat-q4f16_1-MLC/Eagle-llama2-7b-chat-q4f16_1-MLC-cuda.so"
|
||||
)
|
||||
timer = CallbackTimer()
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[(small_model, small_model_lib)],
|
||||
speculative_mode="eagle",
|
||||
),
|
||||
request_stream_callback=timer.callback_getter(),
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens_low,
|
||||
max_tokens_high=max_tokens_high,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max(request.generation_config.max_tokens for request in requests) - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
timer.step()
|
||||
assert timer.timer == step
|
||||
engine.step()
|
||||
|
||||
for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)):
|
||||
print(f"Prompt {req_id}: {request.inputs[0]}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
# assert fin_time == request.generation_config.max_tokens - 1
|
||||
|
||||
|
||||
def compare_output_text(output_text1, output_text2):
|
||||
if isinstance(output_text1, list) and isinstance(output_text2, list):
|
||||
for item1, item2 in zip(output_text1, output_text2):
|
||||
if not compare_output_text(item1, item2):
|
||||
return False
|
||||
elif output_text1 != output_text2:
|
||||
print(output_text1)
|
||||
print(output_text2)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@require_test_model(
|
||||
"Llama-2-7b-chat-hf-q0f16-MLC",
|
||||
"Llama-2-7b-chat-hf-q4f16_1-MLC",
|
||||
)
|
||||
def test_engine_generate(model: str, small_model: str, compare_precision=False):
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[small_model],
|
||||
speculative_mode="small_draft",
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 10
|
||||
max_tokens = 256
|
||||
|
||||
# Generate output.
|
||||
if compare_precision:
|
||||
print("compare precision")
|
||||
generation_config = GenerationConfig(
|
||||
temperature=0.0, top_p=0, max_tokens=1024, stop_token_ids=[2], n=1
|
||||
)
|
||||
engine_single_model = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
),
|
||||
)
|
||||
output_texts_single_model, _ = engine_single_model.generate(
|
||||
prompts[:num_requests], generation_config
|
||||
)
|
||||
for req_id, outputs in enumerate(output_texts_single_model):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
# TODO: Add pytorch precision
|
||||
else:
|
||||
generation_config = GenerationConfig(max_tokens=max_tokens, n=3)
|
||||
output_texts, _ = engine.generate(prompts[:num_requests], generation_config)
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
if compare_precision:
|
||||
precision_flag = compare_output_text(output_texts, output_texts_single_model)
|
||||
if precision_flag:
|
||||
print("Accuracy verification succeed\n")
|
||||
else:
|
||||
print("Accuracy verification failed\n")
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_eagle_generate(model: str):
|
||||
# Create engine
|
||||
small_model = "dist/Eagle-llama2-7b-chat-q4f16_1-MLC"
|
||||
small_model_lib = (
|
||||
"dist/Eagle-llama2-7b-chat-q4f16_1-MLC/Eagle-llama2-7b-chat-q4f16_1-MLC-cuda.so"
|
||||
)
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[(small_model, small_model_lib)],
|
||||
speculative_mode="eagle",
|
||||
),
|
||||
)
|
||||
|
||||
num_requests = 10
|
||||
max_tokens = 256
|
||||
|
||||
# Generate output.
|
||||
output_texts, _ = engine.generate(
|
||||
prompts[:num_requests], GenerationConfig(max_tokens=max_tokens, n=3)
|
||||
)
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
|
||||
@require_test_model("Llama-2-13b-chat-hf-q4f16_1-MLC")
|
||||
def test_engine_efficiency(model: str):
|
||||
"""Test engine speculative decoding efficiency."""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations).
|
||||
num_requests = 1 # [4, 8, 10]
|
||||
temperature = 0.9 # [0, 0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.0 # [1.0, 1.01]
|
||||
max_tokens: int = 512
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
# Define the callback function for request generation results
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
request_stream_callback=fcallback,
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max_tokens - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
engine.step()
|
||||
|
||||
for eg, name in zip([engine], ["Normal Deconding"]):
|
||||
metrics = eg.metrics()
|
||||
print("engine name:", name)
|
||||
if name == "Speculative Decoding":
|
||||
print("spec decode metrics:", metrics["spec_decode"])
|
||||
print("engine total decode time:", metrics["engine_decode_time_sum"])
|
||||
print()
|
||||
|
||||
|
||||
@require_test_model(
|
||||
"Llama-2-13b-chat-hf-q4f16_1-MLC",
|
||||
"Llama-2-7b-chat-hf-q4f16_1-MLC",
|
||||
)
|
||||
def test_engine_spec_efficiency(model: str, small_model: str):
|
||||
"""Test engine speculative decoding efficiency."""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations).
|
||||
num_requests = 1 # [4, 8, 10]
|
||||
temperature = 0.9 # [0, 0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.0 # [1.0, 1.01]
|
||||
max_tokens: int = 512
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
# Define the callback function for request generation results
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
|
||||
# Create engine
|
||||
spec_engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[small_model],
|
||||
spec_draft_length=6,
|
||||
speculative_mode="small_draft",
|
||||
),
|
||||
request_stream_callback=fcallback,
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
spec_engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max_tokens - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
spec_engine.step()
|
||||
|
||||
for eg, name in zip([spec_engine], ["Speculative Decoding"]):
|
||||
metrics = eg.metrics()
|
||||
print("engine name:", name)
|
||||
if name == "Speculative Decoding":
|
||||
print("total draft tokens:", metrics["sum_num_draft_tokens"])
|
||||
print("total accepted tokens:", metrics["sum_num_accepted_tokens"])
|
||||
print(
|
||||
"Accept rate:",
|
||||
metrics["sum_num_accepted_tokens"] / (1e-10 + metrics["sum_num_draft_tokens"]),
|
||||
)
|
||||
print("engine total decode time:", metrics["engine_decode_time_sum"])
|
||||
print()
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q4f16_1-MLC")
|
||||
def test_engine_eagle_spec_efficiency(model: str):
|
||||
"""Test engine speculative decoding efficiency."""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations).
|
||||
num_requests = 1 # [4, 8, 10]
|
||||
temperature = 0.9 # [0, 0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.0 # [1.0, 1.01]
|
||||
max_tokens: int = 512
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
# Define the callback function for request generation results
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
|
||||
# Create engine
|
||||
small_model = "dist/Eagle-llama2-7b-chat-q0f16-MLC"
|
||||
small_model_lib = "dist/Eagle-llama2-7b-chat-q0f16-MLC/Eagle-llama2-7b-chat-q0f16-MLC-cuda.so"
|
||||
spec_engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(
|
||||
max_total_sequence_length=4096,
|
||||
additional_models=[(small_model, small_model_lib)],
|
||||
spec_draft_length=6,
|
||||
speculative_mode="eagle",
|
||||
),
|
||||
request_stream_callback=fcallback,
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
spec_engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max_tokens - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
spec_engine.step()
|
||||
|
||||
for eg, name in zip([spec_engine], ["Speculative Decoding"]):
|
||||
metrics = eg.metrics()
|
||||
print("engine name:", name)
|
||||
if name == "Speculative Decoding":
|
||||
print("spec decode:", metrics["spec_decode"])
|
||||
print("engine total decode time:", metrics["engine_decode_time_sum"])
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_engine_basic()
|
||||
test_engine_eagle_basic()
|
||||
test_engine_continuous_batching_1()
|
||||
test_engine_eagle_continuous_batching_1()
|
||||
test_engine_generate(compare_precision=True)
|
||||
test_engine_eagle_generate()
|
||||
test_engine_efficiency()
|
||||
test_engine_spec_efficiency()
|
||||
test_engine_eagle_spec_efficiency()
|
||||
@@ -0,0 +1,474 @@
|
||||
from typing import Callable, List, Optional # noqa: UP035
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mlc_llm.protocol.generation_config import GenerationConfig
|
||||
from mlc_llm.serve import Request, RequestStreamOutput, data
|
||||
from mlc_llm.serve.sync_engine import EngineConfig, SyncMLCEngine
|
||||
from mlc_llm.testing import require_test_model
|
||||
|
||||
prompts = [
|
||||
"What is the meaning of life?",
|
||||
"Introduce the history of Pittsburgh to me. Please elaborate in detail.",
|
||||
"Write a three-day Seattle travel plan. Please elaborate in detail.",
|
||||
"What is Alaska famous of? Please elaborate in detail.",
|
||||
"What is the difference between Lambda calculus and Turing machine? Please elaborate in detail.", # noqa: E501
|
||||
"What are the necessary components to assemble a desktop computer? Please elaborate in detail.",
|
||||
"Why is Vitamin D important to human beings? Please elaborate in detail.",
|
||||
"Where is milk tea originated from? Please elaborate in detail.",
|
||||
"Where is the southernmost place in United States? Please elaborate in detail.",
|
||||
"Do you know AlphaGo? What capabilities does it have, and what achievements has it got? Please elaborate in detail.", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
def create_requests(
|
||||
engine: SyncMLCEngine,
|
||||
num_requests: int,
|
||||
stop_token_id: Optional[int] = None,
|
||||
temperature: float = 0.8,
|
||||
repetition_penalty: float = 1.0,
|
||||
max_tokens_low: int = 256,
|
||||
max_tokens_high: int = 257,
|
||||
) -> List[Request]: # noqa: UP006
|
||||
assert num_requests >= 0 and num_requests <= len(prompts)
|
||||
|
||||
stop_token_ids = [stop_token_id] if stop_token_id is not None else []
|
||||
requests = []
|
||||
for req_id, prompt in zip(range(num_requests), prompts):
|
||||
max_tokens = np.random.randint(max_tokens_low, max_tokens_high)
|
||||
requests.append(
|
||||
engine.create_request(
|
||||
request_id=str(req_id),
|
||||
inputs=data.TextData(prompt),
|
||||
generation_config=GenerationConfig(
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens=max_tokens,
|
||||
stop_token_ids=stop_token_ids,
|
||||
),
|
||||
)
|
||||
)
|
||||
return requests
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_basic(model: str):
|
||||
"""Test engine **without continuous batching**.
|
||||
|
||||
- Add all requests to the engine altogether in the beginning.
|
||||
- All requests have the same max_tokens. This means all requests
|
||||
will end together.
|
||||
- Engine keeps running `step` for estimated number of steps (number of
|
||||
requests + max_tokens - 1). Then check the output of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations).
|
||||
num_requests = 10 # [4, 8, 10]
|
||||
temperature = 0.9 # [0, 0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.0 # [1.0, 1.01]
|
||||
max_tokens: int = 256 # [32, 128, 256]
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
|
||||
# Define the callback function for request generation results
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
request_stream_callback=fcallback,
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
engine,
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max_tokens - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
engine.step()
|
||||
|
||||
for req_id, output in enumerate(outputs):
|
||||
print(f"Prompt {req_id}: {requests[req_id].inputs[0]}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_continuous_batching_1(model: str):
|
||||
"""Test engine **with continuous batching**.
|
||||
|
||||
- Add all requests to the engine altogether in the beginning.
|
||||
- All requests have a random maximum generation length. So each
|
||||
request keeps generating until reaching the maximum length.
|
||||
- Engine keeps running `step` for estimated number of steps (number of
|
||||
requests + the maximum max_tokens - 1). Then check the output
|
||||
of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations)
|
||||
num_requests = 10 # [4, 8, 10]
|
||||
temperature = 0.9 # [0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.00 # [1.0, 1.01]
|
||||
max_tokens_low = 128
|
||||
max_tokens_high = 384
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006
|
||||
|
||||
# Define the callback class for request generation results
|
||||
class CallbackTimer:
|
||||
timer: int = -1
|
||||
|
||||
def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
if stream_outputs[0].finish_reason is not None:
|
||||
print(f"Request {request_id} finished at step {self.timer}.")
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
finish_time[int(request_id)] = self.timer
|
||||
|
||||
return fcallback
|
||||
|
||||
def step(self) -> None:
|
||||
self.timer += 1
|
||||
|
||||
# Create engine
|
||||
timer = CallbackTimer()
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
request_stream_callback=timer.callback_getter(),
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
engine,
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens_low,
|
||||
max_tokens_high=max_tokens_high,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max(request.generation_config.max_tokens for request in requests) - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
timer.step()
|
||||
assert timer.timer == step
|
||||
engine.step()
|
||||
|
||||
for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)):
|
||||
print(f"Prompt {req_id}: {request.inputs[0]}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
assert fin_time == request.generation_config.max_tokens - 1, (
|
||||
f"finish time = {fin_time}, max tokens = {request.generation_config.max_tokens - 1}"
|
||||
)
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_continuous_batching_2(model: str):
|
||||
"""Test engine **with continuous batching**.
|
||||
|
||||
- Add all requests to the engine altogether in the beginning.
|
||||
- All requests have the stop token. So each request keeps generating
|
||||
until having the stop token or reaching the maximum length.
|
||||
- Engine keeps running `step` for estimated number of steps (number of
|
||||
requests + the maximum max_tokens - 1). Then check the output
|
||||
of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations)
|
||||
num_requests = 10 # [4, 8, 10]
|
||||
temperature = 0.9 # [0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.00 # [1.0, 1.01]
|
||||
stop_token_id = 2
|
||||
max_tokens = 512
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006
|
||||
|
||||
# Define the callback class for request generation results
|
||||
class CallbackTimer:
|
||||
timer: int = -1
|
||||
|
||||
def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
if stream_outputs[0].finish_reason is not None:
|
||||
print(f"Request {request_id} finished at step {self.timer}.")
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
finish_time[int(request_id)] = self.timer
|
||||
|
||||
return fcallback
|
||||
|
||||
def step(self) -> None:
|
||||
self.timer += 1
|
||||
|
||||
# Create engine
|
||||
timer = CallbackTimer()
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
request_stream_callback=timer.callback_getter(),
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
engine,
|
||||
num_requests,
|
||||
stop_token_id=stop_token_id,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine
|
||||
for request in requests:
|
||||
engine.add_request(request)
|
||||
|
||||
num_steps = num_requests + max_tokens - 1
|
||||
# Run steps
|
||||
for step in range(num_steps):
|
||||
timer.step()
|
||||
assert timer.timer == step
|
||||
engine.step()
|
||||
|
||||
for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)):
|
||||
print(f"Prompt {req_id}: {request.inputs[0]}")
|
||||
if fin_time < num_requests + max_tokens - 2:
|
||||
print(f"Request {req_id} ends early on the stop token")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_continuous_batching_3(model: str):
|
||||
"""Test engine **with continuous batching**.
|
||||
|
||||
- Add requests randomly between time [0, 200).
|
||||
- All requests have a random maximum generation length. So each
|
||||
request keeps generating until reaching the maximum length.
|
||||
- Engine keeps running `step` until all requests finish.
|
||||
Then check the output of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations)
|
||||
num_requests = 10 # [4, 8, 10]
|
||||
temperature = 0.9 # [0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.00 # [1.0, 1.01]
|
||||
stop_token_id = 2
|
||||
max_tokens_low = 64
|
||||
max_tokens_high = 192
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006
|
||||
|
||||
# Define the callback class for request generation results
|
||||
class CallbackTimer:
|
||||
timer: int = -1
|
||||
finished_requests: int = 0
|
||||
|
||||
def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
if stream_outputs[0].finish_reason is not None:
|
||||
print(f"Request {request_id} finished at step {self.timer}.")
|
||||
self.finished_requests += 1
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
finish_time[int(request_id)] = self.timer
|
||||
|
||||
return fcallback
|
||||
|
||||
def step(self) -> None:
|
||||
self.timer += 1
|
||||
|
||||
def all_finished(self) -> bool:
|
||||
return self.finished_requests == num_requests
|
||||
|
||||
# Create engine
|
||||
timer = CallbackTimer()
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
request_stream_callback=timer.callback_getter(),
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
engine,
|
||||
num_requests,
|
||||
stop_token_id=stop_token_id,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens_low,
|
||||
max_tokens_high=max_tokens_high,
|
||||
)
|
||||
|
||||
# Assign the time to add requests to engine
|
||||
request_add_time = [np.random.randint(0, 200) for _ in range(num_requests)]
|
||||
|
||||
# Run steps
|
||||
while not timer.all_finished():
|
||||
timer.step()
|
||||
|
||||
# Add requests to engine
|
||||
for req_id, add_time in enumerate(request_add_time):
|
||||
if add_time == timer.timer:
|
||||
print(f"add request {req_id} at step {timer.timer}")
|
||||
engine.add_request(requests[req_id])
|
||||
|
||||
engine.step()
|
||||
|
||||
for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)):
|
||||
print(f"Prompt {req_id}: {request.inputs[0]}")
|
||||
print(f"Finish time: {fin_time}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_generate(model: str):
|
||||
# Create engine
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
engine_config=EngineConfig(max_total_sequence_length=4096),
|
||||
)
|
||||
|
||||
num_requests = 10
|
||||
max_tokens = 256
|
||||
|
||||
# Generate output.
|
||||
output_texts, _ = engine.generate(
|
||||
prompts[:num_requests], GenerationConfig(max_tokens=max_tokens, n=7)
|
||||
)
|
||||
for req_id, outputs in enumerate(output_texts):
|
||||
print(f"Prompt {req_id}: {prompts[req_id]}")
|
||||
if len(outputs) == 1:
|
||||
print(f"Output {req_id}:{outputs[0]}\n")
|
||||
else:
|
||||
for i, output in enumerate(outputs):
|
||||
print(f"Output {req_id}({i}):{output}\n")
|
||||
|
||||
|
||||
@require_test_model("Llama-2-7b-chat-hf-q0f16-MLC")
|
||||
def test_engine_hybrid_prefill(model: str):
|
||||
"""Test engine **with hybrid prefill**.
|
||||
|
||||
- Add each single request step by step.
|
||||
- All requests have the same generation length. But due to hybrid prefill,
|
||||
the earlier request will decode with later request prefill, in single step.
|
||||
So each request lasts the same steps, and stops generation step by step as well.
|
||||
- Engine keeps running `step` for the generation length, to finish the last request.
|
||||
Then check the output of each request.
|
||||
"""
|
||||
|
||||
# Hyperparameters for tests (you can try different combinations)
|
||||
num_requests = 10 # [4, 8, 10]
|
||||
temperature = 0.9 # [0.8, 0.9, 1.0, 1.1]
|
||||
repetition_penalty = 1.00 # [1.0, 1.01]
|
||||
max_tokens = 15
|
||||
np.random.seed(0)
|
||||
|
||||
# Output list
|
||||
outputs: List[List[int]] = [[] for _ in range(num_requests)] # noqa: UP006
|
||||
finish_time: List[Optional[int]] = [None] * num_requests # noqa: UP006
|
||||
|
||||
# Define the callback class for request generation results
|
||||
class CallbackTimer:
|
||||
timer: int = -1
|
||||
|
||||
def callback_getter(self) -> Callable[[List[RequestStreamOutput]], None]: # noqa: UP006
|
||||
def fcallback(delta_outputs: List[RequestStreamOutput]): # noqa: UP006
|
||||
for delta_output in delta_outputs:
|
||||
request_id, stream_outputs = delta_output.unpack()
|
||||
assert len(stream_outputs) == 1
|
||||
if stream_outputs[0].finish_reason is not None:
|
||||
print(f"Request {request_id} finished at step {self.timer}.")
|
||||
outputs[int(request_id)] += stream_outputs[0].delta_token_ids
|
||||
finish_time[int(request_id)] = self.timer
|
||||
|
||||
return fcallback
|
||||
|
||||
def step(self) -> None:
|
||||
self.timer += 1
|
||||
|
||||
# Create engine
|
||||
timer = CallbackTimer()
|
||||
engine = SyncMLCEngine(
|
||||
model=model,
|
||||
mode="server",
|
||||
request_stream_callback=timer.callback_getter(),
|
||||
engine_config=EngineConfig(prefill_mode="hybrid"),
|
||||
)
|
||||
|
||||
# Create requests
|
||||
requests = create_requests(
|
||||
engine,
|
||||
num_requests,
|
||||
temperature=temperature,
|
||||
repetition_penalty=repetition_penalty,
|
||||
max_tokens_low=max_tokens,
|
||||
max_tokens_high=max_tokens + 1,
|
||||
)
|
||||
|
||||
# Add all requests to engine step by step
|
||||
for step, request in enumerate(requests):
|
||||
engine.add_request(request)
|
||||
timer.step()
|
||||
assert timer.timer == step
|
||||
engine.step()
|
||||
|
||||
# Run steps
|
||||
for step in range(max_tokens):
|
||||
timer.step()
|
||||
assert timer.timer == step + num_requests
|
||||
engine.step()
|
||||
|
||||
for req_id, (request, output, fin_time) in enumerate(zip(requests, outputs, finish_time)):
|
||||
print(f"Prompt {req_id}: {request.inputs[0]}")
|
||||
print(f"Output {req_id}:{engine.tokenizer.decode(output)}\n")
|
||||
assert fin_time == req_id + request.generation_config.max_tokens - 1, (
|
||||
f"finish time = {fin_time}, max tokens = {req_id + request.generation_config.max_tokens - 1}" # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_engine_basic()
|
||||
test_engine_continuous_batching_1()
|
||||
test_engine_continuous_batching_2()
|
||||
test_engine_continuous_batching_3()
|
||||
test_engine_generate()
|
||||
test_engine_hybrid_prefill()
|
||||
Reference in New Issue
Block a user