Support multi-round conversations in bench_serving (#6135)
This commit is contained in:
@@ -12,6 +12,7 @@ python3 -m sglang.bench_serving --backend sglang --dataset-name random --num-pro
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import copy
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@@ -27,12 +28,12 @@ import uuid
|
|||||||
import warnings
|
import warnings
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field, replace
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
|
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -80,7 +81,7 @@ def _create_bench_client_session():
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RequestFuncInput:
|
class RequestFuncInput:
|
||||||
prompt: str
|
prompt: Union[str, List[str], List[Dict[str, str]]]
|
||||||
api_url: str
|
api_url: str
|
||||||
prompt_len: int
|
prompt_len: int
|
||||||
output_len: int
|
output_len: int
|
||||||
@@ -339,7 +340,9 @@ async def async_request_openai_chat_completions(
|
|||||||
f'rid={rid} time={request_start_time} message="request start" request_func_input="{str(input_partial)}"'
|
f'rid={rid} time={request_start_time} message="request start" request_func_input="{str(input_partial)}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
if request_func_input.image_data:
|
if isinstance(request_func_input.prompt, list):
|
||||||
|
messages = request_func_input.prompt
|
||||||
|
elif request_func_input.image_data:
|
||||||
# Build multi-image content: a list of image_url entries followed by the text
|
# Build multi-image content: a list of image_url entries followed by the text
|
||||||
content_items = [
|
content_items = [
|
||||||
{
|
{
|
||||||
@@ -1768,9 +1771,10 @@ def sample_generated_shared_prefix_requests(
|
|||||||
) -> List[DatasetRow]:
|
) -> List[DatasetRow]:
|
||||||
"""Generate benchmark requests with shared system prompts using random tokens and caching."""
|
"""Generate benchmark requests with shared system prompts using random tokens and caching."""
|
||||||
send_routing_key = getattr(args, "gsp_send_routing_key", False)
|
send_routing_key = getattr(args, "gsp_send_routing_key", False)
|
||||||
|
num_turns = getattr(args, "gsp_num_turns", 1)
|
||||||
|
|
||||||
cache_path = get_gen_prefix_cache_path(args, tokenizer)
|
cache_path = get_gen_prefix_cache_path(args, tokenizer)
|
||||||
should_cache = (range_ratio == 1) and not send_routing_key
|
should_cache = (range_ratio == 1) and not send_routing_key and num_turns == 1
|
||||||
|
|
||||||
# Try to load from cache first
|
# Try to load from cache first
|
||||||
if cache_path.exists() and should_cache:
|
if cache_path.exists() and should_cache:
|
||||||
@@ -1780,7 +1784,7 @@ def sample_generated_shared_prefix_requests(
|
|||||||
|
|
||||||
print(
|
print(
|
||||||
f"\nGenerating new input data... "
|
f"\nGenerating new input data... "
|
||||||
f"({num_groups=}, {prompts_per_group}, {system_prompt_len=}, {question_len=}, {output_len=}, {range_ratio=})"
|
f"({num_groups=}, {prompts_per_group}, {system_prompt_len=}, {question_len=}, {output_len=}, {range_ratio=}, {num_turns=})"
|
||||||
)
|
)
|
||||||
|
|
||||||
run_random_str = uuid.uuid4().hex[:8]
|
run_random_str = uuid.uuid4().hex[:8]
|
||||||
@@ -1794,26 +1798,31 @@ def sample_generated_shared_prefix_requests(
|
|||||||
question_lens = compute_random_lens(
|
question_lens = compute_random_lens(
|
||||||
full_len=question_len,
|
full_len=question_len,
|
||||||
range_ratio=range_ratio,
|
range_ratio=range_ratio,
|
||||||
num=num_groups * prompts_per_group,
|
num=num_groups * prompts_per_group * num_turns,
|
||||||
)
|
).reshape(num_groups, prompts_per_group, num_turns)
|
||||||
output_lens = compute_random_lens(
|
output_lens = compute_random_lens(
|
||||||
full_len=output_len,
|
full_len=output_len,
|
||||||
range_ratio=range_ratio,
|
range_ratio=range_ratio,
|
||||||
num=num_groups * prompts_per_group,
|
num=num_groups * prompts_per_group,
|
||||||
)
|
).reshape(num_groups, prompts_per_group)
|
||||||
del system_prompt_len, question_len, output_len
|
del system_prompt_len, question_len, output_len
|
||||||
|
|
||||||
# Generate system prompts for each group
|
# Generate system prompts for each group
|
||||||
system_prompts = []
|
system_prompts = [
|
||||||
for i in range(num_groups):
|
gen_prompt(tokenizer, system_prompt_lens[i].item()) for i in range(num_groups)
|
||||||
system_prompt = gen_prompt(tokenizer, system_prompt_lens[i].item())
|
]
|
||||||
system_prompts.append(system_prompt)
|
|
||||||
|
|
||||||
# Generate questions
|
# Generate questions: shape (num_groups, prompts_per_group, num_turns)
|
||||||
questions = []
|
questions = [
|
||||||
for i in range(num_groups * prompts_per_group):
|
[
|
||||||
question = gen_prompt(tokenizer, question_lens[i].item())
|
[
|
||||||
questions.append(question)
|
gen_prompt(tokenizer, question_lens[g, p, t].item())
|
||||||
|
for t in range(num_turns)
|
||||||
|
]
|
||||||
|
for p in range(prompts_per_group)
|
||||||
|
]
|
||||||
|
for g in range(num_groups)
|
||||||
|
]
|
||||||
|
|
||||||
# Combine system prompts with questions
|
# Combine system prompts with questions
|
||||||
input_requests = []
|
input_requests = []
|
||||||
@@ -1830,25 +1839,28 @@ def sample_generated_shared_prefix_requests(
|
|||||||
for prompt_idx in tqdm(
|
for prompt_idx in tqdm(
|
||||||
range(prompts_per_group), desc="Generating questions", leave=False
|
range(prompts_per_group), desc="Generating questions", leave=False
|
||||||
):
|
):
|
||||||
flat_index = group_idx * prompts_per_group + prompt_idx
|
turn_questions = questions[group_idx][prompt_idx]
|
||||||
question = questions[flat_index]
|
turn_prompts = [f"{system_prompt}\n\n{turn_questions[0]}"] + turn_questions[
|
||||||
full_prompt = f"{system_prompt}\n\n{question}"
|
1:
|
||||||
|
]
|
||||||
|
full_prompt = turn_prompts[0] if num_turns == 1 else turn_prompts
|
||||||
prompt_len = (
|
prompt_len = (
|
||||||
1
|
1
|
||||||
if getattr(args, "gsp_fast_prepare", False)
|
if getattr(args, "gsp_fast_prepare", False)
|
||||||
else len(tokenizer.encode(full_prompt))
|
else len(tokenizer.encode(turn_prompts[0]))
|
||||||
)
|
)
|
||||||
|
output_len_val = output_lens[group_idx, prompt_idx].item()
|
||||||
|
|
||||||
input_requests.append(
|
input_requests.append(
|
||||||
DatasetRow(
|
DatasetRow(
|
||||||
prompt=full_prompt,
|
prompt=full_prompt,
|
||||||
prompt_len=prompt_len,
|
prompt_len=prompt_len,
|
||||||
output_len=output_lens[flat_index].item(),
|
output_len=output_len_val,
|
||||||
routing_key=routing_key,
|
routing_key=routing_key,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
total_input_tokens += prompt_len
|
total_input_tokens += prompt_len
|
||||||
total_output_tokens += output_lens[flat_index].item()
|
total_output_tokens += output_len_val
|
||||||
|
|
||||||
# Shuffle questions
|
# Shuffle questions
|
||||||
random.shuffle(input_requests)
|
random.shuffle(input_requests)
|
||||||
@@ -1857,6 +1869,7 @@ def sample_generated_shared_prefix_requests(
|
|||||||
print(f"\nGenerated shared prefix dataset statistics:")
|
print(f"\nGenerated shared prefix dataset statistics:")
|
||||||
print(f"Number of groups: {num_groups}")
|
print(f"Number of groups: {num_groups}")
|
||||||
print(f"Prompts per group: {prompts_per_group}")
|
print(f"Prompts per group: {prompts_per_group}")
|
||||||
|
print(f"Number of turns: {num_turns}")
|
||||||
print(f"Total prompts: {len(input_requests)}")
|
print(f"Total prompts: {len(input_requests)}")
|
||||||
if not getattr(args, "gsp_fast_prepare", False):
|
if not getattr(args, "gsp_fast_prepare", False):
|
||||||
print(f"Total input tokens: {total_input_tokens}")
|
print(f"Total input tokens: {total_input_tokens}")
|
||||||
@@ -1864,8 +1877,9 @@ def sample_generated_shared_prefix_requests(
|
|||||||
print(
|
print(
|
||||||
f"Average system prompt length: {sum(len(tokenizer.encode(sp)) for sp in system_prompts) / len(system_prompts):.1f} tokens"
|
f"Average system prompt length: {sum(len(tokenizer.encode(sp)) for sp in system_prompts) / len(system_prompts):.1f} tokens"
|
||||||
)
|
)
|
||||||
|
all_questions = [q for group in questions for conv in group for q in conv]
|
||||||
print(
|
print(
|
||||||
f"Average question length: {sum(len(tokenizer.encode(q)) for q in questions) / len(questions):.1f} tokens\n"
|
f"Average question length: {sum(len(tokenizer.encode(q)) for q in all_questions) / len(all_questions):.1f} tokens\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save to cache
|
# Save to cache
|
||||||
@@ -1919,7 +1933,7 @@ async def get_request(
|
|||||||
|
|
||||||
|
|
||||||
def calculate_metrics(
|
def calculate_metrics(
|
||||||
input_requests: List[DatasetRow],
|
input_requests: Optional[List[DatasetRow]],
|
||||||
outputs: List[RequestFuncOutput],
|
outputs: List[RequestFuncOutput],
|
||||||
dur_s: float,
|
dur_s: float,
|
||||||
tokenizer: PreTrainedTokenizerBase,
|
tokenizer: PreTrainedTokenizerBase,
|
||||||
@@ -1953,9 +1967,10 @@ def calculate_metrics(
|
|||||||
tokenizer.encode(outputs[i].generated_text, add_special_tokens=False)
|
tokenizer.encode(outputs[i].generated_text, add_special_tokens=False)
|
||||||
)
|
)
|
||||||
retokenized_output_lens.append(retokenized_output_len)
|
retokenized_output_lens.append(retokenized_output_len)
|
||||||
total_input += input_requests[i].prompt_len
|
if input_requests is not None:
|
||||||
total_input_text += input_requests[i].text_prompt_len
|
total_input += input_requests[i].prompt_len
|
||||||
total_input_vision += input_requests[i].vision_prompt_len
|
total_input_text += input_requests[i].text_prompt_len
|
||||||
|
total_input_vision += input_requests[i].vision_prompt_len
|
||||||
if output_len > 1:
|
if output_len > 1:
|
||||||
tpots.append((outputs[i].latency - outputs[i].ttft) / (output_len - 1))
|
tpots.append((outputs[i].latency - outputs[i].ttft) / (output_len - 1))
|
||||||
if use_retokenized_itl:
|
if use_retokenized_itl:
|
||||||
@@ -2092,6 +2107,42 @@ def calculate_metrics(
|
|||||||
return metrics, output_lens
|
return metrics, output_lens
|
||||||
|
|
||||||
|
|
||||||
|
MULTI_TURN_BACKENDS = {"sglang-oai-chat", "vllm-chat", "lmdeploy-chat"}
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_multi_turn_request_func(request_func: Callable, backend: str) -> Callable:
|
||||||
|
assert (
|
||||||
|
backend in MULTI_TURN_BACKENDS
|
||||||
|
), f"Multi-turn only supports chat backends: {MULTI_TURN_BACKENDS}, got {backend}"
|
||||||
|
|
||||||
|
async def f(
|
||||||
|
request_func_input: RequestFuncInput,
|
||||||
|
pbar: Optional[tqdm] = None,
|
||||||
|
) -> List[RequestFuncOutput]:
|
||||||
|
prompts: List[str] = request_func_input.prompt
|
||||||
|
prev_messages: List[Dict[str, str]] = []
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
for round_index in range(len(prompts)):
|
||||||
|
prev_messages.append({"role": "user", "content": prompts[round_index]})
|
||||||
|
|
||||||
|
inner_input = replace(
|
||||||
|
copy.deepcopy(request_func_input), prompt=copy.deepcopy(prev_messages)
|
||||||
|
)
|
||||||
|
output = await request_func(
|
||||||
|
inner_input, pbar=pbar if round_index == len(prompts) - 1 else None
|
||||||
|
)
|
||||||
|
outputs.append(output)
|
||||||
|
|
||||||
|
prev_messages.append(
|
||||||
|
{"role": "assistant", "content": output.generated_text}
|
||||||
|
)
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
async def benchmark(
|
async def benchmark(
|
||||||
backend: str,
|
backend: str,
|
||||||
api_url: str,
|
api_url: str,
|
||||||
@@ -2121,6 +2172,10 @@ async def benchmark(
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown backend: {backend}")
|
raise ValueError(f"Unknown backend: {backend}")
|
||||||
|
|
||||||
|
is_multi_turn = isinstance(input_requests[0].prompt, list)
|
||||||
|
if is_multi_turn:
|
||||||
|
request_func = wrap_multi_turn_request_func(request_func, backend=backend)
|
||||||
|
|
||||||
# Limit concurrency
|
# Limit concurrency
|
||||||
# From https://github.com/vllm-project/vllm/pull/9390
|
# From https://github.com/vllm-project/vllm/pull/9390
|
||||||
semaphore = asyncio.Semaphore(max_concurrency) if max_concurrency else None
|
semaphore = asyncio.Semaphore(max_concurrency) if max_concurrency else None
|
||||||
@@ -2186,6 +2241,8 @@ async def benchmark(
|
|||||||
)
|
)
|
||||||
|
|
||||||
warmup_outputs = await asyncio.gather(*warmup_tasks)
|
warmup_outputs = await asyncio.gather(*warmup_tasks)
|
||||||
|
if is_multi_turn:
|
||||||
|
warmup_outputs = [x for output in warmup_outputs for x in output]
|
||||||
|
|
||||||
# Check if at least one warmup request succeeded
|
# Check if at least one warmup request succeeded
|
||||||
if warmup_requests > 0 and not any(output.success for output in warmup_outputs):
|
if warmup_requests > 0 and not any(output.success for output in warmup_outputs):
|
||||||
@@ -2291,6 +2348,8 @@ async def benchmark(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
outputs: List[RequestFuncOutput] = await asyncio.gather(*tasks)
|
outputs: List[RequestFuncOutput] = await asyncio.gather(*tasks)
|
||||||
|
if is_multi_turn:
|
||||||
|
outputs = [x for output in outputs for x in output]
|
||||||
|
|
||||||
# Stop profiler
|
# Stop profiler
|
||||||
if profile:
|
if profile:
|
||||||
@@ -2334,7 +2393,7 @@ async def benchmark(
|
|||||||
# Compute metrics and print results
|
# Compute metrics and print results
|
||||||
benchmark_duration = time.perf_counter() - benchmark_start_time
|
benchmark_duration = time.perf_counter() - benchmark_start_time
|
||||||
metrics, output_lens = calculate_metrics(
|
metrics, output_lens = calculate_metrics(
|
||||||
input_requests=input_requests,
|
input_requests=None if is_multi_turn else input_requests,
|
||||||
outputs=outputs,
|
outputs=outputs,
|
||||||
dur_s=benchmark_duration,
|
dur_s=benchmark_duration,
|
||||||
tokenizer=tokenizer,
|
tokenizer=tokenizer,
|
||||||
@@ -3124,6 +3183,12 @@ if __name__ == "__main__":
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Send routing key in requests via X-SMG-Routing-Key header. Requests with the same prefix share the same routing key.",
|
help="Send routing key in requests via X-SMG-Routing-Key header. Requests with the same prefix share the same routing key.",
|
||||||
)
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--gsp-num-turns",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Number of turns for multi-turn conversations. If > 1, each prompt becomes a list of questions sharing the same system prefix.",
|
||||||
|
)
|
||||||
mooncake_group = parser.add_argument_group("mooncake dataset arguments")
|
mooncake_group = parser.add_argument_group("mooncake dataset arguments")
|
||||||
mooncake_group.add_argument(
|
mooncake_group.add_argument(
|
||||||
"--mooncake-slowdown-factor",
|
"--mooncake-slowdown-factor",
|
||||||
|
|||||||
@@ -752,6 +752,7 @@ def get_similarities(vec1, vec2):
|
|||||||
|
|
||||||
def get_benchmark_args(
|
def get_benchmark_args(
|
||||||
base_url="",
|
base_url="",
|
||||||
|
backend="sglang",
|
||||||
dataset_name="",
|
dataset_name="",
|
||||||
dataset_path="",
|
dataset_path="",
|
||||||
tokenizer="",
|
tokenizer="",
|
||||||
@@ -769,9 +770,15 @@ def get_benchmark_args(
|
|||||||
lora_name=None,
|
lora_name=None,
|
||||||
lora_request_distribution="uniform",
|
lora_request_distribution="uniform",
|
||||||
lora_zipf_alpha=1.5,
|
lora_zipf_alpha=1.5,
|
||||||
|
gsp_num_groups=4,
|
||||||
|
gsp_prompts_per_group=4,
|
||||||
|
gsp_system_prompt_len=128,
|
||||||
|
gsp_question_len=32,
|
||||||
|
gsp_output_len=32,
|
||||||
|
gsp_num_turns=1,
|
||||||
):
|
):
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
backend="sglang",
|
backend=backend,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
host=None,
|
host=None,
|
||||||
port=None,
|
port=None,
|
||||||
@@ -803,6 +810,12 @@ def get_benchmark_args(
|
|||||||
prompt_suffix="",
|
prompt_suffix="",
|
||||||
device=device,
|
device=device,
|
||||||
pd_separated=pd_separated,
|
pd_separated=pd_separated,
|
||||||
|
gsp_num_groups=gsp_num_groups,
|
||||||
|
gsp_prompts_per_group=gsp_prompts_per_group,
|
||||||
|
gsp_system_prompt_len=gsp_system_prompt_len,
|
||||||
|
gsp_question_len=gsp_question_len,
|
||||||
|
gsp_output_len=gsp_output_len,
|
||||||
|
gsp_num_turns=gsp_num_turns,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from sglang.bench_serving import run_benchmark
|
||||||
|
from sglang.srt.utils import kill_process_tree
|
||||||
|
from sglang.test.ci.ci_register import register_cuda_ci
|
||||||
|
from sglang.test.test_utils import (
|
||||||
|
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||||
|
DEFAULT_URL_FOR_TEST,
|
||||||
|
CustomTestCase,
|
||||||
|
get_benchmark_args,
|
||||||
|
popen_launch_server,
|
||||||
|
)
|
||||||
|
|
||||||
|
register_cuda_ci(est_time=300, suite="nightly-1-gpu", nightly=True)
|
||||||
|
|
||||||
|
MODEL = "Qwen/Qwen3-0.6B"
|
||||||
|
NUM_CONVERSATIONS, NUM_TURNS = 4, 3
|
||||||
|
|
||||||
|
|
||||||
|
class TestBenchServingFunctionality(CustomTestCase):
|
||||||
|
def test_gsp_multi_turn(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
process = popen_launch_server(
|
||||||
|
MODEL,
|
||||||
|
DEFAULT_URL_FOR_TEST,
|
||||||
|
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||||
|
other_args=[
|
||||||
|
"--mem-fraction-static",
|
||||||
|
"0.7",
|
||||||
|
"--log-requests",
|
||||||
|
"--log-requests-level",
|
||||||
|
"3",
|
||||||
|
"--log-requests-format",
|
||||||
|
"json",
|
||||||
|
"--log-requests-target",
|
||||||
|
"stdout",
|
||||||
|
temp_dir,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
args = get_benchmark_args(
|
||||||
|
base_url=DEFAULT_URL_FOR_TEST,
|
||||||
|
backend="sglang-oai-chat",
|
||||||
|
tokenizer=MODEL,
|
||||||
|
dataset_name="generated-shared-prefix",
|
||||||
|
num_prompts=NUM_CONVERSATIONS,
|
||||||
|
request_rate=float("inf"),
|
||||||
|
gsp_num_groups=2,
|
||||||
|
gsp_prompts_per_group=2,
|
||||||
|
gsp_system_prompt_len=64,
|
||||||
|
gsp_question_len=16,
|
||||||
|
gsp_output_len=16,
|
||||||
|
gsp_num_turns=NUM_TURNS,
|
||||||
|
)
|
||||||
|
args.warmup_requests = 0
|
||||||
|
res = run_benchmark(args)
|
||||||
|
self.assertEqual(res["completed"], NUM_CONVERSATIONS * NUM_TURNS)
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
logs = "".join(f.read_text() for f in Path(temp_dir).glob("*.log"))
|
||||||
|
self._verify_multi_turn_logs(logs)
|
||||||
|
finally:
|
||||||
|
kill_process_tree(process.pid)
|
||||||
|
|
||||||
|
def _verify_multi_turn_logs(self, content: str):
|
||||||
|
reqs = []
|
||||||
|
for line in content.splitlines():
|
||||||
|
if not line.startswith("{"):
|
||||||
|
continue
|
||||||
|
obj = json.loads(line)
|
||||||
|
if obj.get("event") != "request.finished":
|
||||||
|
continue
|
||||||
|
text = obj.get("obj", {}).get("text")
|
||||||
|
rid = obj.get("rid", "")
|
||||||
|
if text and not rid.startswith("HEALTH_CHECK"):
|
||||||
|
reqs.append(text)
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(reqs), NUM_CONVERSATIONS * NUM_TURNS)
|
||||||
|
|
||||||
|
# Verify prefix relationships
|
||||||
|
reqs_sorted = sorted(reqs, key=len)
|
||||||
|
prefix_count = 0
|
||||||
|
for i, text in enumerate(reqs_sorted):
|
||||||
|
for j in range(i + 1, len(reqs_sorted)):
|
||||||
|
if reqs_sorted[j].startswith(text):
|
||||||
|
prefix_count += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
expected = NUM_CONVERSATIONS * (NUM_TURNS - 1)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
prefix_count, expected, f"Expected at least {expected} prefix pairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user