#!/usr/bin/env python # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import copy import json import os import socket import subprocess import sys import time import unittest import pytest import requests from paddlenlp.transformers import LlamaTokenizer def is_port_in_use(port): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(0.01) try: s.bind(("localhost", port)) return False except socket.error: return True class UITest(unittest.TestCase): def setUp(self): # start web ui self.flask_port = self.available_free_port() self.port = self.available_free_port([self.flask_port]) self.model_path = "__internal_testing__/micro-random-llama" command = ( "cd ./llm && PYTHONPATH=../:$PYTHONPATH" + " {python} predict/flask_server.py --model_name_or_path {model_path} " + '--port {port} --flask_port {flask_port} --src_length 1024 --dtype "float16"' ).format(flask_port=self.flask_port, port=self.port, model_path=self.model_path, python=sys.executable) current_env = copy.copy(os.environ.copy()) current_env.pop("http_proxy", None) current_env.pop("https_proxy", None) os.environ["http_proxy"] = "" os.environ["https_proxy"] = "" self.ui_process = subprocess.Popen(command, shell=True, stdout=sys.stdout, stderr=sys.stderr, env=current_env) self.tokenizer = LlamaTokenizer.from_pretrained(self.model_path) return super().setUp() def tearDown(self): self.ui_process.terminate() def available_free_port(self, exclude=None): exclude = exclude or [] for port in range(8000, 10000): if port in exclude: continue if is_port_in_use(port): continue return port raise ValueError("can not get available port in [8000, 8200]") def wait_until_server_is_ready(self): while True: if is_port_in_use(self.flask_port) and is_port_in_use(self.port): break print("waiting for server ...") time.sleep(1) def get_gradio_ui_result(self, *args, **kwargs): _, _, file = self.client.predict(*args, **kwargs) with open(file, "r", encoding="utf-8") as f: content = json.load(f) return content[-1]["utterance"] @pytest.mark.timeout(4 * 60) def test_argument(self): self.wait_until_server_is_ready() def get_response(data): res = requests.post(f"http://localhost:{self.flask_port}/v1/chat/completions", json=data, stream=True) result_ = "" for line in res.iter_lines(): if not line: continue decoded_line = line.decode("utf-8").strip() # 如果返回行以 "data:" 开头,则去除该前缀 if decoded_line.startswith("data:"): data_str = decoded_line[len("data:") :].strip() else: data_str = decoded_line if data_str == "[DONE]": break chunk = json.loads(data_str) # 根据 OpenAI 的流式返回,每个 chunk 在 choices[0]["delta"] 中包含回复增量 delta = chunk["choices"][0]["delta"].get("content", "") result_ += delta return result_ # 测试用例1:greedy search 模式(top_p 为1.0) data = { "messages": [{"role": "user", "content": "你好"}], "temperature": 1.0, "max_tokens": 20, "top_p": 1.0, "stream": True, } result_1 = get_response(data) # 测试用例2:采样模式(top_p 为 0.7) data = { "messages": [{"role": "user", "content": "你好"}], "temperature": 1.0, "max_tokens": 20, "top_p": 0.7, "stream": True, } result_2 = get_response(data) # 对生成文本的长度进行简单检测 assert 10 <= len(self.tokenizer.tokenize(result_1)) <= 50 assert 10 <= len(self.tokenizer.tokenize(result_2)) <= 50 # 测试用例3:更长的 max_tokens 参数 data = { "messages": [{"role": "user", "content": "你好"}], "temperature": 1.0, "max_tokens": 100, "top_p": 0.7, "stream": True, } result_3 = get_response(data) assert result_3 != result_2 assert 70 <= len(self.tokenizer.tokenize(result_3)) <= 150