diff --git a/tests/benchmarks/lib/test_endpoint_request_func_benchmarks.py b/tests/benchmarks/lib/test_endpoint_request_func_benchmarks.py index 6f8be71b611..11f201ce023 100644 --- a/tests/benchmarks/lib/test_endpoint_request_func_benchmarks.py +++ b/tests/benchmarks/lib/test_endpoint_request_func_benchmarks.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + """ Test cases for endpoint_request_func.py """ diff --git a/tests/benchmarks/lib/test_utils_benchmarks.py b/tests/benchmarks/lib/test_utils_benchmarks.py index c35aaa6b697..f441e6790f5 100644 --- a/tests/benchmarks/lib/test_utils_benchmarks.py +++ b/tests/benchmarks/lib/test_utils_benchmarks.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import json import os import tempfile diff --git a/tests/benchmarks/test_datasets_benchmarks.py b/tests/benchmarks/test_datasets_benchmarks.py index 75d3451b3b7..cd60e1dde02 100644 --- a/tests/benchmarks/test_datasets_benchmarks.py +++ b/tests/benchmarks/test_datasets_benchmarks.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import io import json from argparse import ArgumentParser, Namespace diff --git a/tests/cache_manager/test_cache_transfer_manager.py b/tests/cache_manager/test_cache_transfer_manager.py index 96f0b2ada26..f8b7799832f 100644 --- a/tests/cache_manager/test_cache_transfer_manager.py +++ b/tests/cache_manager/test_cache_transfer_manager.py @@ -1,15 +1,31 @@ +# Copyright (c) 2025 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. + +import sys import time import unittest from unittest.mock import MagicMock, patch import fastdeploy.cache_manager.cache_transfer_manager as cache_transfer_manager +import fastdeploy.cache_manager.transfer_factory.rdma_cache_transfer as rdma_module from fastdeploy.cache_manager.cache_transfer_manager import CacheTransferManager -# ========================== -# 测试用 Args -# ========================== +# Test Configuration class Args: + """Test configuration class to simulate input arguments for CacheTransferManager.""" + rank = 0 local_data_parallel_id = 0 mp_num = 1 @@ -27,83 +43,92 @@ class Args: create_cache_tensor = False -# ========================== -# 测试类 -# ========================== +# RDMA Test Utilities +def create_rdma_manager(rdma_comm, splitwise_role="prefill"): + """Factory function to create RDMACommManager instance with default test parameters. + + Args: + rdma_comm: Mocked rdma_comm module or None + splitwise_role (str): Splitwise role, default to "prefill" + + Returns: + rdma_module.RDMACommManager: Initialized RDMACommManager instance + """ + return rdma_module.RDMACommManager( + splitwise_role=splitwise_role, + rank=0, + gpu_id=0, + cache_k_ptr_list=[1, 2], + cache_v_ptr_list=[3, 4], + max_block_num=10, + block_bytes=1024, + rdma_port=20000, + ) + + +# CacheTransferManager Test Cases class TestCacheTransferManager(unittest.TestCase): + """Unit test suite for CacheTransferManager class.""" + def setUp(self): - # -------------------------- - # mock logger - # -------------------------- + """Set up test fixtures before each test method. + + Mocks dependencies, initializes test objects, and configures test environment. + """ + # Mock logger cache_transfer_manager.logger = MagicMock() - # -------------------------- - # mock current_platform - # -------------------------- + # Mock current platform detection class DummyPlatform: + """Mock platform class to disable specific hardware checks in tests.""" + @staticmethod def is_iluvatar(): return False @staticmethod def is_xpu(): - # 测试环境下不使用 XPU,返回 False - return False + return False # Disable XPU in test environment @staticmethod def is_cuda(): - # 测试环境下不使用 CUDA,返回 False - return False + return False # Disable CUDA in test environment cache_transfer_manager.current_platform = DummyPlatform() - # -------------------------- - # mock EngineCacheQueue - # -------------------------- - patcher1 = patch("fastdeploy.cache_manager.cache_transfer_manager.EngineCacheQueue", new=MagicMock()) - patcher1.start() - self.addCleanup(patcher1.stop) - - # -------------------------- - # mock IPCSignal - # -------------------------- - patcher2 = patch("fastdeploy.cache_manager.cache_transfer_manager.IPCSignal", new=MagicMock()) - patcher2.start() - self.addCleanup(patcher2.stop) - - # -------------------------- - # mock _init_cpu_cache 和 _init_gpu_cache - # -------------------------- - patcher3 = patch.object(CacheTransferManager, "_init_cpu_cache", lambda self, args: None) - patcher4 = patch.object(CacheTransferManager, "_init_gpu_cache", lambda self, args: None) - patcher3.start() - patcher4.start() - self.addCleanup(patcher3.stop) - self.addCleanup(patcher4.stop) - - # -------------------------- - # 创建 manager - # -------------------------- + # Mock EngineCacheQueue class + self.engine_cache_queue_patcher = patch( + "fastdeploy.cache_manager.cache_transfer_manager.EngineCacheQueue", new=MagicMock() + ) + self.engine_cache_queue_patcher.start() + + # Mock IPCSignal class + self.ipc_signal_patcher = patch("fastdeploy.cache_manager.cache_transfer_manager.IPCSignal", new=MagicMock()) + self.ipc_signal_patcher.start() + + # Mock cache initialization methods to avoid actual resource allocation + self.init_cpu_cache_patcher = patch.object(CacheTransferManager, "_init_cpu_cache", lambda self, args: None) + self.init_gpu_cache_patcher = patch.object(CacheTransferManager, "_init_gpu_cache", lambda self, args: None) + self.init_cpu_cache_patcher.start() + self.init_gpu_cache_patcher.start() + + # Initialize CacheTransferManager with test configuration self.manager = CacheTransferManager(Args()) - # -------------------------- - # mock worker_healthy_live_signal - # -------------------------- + # Mock worker health check signal class DummySignal: + """Mock signal class to simulate worker health status.""" + def __init__(self): - self.value = [0] + self.value = [0] # Default to unhealthy initial state self.manager.worker_healthy_live_signal = DummySignal() - # -------------------------- - # mock swap thread pools - # -------------------------- + # Mock thread pools for swap operations self.manager.swap_to_cpu_thread_pool = MagicMock() self.manager.swap_to_gpu_thread_pool = MagicMock() - # -------------------------- - # mock cache_task_queue - # -------------------------- + # Mock cache task queue with test data self.manager.cache_task_queue = MagicMock() self.manager.cache_task_queue.empty.return_value = False self.manager.cache_task_queue.get_transfer_task.return_value = (([0], 0, 0, MagicMock(value=0), 0), True) @@ -111,53 +136,119 @@ def __init__(self): self.manager.cache_task_queue.barrier2 = MagicMock() self.manager.cache_task_queue.barrier3 = MagicMock() - # -------------------------- - # 避免 sleep 阻塞测试 - # -------------------------- - self.sleep_patch = patch("time.sleep", lambda x: None) - self.sleep_patch.start() - self.addCleanup(self.sleep_patch.stop) + # Mock time.sleep to prevent test blocking + self.sleep_patcher = patch("time.sleep", lambda x: None) + self.sleep_patcher.start() + + def tearDown(self): + """Clean up test fixtures after each test method.""" + self.engine_cache_queue_patcher.stop() + self.ipc_signal_patcher.stop() + self.init_cpu_cache_patcher.stop() + self.init_gpu_cache_patcher.stop() + self.sleep_patcher.stop() - # ========================== - # check_work_status 测试 - # ========================== def test_check_work_status_no_signal(self): + """Test check_work_status when no health signal is set. + + Verifies that the method returns healthy status with empty message + when the health signal value is 0 (initial state). + """ healthy, msg = self.manager.check_work_status() self.assertTrue(healthy) self.assertEqual(msg, "") def test_check_work_status_healthy(self): + """Test check_work_status with valid (recent) health signal. + + Verifies that the method returns healthy status when the health signal + is set to current time (within threshold). + """ self.manager.worker_healthy_live_signal.value[0] = int(time.time()) healthy, msg = self.manager.check_work_status() self.assertTrue(healthy) self.assertEqual(msg, "") def test_check_work_status_unhealthy(self): + """Test check_work_status with expired health signal. + + Verifies that the method returns unhealthy status with appropriate + message when the health signal is older than the threshold. + """ self.manager.worker_healthy_live_signal.value[0] = int(time.time()) - 1000 healthy, msg = self.manager.check_work_status(time_interval_threashold=10) self.assertFalse(healthy) self.assertIn("Not Healthy", msg) - # ========================== - # do_data_transfer 异常处理测试 - # ========================== - def test_do_data_transfer_broken_pipe(self): - # mock get_transfer_task 抛出 BrokenPipeError - self.manager.cache_task_queue.get_transfer_task.side_effect = BrokenPipeError("mock broken pipe") - - # mock check_work_status 返回 False,触发 break - self.manager.check_work_status = MagicMock(return_value=(False, "Not Healthy")) - - # patch do_data_transfer 本身,避免死循环 - with patch.object(self.manager, "do_data_transfer") as mock_transfer: - mock_transfer.side_effect = lambda: None # 直接返回,不执行死循环 - self.manager.do_data_transfer() - - # 验证 check_work_status 已被调用 - self.assertTrue(self.manager.check_work_status.called or True) - # 验证 logger 调用 - self.assertTrue(cache_transfer_manager.logger.error.called or True) - self.assertTrue(cache_transfer_manager.logger.critical.called or True) + +# RDMACommManager Test Cases +class TestRDMACommManager(unittest.TestCase): + """Unit test suite for RDMACommManager class.""" + + def test_init_with_rdma_comm(self): + """Test RDMACommManager initialization with valid rdma_comm module. + + Verifies that the messager is created using RDMACommunicator and + instance attributes are set correctly. + """ + mock_comm = MagicMock() + with patch.dict(sys.modules, {"rdma_comm": mock_comm}): + manager = create_rdma_manager(mock_comm) + + mock_comm.RDMACommunicator.assert_called_once() + self.assertTrue(hasattr(manager, "messager")) + self.assertEqual(manager.splitwise_role, "prefill") + + def test_connect_nominal(self): + """Test connect method with valid prefill role. + + Verifies that connect succeeds (returns True) when called with + prefill role and RDMA connection is successful. + """ + mock_comm = MagicMock() + mock_instance = MagicMock() + mock_instance.is_connected.return_value = False + mock_instance.connect.return_value = 0 # Simulate successful connection + mock_comm.RDMACommunicator.return_value = mock_instance + + with patch.dict(sys.modules, {"rdma_comm": mock_comm}): + manager = create_rdma_manager(mock_comm, splitwise_role="prefill") + + result = manager.connect("127.0.0.1", 5001) + self.assertTrue(result) + mock_instance.connect.assert_called_once_with("127.0.0.1", "5001") + + def test_connect_invalid_role(self): + """Test connect method with invalid role (decode). + + Verifies that an AssertionError is raised when connect is called + with a role other than prefill. + """ + mock_comm = MagicMock() + mock_comm.RDMACommunicator.return_value = MagicMock() + + with patch.dict(sys.modules, {"rdma_comm": mock_comm}): + manager = create_rdma_manager(mock_comm, splitwise_role="decode") + + with self.assertRaises(AssertionError): + manager.connect("1.2.3.4", 1234) + + def test_write_cache(self): + """Test write_cache method parameter passing. + + Verifies that write_cache correctly forwards all parameters to + the underlying messager's write_cache method. + """ + mock_comm = MagicMock() + mock_instance = MagicMock() + mock_comm.RDMACommunicator.return_value = mock_instance + + with patch.dict(sys.modules, {"rdma_comm": mock_comm}): + manager = create_rdma_manager(mock_comm) + + manager.write_cache("1.1.1.1", 9999, [1], [2], 3) + + mock_instance.write_cache.assert_called_once_with("1.1.1.1", "9999", [1], [2], 3) if __name__ == "__main__": diff --git a/tests/ce/deploy/deploy.py b/tests/ce/deploy/deploy.py index be6a4f0bf7d..bfe88ca4002 100644 --- a/tests/ce/deploy/deploy.py +++ b/tests/ce/deploy/deploy.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import ast import json import os diff --git a/tests/ci_use/EB_Lite_with_adapter/zmq_client.py b/tests/ci_use/EB_Lite_with_adapter/zmq_client.py index a10d5095c11..4665787847f 100644 --- a/tests/ci_use/EB_Lite_with_adapter/zmq_client.py +++ b/tests/ci_use/EB_Lite_with_adapter/zmq_client.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import threading import time import uuid diff --git a/tests/ci_use/XPU_45T/run_ep.py b/tests/ci_use/XPU_45T/run_ep.py index f375f081d2b..0c7bba80e2b 100644 --- a/tests/ci_use/XPU_45T/run_ep.py +++ b/tests/ci_use/XPU_45T/run_ep.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import os import psutil diff --git a/tests/ci_use/iluvatar_UT/run_ernie300B_4layer.py b/tests/ci_use/iluvatar_UT/run_ernie300B_4layer.py index 2fb33d74b7c..a6ebd469e6b 100644 --- a/tests/ci_use/iluvatar_UT/run_ernie300B_4layer.py +++ b/tests/ci_use/iluvatar_UT/run_ernie300B_4layer.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import functools import sys import threading diff --git a/tests/ci_use/iluvatar_UT/run_ernie_vl_28B.py b/tests/ci_use/iluvatar_UT/run_ernie_vl_28B.py index 2fd6fee0ad9..c2f70c9778c 100644 --- a/tests/ci_use/iluvatar_UT/run_ernie_vl_28B.py +++ b/tests/ci_use/iluvatar_UT/run_ernie_vl_28B.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import functools import io import sys diff --git a/tests/ci_use/metrics/test_metrics.py b/tests/ci_use/metrics/test_metrics.py index b2ddb685f21..16a3d7d35de 100644 --- a/tests/ci_use/metrics/test_metrics.py +++ b/tests/ci_use/metrics/test_metrics.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import asyncio import os import shutil diff --git a/tests/e2e/utils/serving_utils.py b/tests/e2e/utils/serving_utils.py index ad2538e4962..dcc2315e678 100644 --- a/tests/e2e/utils/serving_utils.py +++ b/tests/e2e/utils/serving_utils.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import os import signal import socket diff --git a/tests/entrypoints/cli/test_collect_env_conmmand.py b/tests/entrypoints/cli/test_collect_env_conmmand.py index f71184ea123..c7c5d460b0d 100644 --- a/tests/entrypoints/cli/test_collect_env_conmmand.py +++ b/tests/entrypoints/cli/test_collect_env_conmmand.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import unittest from argparse import Namespace, _SubParsersAction from unittest.mock import MagicMock, patch diff --git a/tests/entrypoints/cli/test_collect_env_script.py b/tests/entrypoints/cli/test_collect_env_script.py index 68355a3d2b4..7c7e0c666c2 100644 --- a/tests/entrypoints/cli/test_collect_env_script.py +++ b/tests/entrypoints/cli/test_collect_env_script.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import io import os import sys diff --git a/tests/entrypoints/cli/test_main.py b/tests/entrypoints/cli/test_main.py index 787d3d035d2..01dd9804e8a 100644 --- a/tests/entrypoints/cli/test_main.py +++ b/tests/entrypoints/cli/test_main.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import unittest from unittest.mock import MagicMock, patch diff --git a/tests/entrypoints/cli/test_openai.py b/tests/entrypoints/cli/test_openai.py index 81cf79b2cb9..1915718cfd1 100644 --- a/tests/entrypoints/cli/test_openai.py +++ b/tests/entrypoints/cli/test_openai.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import argparse import signal import unittest diff --git a/tests/entrypoints/cli/test_serve.py b/tests/entrypoints/cli/test_serve.py index 9c32351839e..c69bc0ec90c 100644 --- a/tests/entrypoints/cli/test_serve.py +++ b/tests/entrypoints/cli/test_serve.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import argparse import unittest from unittest.mock import MagicMock, patch diff --git a/tests/entrypoints/cli/test_tokenizer_cli.py b/tests/entrypoints/cli/test_tokenizer_cli.py index 0b52d027e9b..64c2ad2e6ed 100644 --- a/tests/entrypoints/cli/test_tokenizer_cli.py +++ b/tests/entrypoints/cli/test_tokenizer_cli.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + """ Test cases for tokenizer CLI """ diff --git a/tests/entrypoints/cli/test_types.py b/tests/entrypoints/cli/test_types.py index 22b0998552f..b46dc54ea82 100644 --- a/tests/entrypoints/cli/test_types.py +++ b/tests/entrypoints/cli/test_types.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 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. + import unittest from unittest.mock import MagicMock diff --git a/tests/entrypoints/openai/test_api_authentication.py b/tests/entrypoints/openai/test_api_authentication.py index 199a45d29ee..b8b8f89aef7 100644 --- a/tests/entrypoints/openai/test_api_authentication.py +++ b/tests/entrypoints/openai/test_api_authentication.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import asyncio import hashlib import secrets diff --git a/tests/entrypoints/openai/test_error_response.py b/tests/entrypoints/openai/test_error_response.py index 1d00495e801..2ec7b102b6a 100644 --- a/tests/entrypoints/openai/test_error_response.py +++ b/tests/entrypoints/openai/test_error_response.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from pydantic import ValidationError diff --git a/tests/entrypoints/openai/test_finish_reason.py b/tests/entrypoints/openai/test_finish_reason.py index 4bdb3feefc8..c55e8eb17f4 100644 --- a/tests/entrypoints/openai/test_finish_reason.py +++ b/tests/entrypoints/openai/test_finish_reason.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import json from typing import Any, Dict, List from unittest import IsolatedAsyncioTestCase diff --git a/tests/entrypoints/openai/test_max_streaming_tokens.py b/tests/entrypoints/openai/test_max_streaming_tokens.py index 1e728aa3b8d..d8b46f10802 100644 --- a/tests/entrypoints/openai/test_max_streaming_tokens.py +++ b/tests/entrypoints/openai/test_max_streaming_tokens.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import json import unittest from unittest import IsolatedAsyncioTestCase diff --git a/tests/entrypoints/openai/test_metrics_routes.py b/tests/entrypoints/openai/test_metrics_routes.py index d76ce6226cb..9837bb8f06a 100644 --- a/tests/entrypoints/openai/test_metrics_routes.py +++ b/tests/entrypoints/openai/test_metrics_routes.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + """ Unit tests for metrics routes on the main API port (no --metrics-port set). Mimics the patching pattern used by other tests under tests/entrypoints/openai. diff --git a/tests/entrypoints/openai/test_run_batch.py b/tests/entrypoints/openai/test_run_batch.py index 4cd82f49165..7955cde5aa9 100644 --- a/tests/entrypoints/openai/test_run_batch.py +++ b/tests/entrypoints/openai/test_run_batch.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import asyncio import json import os diff --git a/tests/entrypoints/openai/test_run_batch_proto.py b/tests/entrypoints/openai/test_run_batch_proto.py index 36311be887c..908e4948481 100644 --- a/tests/entrypoints/openai/test_run_batch_proto.py +++ b/tests/entrypoints/openai/test_run_batch_proto.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from pydantic import ValidationError diff --git a/tests/entrypoints/openai/test_run_batch_subcommand.py b/tests/entrypoints/openai/test_run_batch_subcommand.py index e3f7db81258..7d16d47328a 100644 --- a/tests/entrypoints/openai/test_run_batch_subcommand.py +++ b/tests/entrypoints/openai/test_run_batch_subcommand.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + """ Unit tests for RunBatchSubcommand class. """ diff --git a/tests/entrypoints/openai/test_serving_embedding.py b/tests/entrypoints/openai/test_serving_embedding.py index 05dc822fb98..67f23cd42b3 100644 --- a/tests/entrypoints/openai/test_serving_embedding.py +++ b/tests/entrypoints/openai/test_serving_embedding.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import time import unittest from unittest.mock import AsyncMock, MagicMock diff --git a/tests/entrypoints/openai/test_serving_models.py b/tests/entrypoints/openai/test_serving_models.py index a6b8045081c..1ef2fff7ead 100644 --- a/tests/entrypoints/openai/test_serving_models.py +++ b/tests/entrypoints/openai/test_serving_models.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import asyncio import unittest diff --git a/tests/entrypoints/openai/test_serving_reward.py b/tests/entrypoints/openai/test_serving_reward.py index b7832931dda..69ef06a9ea0 100644 --- a/tests/entrypoints/openai/test_serving_reward.py +++ b/tests/entrypoints/openai/test_serving_reward.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import time import unittest from unittest.mock import AsyncMock, MagicMock diff --git a/tests/entrypoints/openai/test_wrap_streaming_generator.py b/tests/entrypoints/openai/test_wrap_streaming_generator.py index e91fd35bf0a..8e01276ee68 100644 --- a/tests/entrypoints/openai/test_wrap_streaming_generator.py +++ b/tests/entrypoints/openai/test_wrap_streaming_generator.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import time from types import SimpleNamespace from unittest.mock import MagicMock, patch diff --git a/tests/entrypoints/openai/tool_parsers/test_utils.py b/tests/entrypoints/openai/tool_parsers/test_utils.py new file mode 100644 index 00000000000..cc34aba6d74 --- /dev/null +++ b/tests/entrypoints/openai/tool_parsers/test_utils.py @@ -0,0 +1,73 @@ +""" +# Copyright (c) 2025 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. +""" + +import unittest + +from partial_json_parser.core.options import Allow + +from fastdeploy.entrypoints.openai.tool_parsers import utils + + +class TestPartialJsonUtils(unittest.TestCase): + """Unit test suite for partial JSON utility functions.""" + + def test_find_common_prefix(self): + """Test common prefix detection between two strings.""" + string1 = '{"fruit": "ap"}' + string2 = '{"fruit": "apple"}' + self.assertEqual(utils.find_common_prefix(string1, string2), '{"fruit": "ap') + + def test_find_common_suffix(self): + """Test common suffix detection between two strings.""" + string1 = '{"fruit": "ap"}' + string2 = '{"fruit": "apple"}' + self.assertEqual(utils.find_common_suffix(string1, string2), '"}') + + def test_extract_intermediate_diff(self): + """Test extraction of intermediate difference between current and old strings.""" + old_string = '{"fruit": "ap"}' + current_string = '{"fruit": "apple"}' + self.assertEqual(utils.extract_intermediate_diff(current_string, old_string), "ple") + + def test_find_all_indices(self): + """Test finding all occurrence indices of a substring in a string.""" + target_string = "banana" + substring = "an" + self.assertEqual(utils.find_all_indices(target_string, substring), [1, 3]) + + def test_partial_json_loads_complete(self): + """Test partial_json_loads with a complete JSON string.""" + input_json = '{"a": 1, "b": 2}' + parse_flags = Allow.ALL + parsed_obj, parsed_length = utils.partial_json_loads(input_json, parse_flags) + self.assertEqual(parsed_obj, {"a": 1, "b": 2}) + self.assertEqual(parsed_length, len(input_json)) + + def test_is_complete_json(self): + """Test JSON completeness check.""" + self.assertTrue(utils.is_complete_json('{"a": 1}')) + self.assertFalse(utils.is_complete_json('{"a": 1')) + + def test_consume_space(self): + """Test whitespace consumption from the start of a string.""" + input_string = " \t\nabc" + # 3 spaces + 1 tab + 1 newline = 5 whitespace characters + first_non_whitespace_idx = utils.consume_space(0, input_string) + self.assertEqual(first_non_whitespace_idx, 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/entrypoints/test_vllm_run_engine.py b/tests/entrypoints/test_vllm_run_engine.py index 22783b19775..ec3eed31b32 100644 --- a/tests/entrypoints/test_vllm_run_engine.py +++ b/tests/entrypoints/test_vllm_run_engine.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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 unittest.mock import MagicMock import numpy as np diff --git a/tests/input/test_ernie4_5_processor.py b/tests/input/test_ernie4_5_processor.py index 8c7386fef85..d1b5c9988ec 100644 --- a/tests/input/test_ernie4_5_processor.py +++ b/tests/input/test_ernie4_5_processor.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from unittest.mock import MagicMock, patch diff --git a/tests/input/test_ernie_processor.py b/tests/input/test_ernie_processor.py index 2a9dcb23cf8..428d3502a2f 100644 --- a/tests/input/test_ernie_processor.py +++ b/tests/input/test_ernie_processor.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from unittest.mock import MagicMock, patch diff --git a/tests/input/test_ernie_vl_processor.py b/tests/input/test_ernie_vl_processor.py index 92d24d5b96f..d37819ae37f 100644 --- a/tests/input/test_ernie_vl_processor.py +++ b/tests/input/test_ernie_vl_processor.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from unittest.mock import MagicMock, patch diff --git a/tests/input/test_paddleocr_vl_processor.py b/tests/input/test_paddleocr_vl_processor.py index 62b58db265e..85d07e1cf55 100644 --- a/tests/input/test_paddleocr_vl_processor.py +++ b/tests/input/test_paddleocr_vl_processor.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import pickle import unittest from unittest.mock import ANY, MagicMock, patch diff --git a/tests/input/test_process_video.py b/tests/input/test_process_video.py index edabb4a82f4..0a35ee8eb19 100644 --- a/tests/input/test_process_video.py +++ b/tests/input/test_process_video.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import io import math import os diff --git a/tests/input/test_text_processor.py b/tests/input/test_text_processor.py index 794d81895d7..d9b1ffdbacf 100644 --- a/tests/input/test_text_processor.py +++ b/tests/input/test_text_processor.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from unittest.mock import MagicMock, patch diff --git a/tests/input/test_tokenizer_client.py b/tests/input/test_tokenizer_client.py index ef180270f87..7963b0ee845 100644 --- a/tests/input/test_tokenizer_client.py +++ b/tests/input/test_tokenizer_client.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import httpx import pytest import respx diff --git a/tests/layers/test_guided_decoding.py b/tests/layers/test_guided_decoding.py index 964ad1dc02b..d923d34be06 100644 --- a/tests/layers/test_guided_decoding.py +++ b/tests/layers/test_guided_decoding.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + """ 测试GuidedDecoding类的单元测试 """ diff --git a/tests/layers/test_w4a8_moe.py b/tests/layers/test_w4a8_moe.py index dc6dab15427..c7ea97139fd 100644 --- a/tests/layers/test_w4a8_moe.py +++ b/tests/layers/test_w4a8_moe.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import json import os import shutil diff --git a/tests/model_executor/ops/triton_ops/test_triton_utils.py b/tests/model_executor/ops/triton_ops/test_triton_utils.py index aecd2ae5733..b8ab1179b09 100644 --- a/tests/model_executor/ops/triton_ops/test_triton_utils.py +++ b/tests/model_executor/ops/triton_ops/test_triton_utils.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import os import sys import unittest diff --git a/tests/model_executor/ops/triton_ops/test_triton_utils_v2.py b/tests/model_executor/ops/triton_ops/test_triton_utils_v2.py index c9cf2568105..449ed2f2e08 100644 --- a/tests/model_executor/ops/triton_ops/test_triton_utils_v2.py +++ b/tests/model_executor/ops/triton_ops/test_triton_utils_v2.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from unittest.mock import MagicMock, patch diff --git a/tests/model_executor/test_tensor_wise_fp8.py b/tests/model_executor/test_tensor_wise_fp8.py new file mode 100644 index 00000000000..4b7eac1119d --- /dev/null +++ b/tests/model_executor/test_tensor_wise_fp8.py @@ -0,0 +1,111 @@ +""" +# Copyright (c) 2025 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. +""" + +import unittest +from unittest.mock import MagicMock, patch + +import paddle + +from fastdeploy.model_executor.layers.quantization.tensor_wise_fp8 import ( + TensorWiseFP8Config, + TensorWiseFP8LinearMethod, +) + + +# Dummy classes for test +class DummyLayer: + """Dummy linear layer for test purposes""" + + def __init__(self): + self.weight_shape = [4, 8] + self.weight_key = "weight" + self.weight_scale_key = "weight_scale" + self.act_scale_key = "act_scale" + self.weight_dtype = "float32" + self.weight = MagicMock() # Mock weight to avoid dtype copy errors + + def create_parameter(self, shape, dtype, is_bias=False, default_initializer=None): + """Mock parameter creation""" + return MagicMock() + + +class DummyFusedMoE: + """Dummy FusedMoE class for patching""" + + pass + + +class TestTensorWiseFP8Config(unittest.TestCase): + """Test suite for TensorWiseFP8Config""" + + def test_get_quant_method_linear(self): + """Verify linear layer returns TensorWiseFP8LinearMethod""" + cfg = TensorWiseFP8Config() + layer = DummyLayer() + method = cfg.get_quant_method(layer) + self.assertIsInstance(method, TensorWiseFP8LinearMethod) + + def test_get_quant_method_moe(self): + """Verify FusedMoE layer returns valid quant method""" + cfg = TensorWiseFP8Config() + layer = DummyFusedMoE() + with patch("fastdeploy.model_executor.layers.moe.FusedMoE", DummyFusedMoE): + method = cfg.get_quant_method(layer) + self.assertTrue(hasattr(method, "quant_config")) + + +class TestTensorWiseFP8LinearMethod(unittest.TestCase): + """Test suite for TensorWiseFP8LinearMethod""" + + def setUp(self): + """Initialize test fixtures""" + self.layer = DummyLayer() + self.method = TensorWiseFP8LinearMethod(TensorWiseFP8Config()) + # Initialize scales to avoid apply errors + self.method.act_scale = 1.0 + self.method.total_scale = 1.0 + + def test_create_weights(self): + """Verify weight dtype is set to float8_e4m3fn""" + self.method.create_weights(self.layer) + self.assertEqual(self.layer.weight_dtype, "float8_e4m3fn") + + def test_process_prequanted_weights(self): + """Verify prequantized weights and scales are processed correctly""" + self.layer.weight.copy_ = MagicMock() + state_dict = { + "weight": paddle.randn([8, 4]), + "weight_scale": paddle.to_tensor([0.5], dtype="float32"), + "act_scale": paddle.to_tensor([2.0], dtype="float32"), + } + self.method.process_prequanted_weights(self.layer, state_dict) + self.assertAlmostEqual(self.method.act_scale, 2.0) + self.assertAlmostEqual(self.method.total_scale, 1.0) + self.layer.weight.copy_.assert_called_once() + + @patch("fastdeploy.model_executor.ops.gpu.fused_hadamard_quant_fp8", autospec=True) + @patch("fastdeploy.model_executor.ops.gpu.cutlass_fp8_fp8_half_gemm_fused", autospec=True) + def test_apply(self, mock_gemm, mock_quant): + """Verify apply method executes with mocked ops""" + mock_quant.side_effect = lambda x, scale: x + mock_gemm.side_effect = lambda x, w, **kwargs: x + x = paddle.randn([4, 8]) + out = self.method.apply(self.layer, x) + self.assertTrue((out == x).all()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/model_executor/test_w4afp8.py b/tests/model_executor/test_w4afp8.py new file mode 100644 index 00000000000..c3b3918d105 --- /dev/null +++ b/tests/model_executor/test_w4afp8.py @@ -0,0 +1,151 @@ +""" +# Copyright (c) 2025 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. +""" + +import unittest +from unittest.mock import MagicMock, patch + +import paddle + +from fastdeploy.model_executor.layers.quantization.w4afp8 import ( + W4AFP8Config, + W4AFP8LinearMethod, +) + + +class DummyLayer: + def __init__(self): + self.weight_shape = [8, 16] # Mock weight shape + self.weight_dtype = None # To be set by quant method + self._dtype = "float32" # Base dtype + self.prefix = "dummy" # Layer identifier prefix + self.bias = None # No bias by default + self.add_bias = False # Disable bias addition + + # Mock parameter creation for weight/bias + def create_parameter(self, shape, dtype, is_bias, default_initializer): + param = paddle.zeros(shape, dtype=dtype) + return param + + # Mock method to set weight value + def set_value(self, tensor): + self.weight = tensor + + +class DummyFusedMoE: + pass + + +class TestW4AFP8Config(unittest.TestCase): + """Test suite for W4AFP8Config""" + + def test_name_and_from_config(self): + # Test config initialization from dict and name property + config_dict = {"weight_scale_dict": {}, "act_scale_dict": {}, "is_permuted": True, "hadamard_block_size": 128} + cfg = W4AFP8Config.from_config(config_dict) + + # Verify config properties + self.assertEqual(cfg.name(), "w4afp8") + self.assertEqual(cfg.is_permuted, True) + self.assertEqual(cfg.hadamard_block_size, 128) + + def test_get_quant_method_linear(self): + # Test quant method retrieval for linear layer + cfg = W4AFP8Config({}, {}, True, 128) + layer = DummyLayer() + method = cfg.get_quant_method(layer) + + # Verify method type and config binding + self.assertIsInstance(method, W4AFP8LinearMethod) + self.assertEqual(method.quant_config, cfg) + + def test_get_quant_method_moe(self): + # Test quant method retrieval for MoE layer + cfg = W4AFP8Config({}, {}, True, 128) + layer = DummyFusedMoE() + + # Patch FusedMoE to dummy class for test scope + with patch("fastdeploy.model_executor.layers.moe.FusedMoE", DummyFusedMoE): + method = cfg.get_quant_method(layer) + + # Verify method has required config attribute + self.assertTrue(hasattr(method, "quant_config")) + + +class TestW4AFP8LinearMethod(unittest.TestCase): + """Test suite for W4AFP8LinearMethod""" + + def setUp(self): + # Initialize test fixtures: config, method, and dummy layer + self.cfg = W4AFP8Config({}, {}, True, 128) + self.method = W4AFP8LinearMethod(self.cfg) + self.layer = DummyLayer() + + def test_create_weights(self): + # Test weight creation with correct dtype and shape + self.method.create_weights(self.layer) + + # Verify weight properties + self.assertEqual(self.layer.weight_dtype, "int8") # W4A uses int8 storage + self.assertIsInstance(self.layer.weight, paddle.Tensor) + self.assertEqual(list(self.layer.weight.shape), [8, 8]) # Shape adjusted for quantization + + @patch( + "fastdeploy.model_executor.layers.quantization.w4afp8.fastdeploy.model_executor.ops.gpu.scaled_gemm_f8_i4_f16_weight_quantize" + ) + def test_process_loaded_weights(self, mock_quant): + # Mock quantize op output: (quantized_weight, scale) + dummy_weights = paddle.ones([8, 16]) + mock_quant.return_value = (paddle.ones([4, 16]), paddle.ones([4], dtype="float32")) + + # Mock layer weight and scale attributes + self.layer.weight = MagicMock() + self.layer.weight_scale = MagicMock() + + # Execute weight processing + self.method.process_loaded_weights(self.layer, dummy_weights) + + # Verify weight and scale are set + self.layer.weight.set_value.assert_called_once() + self.layer.weight_scale.set_value.assert_called_once() + + @patch( + "fastdeploy.model_executor.layers.quantization.w4afp8.fastdeploy.model_executor.ops.gpu.scaled_gemm_f8_i4_f16" + ) + def test_apply(self, mock_gemm): + # Prepare input and layer attributes + x = paddle.ones([2, 4]) # Mock input tensor + self.layer.weight = paddle.ones([4, 4]) # Quantized weight + self.layer.weight_scale = paddle.ones([4], dtype="float32") # Weight scale + self.layer.add_bias = False # Disable bias + self.layer.prefix = "dummy" + + # Set scale dicts in config + self.cfg.weight_scale_dict = {"dummy.weight_scale": 1.0} + self.cfg.act_scale_dict = {"dummy.activation_scale": 1.0} + + # Mock GEMM op output + mock_gemm.return_value = paddle.ones([2, 4]) + + # Execute forward pass + out = self.method.apply(self.layer, x) + + # Verify output and op call + self.assertIsInstance(out, paddle.Tensor) + mock_gemm.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/output/test_process_batch_output_use_zmq.py b/tests/output/test_process_batch_output_use_zmq.py index 85e3cf5cfc9..724b8b76007 100644 --- a/tests/output/test_process_batch_output_use_zmq.py +++ b/tests/output/test_process_batch_output_use_zmq.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import unittest from unittest.mock import MagicMock, patch diff --git a/tests/platforms/test_platforms.py b/tests/platforms/test_platforms.py index 70481af7f54..c711cd3ece3 100644 --- a/tests/platforms/test_platforms.py +++ b/tests/platforms/test_platforms.py @@ -20,67 +20,213 @@ from fastdeploy.platforms.base import _Backend from fastdeploy.platforms.cpu import CPUPlatform from fastdeploy.platforms.cuda import CUDAPlatform +from fastdeploy.platforms.dcu import DCUPlatform +from fastdeploy.platforms.gcu import GCUPlatform +from fastdeploy.platforms.intel_hpu import INTEL_HPUPlatform +from fastdeploy.platforms.maca import MACAPlatform +from fastdeploy.platforms.npu import NPUPlatform +from fastdeploy.platforms.xpu import XPUPlatform class TestCPUPlatform(unittest.TestCase): + """Test suite for CPUPlatform""" + def setUp(self): self.platform = CPUPlatform() @patch("paddle.device.get_device", return_value="cpu") def test_is_cpu_and_available(self, mock_get_device): - """ - Check hardware type (CPU) and availability - """ + """Verify is_cpu() returns True and platform is available""" self.assertTrue(self.platform.is_cpu()) self.assertTrue(self.platform.available()) def test_attention_backend(self): - """CPUPlatform attention_backend should return empty string""" + """Verify get_attention_backend_cls returns empty string for CPU""" self.assertEqual(self.platform.get_attention_backend_cls(None), "") class TestCUDAPlatform(unittest.TestCase): + """Test suite for CUDAPlatform""" + def setUp(self): self.platform = CUDAPlatform() @patch("paddle.is_compiled_with_cuda", return_value=True) @patch("paddle.device.get_device", return_value="cuda") @patch("paddle.static.cuda_places", return_value=[0]) - def test_is_cuda_and_available(self, mock_get_device, mock_is_cuda, mock_cuda_places): - """ - Check hardware type (CUDA) and availability - """ + def test_is_cuda_and_available(self, mock_cuda_places, mock_is_cuda, mock_get_device): + """Verify is_cuda() returns True and platform is available""" self.assertTrue(self.platform.is_cuda()) self.assertTrue(self.platform.available()) def test_attention_backend_valid(self): - """ - CUDAPlatform should return correct backend class name for valid backends - """ - self.assertIn( - "PaddleNativeAttnBackend", - self.platform.get_attention_backend_cls(_Backend.NATIVE_ATTN), - ) - self.assertIn( - "AppendAttentionBackend", - self.platform.get_attention_backend_cls(_Backend.APPEND_ATTN), - ) - self.assertIn( - "MLAAttentionBackend", - self.platform.get_attention_backend_cls(_Backend.MLA_ATTN), - ) - self.assertIn( - "FlashAttentionBackend", - self.platform.get_attention_backend_cls(_Backend.FLASH_ATTN), - ) + """Verify valid attention backends return correct class names""" + self.assertIn("PaddleNativeAttnBackend", self.platform.get_attention_backend_cls(_Backend.NATIVE_ATTN)) + self.assertIn("AppendAttentionBackend", self.platform.get_attention_backend_cls(_Backend.APPEND_ATTN)) + self.assertIn("MLAAttentionBackend", self.platform.get_attention_backend_cls(_Backend.MLA_ATTN)) + self.assertIn("FlashAttentionBackend", self.platform.get_attention_backend_cls(_Backend.FLASH_ATTN)) def test_attention_backend_invalid(self): - """ - CUDAPlatform should raise ValueError for invalid backend - """ + """Verify invalid backend raises ValueError""" with self.assertRaises(ValueError): self.platform.get_attention_backend_cls("INVALID_BACKEND") +class TestMACAPlatform(unittest.TestCase): + """Test suite for MACAPlatform""" + + @patch("paddle.static.cuda_places", return_value=[0, 1]) + def test_available_true(self, mock_cuda_places): + """Verify available() returns True when GPUs exist""" + self.assertTrue(MACAPlatform.available()) + mock_cuda_places.assert_called_once() + + @patch("paddle.static.cuda_places", side_effect=Exception("No GPU")) + def test_available_false(self, mock_cuda_places): + """Verify available() returns False when no GPUs""" + self.assertFalse(MACAPlatform.available()) + mock_cuda_places.assert_called_once() + + def test_get_attention_backend_native(self): + """Verify NATIVE_ATTN returns correct backend class""" + self.assertIn("PaddleNativeAttnBackend", MACAPlatform.get_attention_backend_cls(_Backend.NATIVE_ATTN)) + + def test_get_attention_backend_append(self): + """Verify APPEND_ATTN returns correct backend class""" + self.assertIn("FlashAttentionBackend", MACAPlatform.get_attention_backend_cls(_Backend.APPEND_ATTN)) + + def test_get_attention_backend_mla(self): + """Verify MLA_ATTN returns correct backend class""" + self.assertIn("MetaxMLAAttentionBackend", MACAPlatform.get_attention_backend_cls(_Backend.MLA_ATTN)) + + def test_get_attention_backend_invalid(self): + """Verify invalid backend raises ValueError""" + with self.assertRaises(ValueError): + MACAPlatform.get_attention_backend_cls("INVALID_BACKEND") + + +class TestINTELHPUPlatform(unittest.TestCase): + """Test suite for INTEL_HPUPlatform""" + + @patch("paddle.base.core.get_custom_device_count", return_value=1) + def test_available_true(self, mock_get_count): + """Verify available() returns True when HPU exists""" + self.assertTrue(INTEL_HPUPlatform.available()) + mock_get_count.assert_called_with("intel_hpu") + + @patch("paddle.base.core.get_custom_device_count", side_effect=Exception("No HPU")) + @patch("fastdeploy.utils.console_logger.warning") + def test_available_false(self, mock_logger_warn, mock_get_count): + """Verify available() returns False and warns when no HPU""" + self.assertFalse(INTEL_HPUPlatform.available()) + mock_logger_warn.assert_called() + self.assertIn("No HPU", mock_logger_warn.call_args[0][0]) + + def test_attention_backend_native(self): + """Verify NATIVE_ATTN returns correct backend class""" + self.assertIn("PaddleNativeAttnBackend", INTEL_HPUPlatform.get_attention_backend_cls(_Backend.NATIVE_ATTN)) + + def test_attention_backend_hpu(self): + """Verify HPU_ATTN returns correct backend class""" + self.assertIn("HPUAttentionBackend", INTEL_HPUPlatform.get_attention_backend_cls(_Backend.HPU_ATTN)) + + @patch("fastdeploy.utils.console_logger.warning") + def test_attention_backend_other(self, mock_logger_warn): + """Verify invalid backend logs warning and returns None""" + self.assertIsNone(INTEL_HPUPlatform.get_attention_backend_cls("INVALID_BACKEND")) + mock_logger_warn.assert_called() + + +class TestNPUPlatform(unittest.TestCase): + """Test suite for NPUPlatform""" + + def setUp(self): + self.platform = NPUPlatform() + + def test_device_name(self): + """Verify device_name is set to 'npu'""" + self.assertEqual(self.platform.device_name, "npu") + + +class TestDCUPlatform(unittest.TestCase): + """Test suite for DCUPlatform""" + + def setUp(self): + self.platform = DCUPlatform() + + @patch("paddle.static.cuda_places", return_value=[0]) + def test_available_with_gpu(self, mock_cuda_places): + """Verify available() returns True when GPU exists""" + self.assertTrue(self.platform.available()) + + @patch("paddle.static.cuda_places", side_effect=Exception("No GPU")) + def test_available_no_gpu(self, mock_cuda_places): + """Verify available() returns False when no GPU""" + self.assertFalse(self.platform.available()) + + def test_attention_backend_native(self): + """Verify NATIVE_ATTN returns correct backend class""" + self.assertIn("PaddleNativeAttnBackend", self.platform.get_attention_backend_cls(_Backend.NATIVE_ATTN)) + + def test_attention_backend_block(self): + """Verify BLOCK_ATTN returns correct backend class""" + self.assertIn("BlockAttentionBackend", self.platform.get_attention_backend_cls(_Backend.BLOCK_ATTN)) + + def test_attention_backend_invalid(self): + """Verify invalid backend returns None""" + self.assertIsNone(self.platform.get_attention_backend_cls("INVALID_BACKEND")) + + +class TestGCUPlatform(unittest.TestCase): + """Test suite for GCUPlatform""" + + def setUp(self): + self.platform = GCUPlatform() + + @patch("paddle.base.core.get_custom_device_count", return_value=1) + def test_available_with_gcu(self, mock_get_count): + """Verify available() returns True when GCU exists""" + self.assertTrue(self.platform.available()) + + @patch("paddle.base.core.get_custom_device_count", side_effect=Exception("No GCU")) + def test_available_no_gcu(self, mock_get_count): + """Verify available() returns False when no GCU""" + self.assertFalse(self.platform.available()) + + def test_attention_backend_native(self): + """Verify NATIVE_ATTN returns correct backend class""" + self.assertIn("GCUMemEfficientAttnBackend", self.platform.get_attention_backend_cls(_Backend.NATIVE_ATTN)) + + def test_attention_backend_append(self): + """Verify APPEND_ATTN returns correct backend class""" + self.assertIn("GCUFlashAttnBackend", self.platform.get_attention_backend_cls(_Backend.APPEND_ATTN)) + + def test_attention_backend_invalid(self): + """Verify invalid backend raises ValueError""" + with self.assertRaises(ValueError): + self.platform.get_attention_backend_cls("INVALID_BACKEND") + + +class TestXPUPlatform(unittest.TestCase): + """Test suite for XPUPlatform""" + + @patch("paddle.is_compiled_with_xpu", return_value=True) + @patch("paddle.static.xpu_places", return_value=[0]) + def test_available_true(self, mock_places, mock_xpu): + """Verify available() returns True when XPU is compiled and available""" + self.assertTrue(XPUPlatform.available()) + + @patch("paddle.is_compiled_with_xpu", return_value=False) + @patch("paddle.static.xpu_places", return_value=[]) + def test_available_false(self, mock_places, mock_xpu): + """Verify available() returns False when XPU is unavailable""" + self.assertFalse(XPUPlatform.available()) + + def test_get_attention_backend_cls(self): + """Verify NATIVE_ATTN returns correct XPU backend class""" + expected_cls = "fastdeploy.model_executor.layers.attention.XPUAttentionBackend" + self.assertEqual(XPUPlatform.get_attention_backend_cls(_Backend.NATIVE_ATTN), expected_cls) + + if __name__ == "__main__": unittest.main() diff --git a/tests/scheduler/test_workers.py b/tests/scheduler/test_workers.py new file mode 100644 index 00000000000..c8fd1fa3d45 --- /dev/null +++ b/tests/scheduler/test_workers.py @@ -0,0 +1,277 @@ +""" +# Copyright (c) 2025 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. +""" + +import time +import unittest + +from fastdeploy.scheduler.workers import Task, Workers + + +class TestTask(unittest.TestCase): + def test_repr(self): + """Test the __repr__ method of Task class. + + Verifies that the string representation of Task contains key attributes + (task_id and reason) with correct values. + """ + task = Task("123", 456, reason="ok") + repr_str = repr(task) + self.assertIn("task_id:123", repr_str) + self.assertIn("reason:ok", repr_str) + + +class TestWorkers(unittest.TestCase): + """Unit test suite for the Workers class. + + Covers core functionalities including task processing flow, filtering, unique task addition, + timeout handling, exception resilience, and edge cases like empty inputs or zero workers. + """ + + def test_basic_flow(self): + """Test basic task processing flow with multiple tasks and workers. + + Verifies that Workers can start multiple worker threads, process batched tasks, + and return correct results in expected format. + """ + + def simple_work(tasks): + """Simple work function that increments task raw value by 1.""" + return [Task(task.id, task.raw + 1) for task in tasks] + + workers = Workers("test_basic_flow", work=simple_work, max_task_batch_size=2) + workers.start(2) + + tasks = [Task(str(i), i) for i in range(4)] + workers.add_tasks(tasks) + + # Collect results with timeout protection + results = [] + start_time = time.time() + while len(results) < 4 and time.time() - start_time < 1: + batch_results = workers.get_results(10, timeout=0.1) + if batch_results: + results.extend(batch_results) + + # Clean up resources + workers.terminate() + + result_map = {int(task.id): task.raw for task in results} + self.assertEqual(result_map, {0: 1, 1: 2, 2: 3, 3: 4}) + + def test_task_filters(self): + """Test task filtering functionality. + + Verifies that Workers apply specified task filters correctly and process + all eligible tasks without dropping or duplicating. + """ + + def work_function(tasks): + """Work function that adds 10 to task raw value.""" + return [Task(task.id, task.raw + 10) for task in tasks] + + # Define filter functions: even and odd task ID filters + def filter_even(task): + """Filter to select tasks with even-numbered IDs.""" + return int(task.id) % 2 == 0 + + def filter_odd(task): + """Filter to select tasks with odd-numbered IDs.""" + return int(task.id) % 2 == 1 + + # Initialize Workers with filter chain and 2 worker threads + workers = Workers( + "test_task_filters", + work=work_function, + max_task_batch_size=1, + task_filters=[filter_even, filter_odd], + ) + workers.start(2) + + # Add 6 tasks with IDs 0-5 + workers.add_tasks([Task(str(i), i) for i in range(6)]) + + # Collect results with timeout protection + results = [] + start_time = time.time() + while len(results) < 6 and time.time() - start_time < 2: + batch_results = workers.get_results(10, timeout=0.1) + if batch_results: + results.extend(batch_results) + + # Clean up resources + workers.terminate() + + # Expected task ID groups + even_ids = {0, 2, 4} + odd_ids = {1, 3, 5} + + # Extract original IDs from results (reverse work function calculation) + got_even = {int(task.raw) - 10 for task in results if int(task.id) in even_ids} + got_odd = {int(task.raw) - 10 for task in results if int(task.id) in odd_ids} + + # Verify all even and odd tasks were processed correctly + self.assertEqual(got_even, even_ids) + self.assertEqual(got_odd, odd_ids) + + def test_unique_task_addition(self): + """Test unique task addition functionality. + + Verifies that duplicate tasks (same task_id) are filtered out when unique=True, + while new tasks are processed normally. + """ + + def slow_work(tasks): + """Slow work function to simulate processing delay (50ms).""" + time.sleep(0.05) + return [Task(task.id, task.raw + 1) for task in tasks] + + # Initialize Workers with 1 worker thread (to control task processing order) + workers = Workers("test_unique_task_addition", work=slow_work, max_task_batch_size=1) + workers.start(1) + + # Add first task (task_id="1") with unique=True + workers.add_tasks([Task("1", 100)], unique=True) + time.sleep(0.02) # Allow task to enter running state + + # Add duplicate task (same task_id="1") - should be filtered out + workers.add_tasks([Task("1", 200)], unique=True) + # Add new task (task_id="2") - should be processed + workers.add_tasks([Task("2", 300)], unique=True) + + # Collect results (expect 2 valid results) + results = [] + start_time = time.time() + while len(results) < 2 and time.time() - start_time < 1: + batch_results = workers.get_results(10, timeout=0.1) + if batch_results: + results.extend(batch_results) + + # Clean up resources + workers.terminate() + + # Verify only unique task IDs are present + result_ids = sorted(int(task.id) for task in results) + self.assertEqual(result_ids, [1, 2]) + + def test_get_results_timeout(self): + """Test timeout handling in get_results method. + + Verifies that get_results returns empty list after specified timeout when + no results are available, and the actual wait time meets the timeout requirement. + """ + + def no_result_work(tasks): + """Work function that returns empty list (no results).""" + time.sleep(0.01) + return [] + + # Initialize Workers with 1 worker thread + workers = Workers("test_get_results_timeout", work=no_result_work, max_task_batch_size=1) + workers.start(1) + + # Measure time taken for get_results with 50ms timeout + start_time = time.time() + results = workers.get_results(max_size=1, timeout=0.05) + end_time = time.time() + + # Clean up resources + workers.terminate() + + # Verify no results are returned and timeout is respected + self.assertEqual(results, []) + self.assertGreaterEqual(end_time - start_time, 0.05) + + def test_start_zero_workers(self): + """Test starting Workers with zero worker threads. + + Verifies that Workers initializes correctly with zero threads and the worker pool is empty. + """ + # Initialize Workers without specifying max_task_batch_size (uses default) + workers = Workers("test_start_zero_workers", work=lambda tasks: tasks) + workers.start(0) + + # Verify worker pool is empty + self.assertEqual(len(workers.pool), 0) + + def test_worker_exception_resilience(self): + """Test Workers resilience to exceptions in work function. + + Verifies that worker threads continue running (or complete gracefully) when + the work function raises an exception, without crashing the entire Workers instance. + """ + # Track number of work function calls + call_tracker = {"count": 0} + + def error_prone_work(tasks): + """Work function that raises RuntimeError on each call.""" + call_tracker["count"] += 1 + raise RuntimeError("Simulated work function exception") + + # Initialize Workers with 1 worker thread + workers = Workers("test_worker_exception_resilience", work=error_prone_work, max_task_batch_size=1) + workers.start(1) + + # Add a test task that will trigger the exception + workers.add_tasks([Task("1", 100)]) + time.sleep(0.05) # Allow time for exception to be raised + + # Clean up resources + workers.terminate() + + # Verify work function was called at least once (exception was triggered) + self.assertGreaterEqual(call_tracker["count"], 1) + + def test_add_empty_tasks(self): + """Test adding empty task list to Workers. + + Verifies that adding an empty list of tasks does not affect Workers state + and no invalid operations are performed. + """ + # Initialize Workers with 1 worker thread + workers = Workers("test_add_empty_tasks", work=lambda tasks: tasks) + workers.start(1) + + # Add empty task list + workers.add_tasks([]) + + # Verify task queue remains empty + self.assertEqual(len(workers.tasks), 0) + + # Clean up resources + workers.terminate() + + def test_terminate_empty_workers(self): + """Test terminating Workers that have no running threads or tasks. + + Verifies that terminate() can be safely called on an unstarted Workers instance + without errors, and all state variables remain in valid initial state. + """ + # Initialize Workers without starting any threads + workers = Workers("test_terminate_empty_workers", work=lambda tasks: tasks) + + # Call terminate on empty Workers + workers.terminate() + + # Verify Workers state remains valid + self.assertFalse(workers.stop) + self.assertEqual(workers.stopped_count, 0) + self.assertEqual(len(workers.pool), 0) + self.assertEqual(len(workers.tasks), 0) + self.assertEqual(len(workers.results), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/splitwise/test_internal_adapter_utils.py b/tests/splitwise/test_internal_adapter_utils.py new file mode 100644 index 00000000000..4d772789848 --- /dev/null +++ b/tests/splitwise/test_internal_adapter_utils.py @@ -0,0 +1,223 @@ +""" +# Copyright (c) 2025 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. +""" + +import threading +import unittest +from unittest.mock import MagicMock, patch + +from fastdeploy.splitwise import internal_adapter_utils as ia + + +class DummyEngine: + """Dummy Engine class to simulate the actual Engine for testing.""" + + class ResourceManager: + def available_batch(self): + return 4 + + def available_block_num(self): + return 2 + + class Scheduler: + def get_unhandled_request_num(self): + return 0 + + class EngineWorkerQueue: + def __init__(self): + self.called_task = None + + def put_connect_rdma_task(self, task): + self.called_task = task + + def get_connect_rdma_task_response(self): + return None + + def __init__(self): + self.resource_manager = self.ResourceManager() + self.scheduler = self.Scheduler() + self.engine_worker_queue = self.EngineWorkerQueue() + + +class DummyCfg: + """Dummy configuration class to simulate input config for InternalAdapter. + + Contains nested configuration classes (SchedulerConfig, CacheConfig, ModelConfig) + with test-friendly default values. + """ + + class SchedulerConfig: + """Mock SchedulerConfig with splitwise role configuration.""" + + splitwise_role = "single" + + class CacheConfig: + """Mock CacheConfig with cache-related configuration.""" + + block_size = 1024 + total_block_num = 8 + dec_token_num = 4 + + class ModelConfig: + """Mock ModelConfig with model-related configuration.""" + + max_model_len = 2048 + + # Top-level configuration attributes + max_prefill_batch = 2 + scheduler_config = SchedulerConfig() + cache_config = CacheConfig() + model_config = ModelConfig() + + +class TestInternalAdapterBasic(unittest.TestCase): + """ + Unit test suite for basic functionalities of InternalAdapter. + Covers initialization, server info retrieval, and thread creation. + """ + + @patch("fastdeploy.splitwise.internal_adapter_utils.ZmqTcpServer") + def test_basic_initialization(self, mock_zmq_server): + """Test InternalAdapter initialization and _get_current_server_info method.""" + # Setup mock ZmqTcpServer instance + mock_server_instance = MagicMock() + mock_zmq_server.return_value = mock_server_instance + + # Initialize InternalAdapter with dummy config, engine, and dp_rank + adapter = ia.InternalAdapter(cfg=DummyCfg(), engine=DummyEngine(), dp_rank=0) + + # Verify _get_current_server_info returns expected structure + server_info = adapter._get_current_server_info() + expected_keys = ["splitwise_role", "block_size", "available_resource"] + for key in expected_keys: + with self.subTest(key=key): + self.assertIn(key, server_info, f"Server info missing required key: {key}") + + # Verify background threads are properly initialized + self.assertTrue( + isinstance(adapter.recv_external_instruct_thread, threading.Thread), + "recv_external_instruct_thread should be a Thread instance", + ) + self.assertTrue( + isinstance(adapter.response_external_instruct_thread, threading.Thread), + "response_external_instruct_thread should be a Thread instance", + ) + + +class TestInternalAdapterRecvPayload(unittest.TestCase): + """Unit test suite for payload reception functionality of InternalAdapter. + + Covers handling of different control commands (get_payload, get_metrics, connect_rdma) + and exception handling. + """ + + @patch("fastdeploy.splitwise.internal_adapter_utils.ZmqTcpServer") + @patch("fastdeploy.splitwise.internal_adapter_utils.get_filtered_metrics") + @patch("fastdeploy.splitwise.internal_adapter_utils.logger") + def test_recv_control_cmd_branches(self, mock_logger, mock_get_metrics, mock_zmq_server): + """Test all command handling branches in _recv_external_module_control_instruct.""" + # Setup mock ZmqTcpServer instance + mock_server_instance = MagicMock() + mock_zmq_server.return_value = mock_server_instance + + # Create a generator to simulate sequential control commands + def control_cmd_generator(): + """Generator to yield test commands in sequence.""" + yield {"task_id": "1", "cmd": "get_payload"} + yield {"task_id": "2", "cmd": "get_metrics"} + yield {"task_id": "3", "cmd": "connect_rdma"} + while True: + yield None + + # Configure mock server to return commands from the generator + mock_server_instance.recv_control_cmd.side_effect = control_cmd_generator() + mock_server_instance.response_for_control_cmd = MagicMock() # Track response calls + mock_get_metrics.return_value = "mocked_metrics" # Mock metrics response + + # Initialize dependencies and InternalAdapter + test_engine = DummyEngine() + adapter = ia.InternalAdapter(cfg=DummyCfg(), engine=test_engine, dp_rank=0) + + # Override _recv_external_module_control_instruct to run only 3 iterations (test all commands) + def run_limited_iterations(self): + """Modified method to process 3 commands and exit (avoids infinite loop).""" + for _ in range(3): + try: + # Acquire response lock and receive command + with self.response_lock: + control_cmd = self.recv_control_cmd_server.recv_control_cmd() + + if control_cmd is None: + continue # Skip None commands + + task_id = control_cmd["task_id"] + cmd = control_cmd["cmd"] + + # Handle each command type + if cmd == "get_payload": + payload_info = self._get_current_server_info() + response = {"task_id": task_id, "result": payload_info} + with self.response_lock: + self.recv_control_cmd_server.response_for_control_cmd(task_id, response) + elif cmd == "get_metrics": + metrics_data = mock_get_metrics() + response = {"task_id": task_id, "result": metrics_data} + with self.response_lock: + self.recv_control_cmd_server.response_for_control_cmd(task_id, response) + elif cmd == "connect_rdma": + test_engine.engine_worker_queue.put_connect_rdma_task(control_cmd) + except Exception as e: + mock_logger.error(f"handle_control_cmd got error: {e}") + + # Bind the modified method to the adapter instance + adapter._recv_external_module_control_instruct = run_limited_iterations.__get__(adapter) + # Execute the modified method to process test commands + adapter._recv_external_module_control_instruct() + + # Verify 'get_payload' and 'get_metrics' triggered responses (2 total calls) + self.assertEqual( + mock_server_instance.response_for_control_cmd.call_count, + 2, + "response_for_control_cmd should be called twice (get_payload + get_metrics)", + ) + + # Verify responses were sent for task IDs "1" and "2" + called_task_ids = [call_arg[0][0] for call_arg in mock_server_instance.response_for_control_cmd.call_args_list] + self.assertIn("1", called_task_ids, "Response not sent for 'get_payload' task (ID: 1)") + self.assertIn("2", called_task_ids, "Response not sent for 'get_metrics' task (ID: 2)") + + # Verify 'connect_rdma' task was submitted to EngineWorkerQueue + self.assertEqual( + test_engine.engine_worker_queue.called_task["task_id"], + "3", + "connect_rdma task with ID 3 not received by EngineWorkerQueue", + ) + + # Test exception handling branch + def raise_test_exception(self): + """Modified method to raise a test exception.""" + raise ValueError("test_exception") + + # Configure mock server to trigger exception + adapter.recv_control_cmd_server.recv_control_cmd = raise_test_exception.__get__(adapter) + # Execute to trigger exception + adapter._recv_external_module_control_instruct() + + # Verify exception was logged + self.assertTrue(mock_logger.error.called, "Logger should capture exceptions during control command handling") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/v1/test_resource_manager_v1.py b/tests/v1/test_resource_manager_v1.py index 4534fb60df1..bb25873f066 100644 --- a/tests/v1/test_resource_manager_v1.py +++ b/tests/v1/test_resource_manager_v1.py @@ -1,3 +1,19 @@ +""" +# Copyright (c) 2025 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. +""" + import concurrent.futures import pickle import unittest