From bcbc2dcf69c8860e445cacc05b41f5ba15f877d6 Mon Sep 17 00:00:00 2001 From: Howard Su Date: Thu, 13 Aug 2026 21:25:53 +0800 Subject: [PATCH 1/3] convert smoke tests to ctest --- server/CMakeLists.txt | 105 ++++++++++++++--------- server/test/model_test_paths.h | 52 +++++++++++ server/test/smoke_load_draft.cpp | 29 ++++--- server/test/smoke_load_target.cpp | 28 +++--- server/test/smoke_load_target_laguna.cpp | 25 +++--- server/test/test_model_test_paths.cpp | 35 ++++++++ server/test/test_paged_attention.cpp | 68 +++++++-------- 7 files changed, 234 insertions(+), 108 deletions(-) create mode 100644 server/test/model_test_paths.h create mode 100644 server/test/test_model_test_paths.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 0ed5743fb..c0cd9d553 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1257,11 +1257,6 @@ if(DFLASH27B_TESTS) target_link_libraries(test_ds4_dspark_load PRIVATE hip::host) endif() endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/smoke_load_draft.cpp") - add_executable(smoke_load_draft test/smoke_load_draft.cpp) - target_include_directories(smoke_load_draft PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(smoke_load_draft PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET}) - endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/spike_thin_copy.cpp") add_executable(spike_thin_copy test/spike_thin_copy.cpp) target_include_directories(spike_thin_copy PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) @@ -1282,16 +1277,6 @@ if(DFLASH27B_TESTS) target_include_directories(test_vs_oracle PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) target_link_libraries(test_vs_oracle PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET}) endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/smoke_load_target.cpp") - add_executable(smoke_load_target test/smoke_load_target.cpp) - target_include_directories(smoke_load_target PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(smoke_load_target PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET}) - endif() - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/smoke_load_target_laguna.cpp") - add_executable(smoke_load_target_laguna test/smoke_load_target_laguna.cpp) - target_include_directories(smoke_load_target_laguna PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) - target_link_libraries(smoke_load_target_laguna PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET}) - endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/smoke_laguna_forward.cpp") add_executable(smoke_laguna_forward test/smoke_laguna_forward.cpp) target_include_directories(smoke_laguna_forward PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) @@ -1479,6 +1464,7 @@ if(DFLASH27B_TESTS) test/test_drafter_tail_capture_guard.cpp test/test_drafter_warm_path_regression.cpp test/test_qwen3_buffer_plan.cpp + test/test_model_test_paths.cpp test/test_gguf_mmap.cpp test/test_kv_quant.cpp test/test_kvflash_placement.cpp @@ -1557,6 +1543,68 @@ if(DFLASH27B_TESTS) list(APPEND _raw_unit_test_targets test_server_unit) endif() + if((DFLASH27B_GPU_BACKEND STREQUAL "cuda" OR + DFLASH27B_GPU_BACKEND STREQUAL "hip") + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/smoke_load_draft.cpp" + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/smoke_load_target.cpp" + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/smoke_load_target_laguna.cpp" + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_paged_attention.cpp") + add_executable(test_model_smoke + test/test_unit_main.cpp + test/smoke_load_draft.cpp + test/smoke_load_target.cpp + test/smoke_load_target_laguna.cpp + test/test_paged_attention.cpp) + target_include_directories(test_model_smoke PRIVATE + ${DFLASH27B_SRC_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/src) + if(DFLASH27B_GPU_BACKEND STREQUAL "hip") + target_compile_definitions(test_model_smoke PRIVATE + DFLASH27B_BACKEND_HIP=1 + GGML_USE_HIP) + else() + target_compile_definitions(test_model_smoke PRIVATE + DFLASH27B_BACKEND_CUDA=1 + DFLASH27B_CUDA_MIN_SM=${_dflash_cuda_min_sm}) + endif() + target_link_libraries(test_model_smoke PRIVATE + dflash_common + ggml + ggml-base + ${DFLASH27B_GGML_BACKEND_TARGET}) + if(DFLASH27B_GPU_BACKEND STREQUAL "cuda") + find_package(CUDAToolkit REQUIRED) + target_link_libraries(test_model_smoke PRIVATE CUDA::cudart) + else() + target_link_libraries(test_model_smoke PRIVATE hip::host) + endif() + if(CMAKE_CROSSCOMPILING) + add_test(NAME model_smoke COMMAND test_model_smoke) + else() + set(_model_smoke_ctest_generated + "${CMAKE_CURRENT_BINARY_DIR}/test_model_smoke_discovered_tests.cmake") + set(_model_smoke_ctest_include + "${CMAKE_CURRENT_BINARY_DIR}/test_model_smoke_discovered_tests_include.cmake") + file(GENERATE OUTPUT "${_model_smoke_ctest_include}" + CONTENT "include([==[${_model_smoke_ctest_generated}]==] OPTIONAL)\n") + set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "${_model_smoke_ctest_include}") + add_custom_command(TARGET test_model_smoke POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DTEST_EXECUTABLE=$ + -DTEST_WORKING_DIR=${CMAKE_CURRENT_BINARY_DIR} + -DCTEST_FILE=${_model_smoke_ctest_generated} + -DTEST_PREFIX=test_model_smoke. + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/DiscoverCppUnitTests.cmake + VERBATIM + COMMENT "Discovering CppUnitTestFramework tests for test_model_smoke") + unset(_model_smoke_ctest_generated) + unset(_model_smoke_ctest_include) + endif() + list(APPEND _raw_unit_test_targets test_model_smoke) + endif() + # Feature/architecture gate tests. check_feature_compatibility(), # collect_feature_warnings() and the capability table are pure functions, # so this target deliberately compiles only feature_gate.cpp and @@ -1662,11 +1710,8 @@ if(DFLASH27B_TESTS) # CUDA::cudart so the toolkit headers are on the compile line (same as # test_dflash historically had alone). set(_dflash_internal_h_cuda_tests - smoke_load_draft smoke_draft_graph test_vs_oracle - smoke_load_target - smoke_load_target_laguna smoke_laguna_forward bench_laguna_ttft bench_laguna_pflash @@ -1674,6 +1719,7 @@ if(DFLASH27B_TESTS) smoke_target_forward test_generate test_dflash + test_model_smoke ) foreach(_t IN LISTS _dflash_internal_h_cuda_tests) if(TARGET ${_t}) @@ -1695,29 +1741,6 @@ if(DFLASH27B_TESTS) endif() endif() - if((DFLASH27B_GPU_BACKEND STREQUAL "cuda" OR - DFLASH27B_GPU_BACKEND STREQUAL "hip") - AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_paged_attention.cpp") - add_executable(test_paged_attention test/test_paged_attention.cpp) - target_link_libraries(test_paged_attention PRIVATE - ggml ${DFLASH27B_GGML_BACKEND_TARGET} ggml-base) - if(DFLASH27B_GPU_BACKEND STREQUAL "cuda") - target_link_libraries(test_paged_attention PRIVATE CUDA::cudart) - else() - target_link_libraries(test_paged_attention PRIVATE hip::host) - endif() - target_include_directories(test_paged_attention PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include - ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/src) - add_test(NAME paged_attention COMMAND test_paged_attention) - add_test(NAME paged_attention_direct - COMMAND test_paged_attention --direct) - set_tests_properties(paged_attention_direct PROPERTIES - ENVIRONMENT "GGML_CUDA_PAGED_ATTN_FORCE_PARTITIONS=1") - if(TARGET check) - add_dependencies(check test_paged_attention) - endif() - endif() if((DFLASH27B_GPU_BACKEND STREQUAL "cuda" OR DFLASH27B_GPU_BACKEND STREQUAL "hip") AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/bench_paged_attention.cpp") diff --git a/server/test/model_test_paths.h b/server/test/model_test_paths.h new file mode 100644 index 000000000..e6cc630d8 --- /dev/null +++ b/server/test/model_test_paths.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace luce_test { + +inline constexpr std::string_view kQwen35ModelEnv = "LUCE_TEST_MODEL_QWEN35"; +inline constexpr std::string_view kQwen3ModelEnv = "LUCE_TEST_MODEL_QWEN3"; +inline constexpr std::string_view kDraftModelEnv = "LUCE_TEST_MODEL_DRAFT"; +inline constexpr std::string_view kDeepSeek4ModelEnv = "LUCE_TEST_MODEL_DEEPSEEK4"; +inline constexpr std::string_view kLagunaModelEnv = "LUCE_TEST_MODEL_LAGUNA"; +inline constexpr std::string_view kOracleDirectoryEnv = "LUCE_TEST_ORACLE_DIR"; + +inline std::filesystem::path require_path( + const std::string_view environment_variable, + const bool directory = false +) { + const std::string name(environment_variable); + const char * const value = std::getenv(name.c_str()); + if (!value || !*value) { + throw std::runtime_error( + "required test asset is not configured: set " + name + ); + } + + const std::filesystem::path path(value); + std::error_code error; + const bool valid = directory + ? std::filesystem::is_directory(path, error) + : std::filesystem::is_regular_file(path, error); + if (!valid || error) { + throw std::runtime_error( + "configured test asset is unavailable for " + name + ": " + path.string() + ); + } + + return path; +} + +inline std::filesystem::path require_model(const std::string_view environment_variable) { + return require_path(environment_variable); +} + +inline std::filesystem::path require_directory(const std::string_view environment_variable) { + return require_path(environment_variable, true); +} + +} // namespace luce_test diff --git a/server/test/smoke_load_draft.cpp b/server/test/smoke_load_draft.cpp index 9e0f37b42..319512732 100644 --- a/server/test/smoke_load_draft.cpp +++ b/server/test/smoke_load_draft.cpp @@ -6,6 +6,8 @@ #include "dflash27b.h" #include "internal.h" +#include "CppUnitTestFramework.hpp" +#include "model_test_paths.h" #include "ggml.h" #include "ggml-backend.h" @@ -18,28 +20,26 @@ #include using namespace dflash::common; +using namespace CppUnitTestFramework; -int main(int argc, char ** argv) { - if (argc < 2) { - std::fprintf(stderr, "usage: %s \n", argv[0]); - return 2; - } - const char * path = argv[1]; +struct SmokeLoadDraft : CommonFixture { + using CommonFixture::CommonFixture; +}; +TEST_CASE(SmokeLoadDraft, LoadsConfiguredModel) { + const auto path = luce_test::require_model(luce_test::kDraftModelEnv); // Initialize CUDA backend ggml_backend_t backend = ggml_backend_cuda_init(0); - if (!backend) { - std::fprintf(stderr, "ggml_backend_cuda_init(0) failed\n"); - return 1; - } + REQUIRE_NOT_NULL(backend); std::printf("cuda backend: %s\n", ggml_backend_name(backend)); DraftWeights w; - if (!load_draft_safetensors(path, backend, w)) { + const bool loaded = load_draft_safetensors(path.c_str(), backend, w); + if (!loaded) { std::fprintf(stderr, "load_draft_safetensors failed: %s\n", dflash27b_last_error()); ggml_backend_free(backend); - return 1; + REQUIRE(loaded); } // Count tensors and total bytes @@ -79,8 +79,11 @@ int main(int argc, char ** argv) { } std::printf("\n"); + CHECK_NOT_NULL(w.fc); + CHECK_NOT_NULL(w.hidden_norm); + CHECK(!w.layers.empty()); + free_draft_weights(w); ggml_backend_free(backend); std::printf("OK\n"); - return 0; } diff --git a/server/test/smoke_load_target.cpp b/server/test/smoke_load_target.cpp index 6f81cc47f..0af89c347 100644 --- a/server/test/smoke_load_target.cpp +++ b/server/test/smoke_load_target.cpp @@ -5,6 +5,8 @@ #include "dflash27b.h" #include "internal.h" +#include "CppUnitTestFramework.hpp" +#include "model_test_paths.h" #include "ggml.h" #include "ggml-backend.h" @@ -17,20 +19,22 @@ #include using namespace dflash::common; +using namespace CppUnitTestFramework; -int main(int argc, char ** argv) { - if (argc < 2) { - std::fprintf(stderr, "usage: %s \n", argv[0]); - return 2; - } - +struct SmokeLoadTarget : CommonFixture { + using CommonFixture::CommonFixture; +}; +TEST_CASE(SmokeLoadTarget, LoadsConfiguredModel) { + const auto path = luce_test::require_model(luce_test::kQwen35ModelEnv); ggml_backend_t backend = ggml_backend_cuda_init(0); - if (!backend) { std::fprintf(stderr, "cuda init failed\n"); return 1; } + REQUIRE_NOT_NULL(backend); TargetWeights w; - if (!load_target_gguf(argv[1], backend, w)) { + const bool loaded = load_target_gguf(path.c_str(), backend, w); + if (!loaded) { std::fprintf(stderr, "load_target_gguf failed: %s\n", dflash27b_last_error()); - return 1; + ggml_backend_free(backend); + REQUIRE(loaded); } // load_target_gguf stashes a summary string in last_error on success (hack) std::printf("%s\n", dflash27b_last_error()); @@ -101,8 +105,12 @@ int main(int argc, char ** argv) { if (w.n_layer > 31) print_layer(31); if (w.n_layer > 63) print_layer(63); + CHECK(w.n_layer > 0); + CHECK_NOT_NULL(w.tok_embd); + CHECK_NOT_NULL(w.output); + CHECK_NOT_NULL(w.out_norm); + free_target_weights(w); ggml_backend_free(backend); std::printf("OK\n"); - return 0; } diff --git a/server/test/smoke_load_target_laguna.cpp b/server/test/smoke_load_target_laguna.cpp index c426cc1ee..2386cf3b9 100644 --- a/server/test/smoke_load_target_laguna.cpp +++ b/server/test/smoke_load_target_laguna.cpp @@ -7,6 +7,8 @@ #include "dflash27b.h" #include "internal.h" #include "laguna_internal.h" +#include "CppUnitTestFramework.hpp" +#include "model_test_paths.h" #include #include @@ -16,22 +18,23 @@ #include "ggml-cuda.h" using namespace dflash::common; +using namespace CppUnitTestFramework; -int main(int argc, char ** argv) { - if (argc < 2) { - std::fprintf(stderr, "usage: %s \n", argv[0]); - return 2; - } - const std::string path = argv[1]; +struct SmokeLoadLagunaTarget : CommonFixture { + using CommonFixture::CommonFixture; +}; +TEST_CASE(SmokeLoadLagunaTarget, LoadsConfiguredModel) { + const std::string path = luce_test::require_model(luce_test::kLagunaModelEnv).string(); ggml_backend_t backend = ggml_backend_cuda_init(0); - if (!backend) { std::fprintf(stderr, "[smoke-laguna] cuda init failed\n"); return 1; } + REQUIRE_NOT_NULL(backend); LagunaTargetWeights w; - if (!load_target_gguf_laguna(path, backend, w)) { + const bool loaded = load_target_gguf_laguna(path, backend, w); + if (!loaded) { std::fprintf(stderr, "[smoke-laguna] load_target_gguf_laguna failed: %s\n", dflash27b_last_error()); ggml_backend_free(backend); - return 1; + REQUIRE(loaded); } std::printf("[smoke-laguna] loaded %s\n", path.c_str()); @@ -47,8 +50,10 @@ int main(int argc, char ** argv) { w.expert_weights_scale, (int)w.expert_gating_sigmoid, w.n_layer_dense_lead); std::printf(" eos_ids=[%d, %d] pad=%d\n", w.eos_id, w.eos_chat_id, w.pad_id); + CHECK(w.n_layer > 0); + CHECK(w.n_embd > 0); + free_laguna_target_weights(w); ggml_backend_free(backend); std::printf("[smoke-laguna] OK\n"); - return 0; } diff --git a/server/test/test_model_test_paths.cpp b/server/test/test_model_test_paths.cpp new file mode 100644 index 000000000..abc61f3bb --- /dev/null +++ b/server/test/test_model_test_paths.cpp @@ -0,0 +1,35 @@ +#include "CppUnitTestFramework.hpp" +#include "model_test_paths.h" +#include "scoped_env.h" + +#include +#include + +using namespace CppUnitTestFramework; + +struct ModelTestPaths : CommonFixture { + using CommonFixture::CommonFixture; +}; +TEST_CASE(ModelTestPaths, RejectsMissingRequiredAsset) { + constexpr char kTestEnvironmentVariable[] = "LUCE_TEST_MODEL_PATHS_TEST"; + luce_test::ScopedEnvVar unset(kTestEnvironmentVariable, nullptr); + + REQUIRE_THROW( + std::runtime_error, + luce_test::require_model(kTestEnvironmentVariable) + ); +} + +TEST_CASE(ModelTestPaths, ResolvesConfiguredDirectory) { + constexpr char kTestEnvironmentVariable[] = "LUCE_TEST_MODEL_PATHS_TEST"; + const auto temporary_directory = std::filesystem::temp_directory_path(); + luce_test::ScopedEnvVar set( + kTestEnvironmentVariable, + temporary_directory.string().c_str() + ); + + REQUIRE_EQUAL( + luce_test::require_directory(kTestEnvironmentVariable).string(), + temporary_directory.string() + ); +} diff --git a/server/test/test_paged_attention.cpp b/server/test/test_paged_attention.cpp index a82fce93d..649994693 100644 --- a/server/test/test_paged_attention.cpp +++ b/server/test/test_paged_attention.cpp @@ -2,6 +2,8 @@ #include "ggml-alloc.h" #include "ggml-backend.h" #include "ggml-cuda.h" +#include "CppUnitTestFramework.hpp" +#include "scoped_env.h" #include #include @@ -11,6 +13,8 @@ #include #include +using namespace CppUnitTestFramework; + namespace { constexpr int D = 256; @@ -332,49 +336,45 @@ bool rejects_unlaunchable_gqa(ggml_backend_t backend) { } // namespace -int main(int argc, char ** argv) { - ggml_backend_t backend = ggml_backend_cuda_init(0); - if (!backend) { - std::fprintf(stderr, "GPU backend unavailable\n"); - return 1; - } +struct PagedAttention : CommonFixture { + using CommonFixture::CommonFixture; - const TestCase partitioned_case{ - "partitioned", - 65, - // Retains page boundaries and >64 blocks, while pinning both context - // clamps: negative becomes empty and over-capacity becomes 65 blocks. - {-7, 1, 15, 16, 17, 31, 33, 257, 511, 1025, 2000}, - true, - }; - // CTest also runs this case with the environment - // GGML_CUDA_PAGED_ATTN_FORCE_PARTITIONS=1 (paged_attention_direct), - // so despite its name it pins the partitioned path on small shapes. - const TestCase direct_case{ - "direct", - 64, - {0, 1, 15, 16, 17, 257, 511}, - false, - }; - const bool direct = - argc == 2 && std::strcmp(argv[1], "--direct") == 0; - if (argc > 2 || (argc == 2 && !direct)) { - std::fprintf(stderr, "usage: %s [--direct]\n", argv[0]); - ggml_backend_free(backend); - return 2; - } - const TestCase & test_case = direct ? direct_case : partitioned_case; +void run_paged_attention_case(const TestCase & test_case) { + ggml_backend_t backend = ggml_backend_cuda_init(0); + REQUIRE_NOT_NULL(backend); const ggml_type types[] = { GGML_TYPE_F16, GGML_TYPE_Q4_0, GGML_TYPE_Q8_0, }; - bool ok = rejects_unlaunchable_gqa(backend); + CHECK(rejects_unlaunchable_gqa(backend)); for (ggml_type k_type : types) { for (ggml_type v_type : types) { - ok = run_case(backend, test_case, k_type, v_type) && ok; + CHECK(run_case(backend, test_case, k_type, v_type)); } } ggml_backend_free(backend); - return ok ? 0 : 1; +} + +}; +TEST_CASE(PagedAttention, PartitionedPathMatchesReference) { + run_paged_attention_case({ + "partitioned", + 65, + {-7, 1, 15, 16, 17, 31, 33, 257, 511, 1025, 2000}, + true, + }); +} + +TEST_CASE(PagedAttention, DirectPathMatchesReference) { + luce_test::ScopedEnvVar force_partitions( + "GGML_CUDA_PAGED_ATTN_FORCE_PARTITIONS", + "1" + ); + run_paged_attention_case({ + "direct", + 64, + {0, 1, 15, 16, 17, 257, 511}, + false, + }); } From 9c929c9557136232897f3d30cfd86357174e0028 Mon Sep 17 00:00:00 2001 From: Howard Su Date: Thu, 13 Aug 2026 22:29:59 +0800 Subject: [PATCH 2/3] Migrate standalone tests to CppUnitTestFramework --- server/CMakeLists.txt | 73 +++- server/cmake/DiscoverCppUnitTests.cmake | 2 +- server/test/CppUnitTestFramework.hpp | 45 ++- server/test/model_test_paths.h | 7 +- server/test/test_cuda_comm_api.cpp | 32 +- server/test/test_deepseek4_hc_cuda.cpp | 23 +- .../test/test_ds4_dmix_entry_validation.cpp | 57 ++- .../test/test_ds4_mix_registry_teardown.cpp | 66 ++-- server/test/test_feature_gate.cpp | 341 +++++++++--------- server/test/test_flashprefill_kernels.cpp | 20 +- server/test/test_ggml_rmsnorm_batch.cpp | 48 ++- server/test/test_mmq_streamk_iq4_xs.cpp | 18 +- server/test/test_model_test_paths.cpp | 4 +- server/test/test_platform_compat.cpp | 33 +- server/test/test_qwen35_split_tree_guard.cpp | 47 ++- server/test/test_qwen35_tensor_parallel.cpp | 70 ++-- server/test/test_recurrent_snapshot.cpp | 34 +- server/test/test_rms_norm_hip.cpp | 22 +- server/test/test_rocmfp3_mix_registry.cpp | 20 +- server/test/test_rocmfp_mix_gateup_glu.cpp | 17 +- server/test/test_rocmfp_mix_glu_fusable.cpp | 13 +- server/test/test_rocmfp_mix_slice_matvec.cpp | 16 +- 22 files changed, 576 insertions(+), 432 deletions(-) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index c0cd9d553..716e5c570 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -808,7 +808,6 @@ if(DFLASH27B_TESTS) if(WIN32) target_link_libraries(test_platform_compat PRIVATE ws2_32) endif() - add_test(NAME platform_compat COMMAND test_platform_compat) endif() if(DFLASH27B_GPU_BACKEND STREQUAL "hip" @@ -832,7 +831,6 @@ if(DFLASH27B_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include) target_link_libraries(test_cuda_comm_api PRIVATE ggml-cuda ggml ggml-base) - add_test(NAME cuda_comm_api COMMAND test_cuda_comm_api) endif() if(DFLASH27B_ENABLE_MIXED_CUDA_HIP) @@ -902,7 +900,6 @@ if(DFLASH27B_TESTS) add_executable(test_qwen35_tensor_parallel test/test_qwen35_tensor_parallel.cpp) target_include_directories(test_qwen35_tensor_parallel PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS}) target_link_libraries(test_qwen35_tensor_parallel PRIVATE dflash_common) - add_test(NAME qwen35_tensor_parallel COMMAND test_qwen35_tensor_parallel) if(DFLASH27B_GPU_BACKEND STREQUAL "cuda" AND _dflash_cuda_min_sm GREATER_EQUAL 80 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_flashprefill_kernels.cpp") add_executable(test_flashprefill_kernels test/test_flashprefill_kernels.cpp) @@ -1065,7 +1062,6 @@ if(DFLASH27B_TESTS) find_package(CUDAToolkit REQUIRED) target_link_libraries(test_ggml_rmsnorm_batch PRIVATE CUDA::cudart) endif() - add_test(NAME ggml_rmsnorm_batch COMMAND test_ggml_rmsnorm_batch) endif() # POSIX-only fixture: it builds its temp model/sidecar stand-ins with symlink()/getpid() # under /tmp and probes them with access(R_OK). Porting that to Win32 would mean @@ -1214,7 +1210,6 @@ if(DFLASH27B_TESTS) else() target_link_libraries(test_ds4_mix_registry_teardown PRIVATE hip::host) endif() - add_test(NAME ds4_mix_registry_teardown COMMAND test_ds4_mix_registry_teardown) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_ds4_budget_hook.cpp") # Thinking-budget force-close rule. Header-only logic, so unlike the other ds4 tests @@ -1239,7 +1234,6 @@ if(DFLASH27B_TESTS) else() target_link_libraries(test_ds4_dmix_entry_validation PRIVATE hip::host) endif() - add_test(NAME ds4_dmix_entry_validation COMMAND test_ds4_dmix_entry_validation) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_ds4_dspark_load.cpp") add_executable(test_ds4_dspark_load test/test_ds4_dspark_load.cpp) @@ -1629,12 +1623,69 @@ if(DFLASH27B_TESTS) target_compile_definitions(test_feature_gate PRIVATE DFLASH27B_BACKEND_CUDA=1) endif() - add_test(NAME feature_gate COMMAND test_feature_gate) endif() + function(dflash_discover_cppunit_tests target) + if(CMAKE_CROSSCOMPILING) + add_test(NAME "${target}" COMMAND ${target}) + set_tests_properties("${target}" PROPERTIES SKIP_RETURN_CODE 77) + return() + endif() + + set(_ctest_generated + "${CMAKE_CURRENT_BINARY_DIR}/${target}_discovered_tests.cmake") + set(_ctest_include + "${CMAKE_CURRENT_BINARY_DIR}/${target}_discovered_tests_include.cmake") + file(GENERATE OUTPUT "${_ctest_include}" + CONTENT "include([==[${_ctest_generated}]==] OPTIONAL)\n") + set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "${_ctest_include}") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DTEST_EXECUTABLE=$ + -DTEST_WORKING_DIR=${CMAKE_CURRENT_BINARY_DIR} + -DCTEST_FILE=${_ctest_generated} + -DTEST_PREFIX=${target}. + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/DiscoverCppUnitTests.cmake + VERBATIM + COMMENT "Discovering CppUnitTestFramework tests for ${target}") + endfunction() + + set(_cppunit_test_targets + test_server_unit + test_model_smoke) + set(_new_cppunit_test_targets + test_platform_compat + test_feature_gate + test_qwen35_split_tree_guard + test_recurrent_snapshot + test_ds4_mix_registry_teardown + test_ds4_dmix_entry_validation + test_ggml_rmsnorm_batch + test_qwen35_tensor_parallel + test_deepseek4_hc_cuda + test_flashprefill_kernels + test_cuda_comm_api + test_mmq_streamk_iq4_xs + test_rms_norm_hip + test_rocmfp3_mix_registry + test_rocmfp_mix_slice_matvec + test_rocmfp_mix_gateup_glu + test_rocmfp_mix_glu_fusable) + foreach(_cppunit_target IN LISTS _new_cppunit_test_targets) + if(TARGET ${_cppunit_target}) + target_sources(${_cppunit_target} PRIVATE test/test_unit_main.cpp) + dflash_discover_cppunit_tests(${_cppunit_target}) + list(APPEND _cppunit_test_targets ${_cppunit_target}) + list(APPEND _raw_unit_test_targets ${_cppunit_target}) + endif() + endforeach() + unset(_new_cppunit_test_targets) + if(_raw_unit_test_targets) foreach(_unit_target IN LISTS _raw_unit_test_targets) - if(NOT _unit_target STREQUAL "test_server_unit") + list(FIND _cppunit_test_targets "${_unit_target}" _cppunit_target_index) + if(_cppunit_target_index EQUAL -1) # Preserve the established CTest names used by CI filters and # downstream scripts even though dependency tracking uses the # executable target names. @@ -1663,9 +1714,7 @@ if(DFLASH27B_TESTS) set(_unit_ctest_name paged_kv_pool) endif() add_test(NAME "${_unit_ctest_name}" COMMAND ${_unit_target}) - if(_unit_target STREQUAL "test_deepseek4_mmid_grouped_cuda") - set_tests_properties("${_unit_ctest_name}" PROPERTIES SKIP_RETURN_CODE 77) - endif() + set_tests_properties("${_unit_ctest_name}" PROPERTIES SKIP_RETURN_CODE 77) if(_unit_target STREQUAL "test_rocmfp3_mix_registry") # Its leak assertion reads GLOBAL free VRAM (cudaMemGetInfo) before and # after 4000 register/unregister cycles, so any other test allocating on @@ -1679,7 +1728,9 @@ if(DFLASH27B_TESTS) endif() endforeach() unset(_unit_ctest_name) + unset(_cppunit_target_index) endif() + unset(_cppunit_test_targets) # 'make check' — build every registered unit-test target, including the # lightweight feature gate added on main, then run the complete CTest set. diff --git a/server/cmake/DiscoverCppUnitTests.cmake b/server/cmake/DiscoverCppUnitTests.cmake index 06acdde70..081684e7a 100644 --- a/server/cmake/DiscoverCppUnitTests.cmake +++ b/server/cmake/DiscoverCppUnitTests.cmake @@ -62,7 +62,7 @@ if(_discover_output) file(APPEND "${CTEST_FILE}" "add_test([==[${_ctest_name}]==] [==[${TEST_EXECUTABLE}]==] --exact [==[${_test_keyword}]==])\n" - "set_tests_properties([==[${_ctest_name}]==] PROPERTIES WORKING_DIRECTORY [==[${TEST_WORKING_DIR}]==])\n") + "set_tests_properties([==[${_ctest_name}]==] PROPERTIES WORKING_DIRECTORY [==[${TEST_WORKING_DIR}]==] SKIP_RETURN_CODE 77)\n") list(APPEND _discovered_ctest_names "${_ctest_name}") endforeach() endif() diff --git a/server/test/CppUnitTestFramework.hpp b/server/test/CppUnitTestFramework.hpp index efd1dce6a..28f8f2a18 100644 --- a/server/test/CppUnitTestFramework.hpp +++ b/server/test/CppUnitTestFramework.hpp @@ -70,6 +70,21 @@ namespace CppUnitTestFramework { std::string m_message; }; + struct TestSkippedException : + std::exception + { + TestSkippedException(std::string message) + : m_message(std::move(message)) + {} + + const char* what() const noexcept override { + return m_message.data(); + } + + private: + std::string m_message; + }; + //-------------------------------------------------------------------------------------------------------- //-------------------------------------------------------------------------------------------------------- //-------------------------------------------------------------------------------------------------------- @@ -324,7 +339,7 @@ namespace CppUnitTestFramework { GetTestVector().push_back(std::move(details)); } - static bool Run(const RunOptions* options, const ILoggerPtr& logger) { + static int Run(const RunOptions* options, const ILoggerPtr& logger) { const auto& all_test_cases = GetTestVector(); if (options->DiscoveryMode) { @@ -339,7 +354,7 @@ namespace CppUnitTestFramework { std::cout << std::endl; } - return true; + return 0; } logger->BeginRun(all_test_cases.size()); @@ -358,8 +373,11 @@ namespace CppUnitTestFramework { logger->EnterTest(test_case.Name); bool test_failed = true; + bool test_skipped = false; try { test_failed = test_case.Callback(logger); + } catch (const TestSkippedException&) { + test_skipped = true; } catch (const AssertException&) { // REQUIRE* statement failed. No need to do anything else. } catch (const std::exception& e) { @@ -368,18 +386,27 @@ namespace CppUnitTestFramework { logger->UnhandledException(""); } - logger->ExitTest(test_failed); + if (test_skipped) { + logger->SkipTest(test_case.Name); + skip_count++; + } else { + logger->ExitTest(test_failed); + } - if (test_failed) { + if (!test_skipped && test_failed) { fail_count++; - } else { + } else if (!test_skipped) { pass_count++; } } logger->EndRun(pass_count, fail_count, skip_count); - return (fail_count == 0); + if (fail_count != 0) { + return 1; + } + + return pass_count == 0 && skip_count != 0 ? 77 : 0; } private: @@ -862,6 +889,8 @@ void TestCase_##TestName::Run() #define CHECK_CLOSE_FRACTION(Left, Right, Fraction) \ CppUnitTestFramework::CommonFixture::HandleAssert(CppUnitTestFramework::AssertType::Continue, _CPPUTF_ASSERT_LOCATION, CppUnitTestFramework::Assert::CloseFraction((Left), (Right), (Fraction))) +#define SKIP(Message) throw CppUnitTestFramework::TestSkippedException((Message)) + //------------------------------------------------------------------------------------------------------------ #define UNUSED_RETURN(Expression) [[maybe_unused]] auto _CPPUTF_NEXT_MAYBEUNUSED_NAME = Expression @@ -875,11 +904,11 @@ int main(int argc, const char* argv[]) { return 2; } - bool success = CppUnitTestFramework::TestRegistry::Run( + const int result = CppUnitTestFramework::TestRegistry::Run( &options, CppUnitTestFramework::ConsoleLogger::Create(&options) ); - return success ? 0 : 1; + return result; } #endif diff --git a/server/test/model_test_paths.h b/server/test/model_test_paths.h index e6cc630d8..3f2181540 100644 --- a/server/test/model_test_paths.h +++ b/server/test/model_test_paths.h @@ -1,8 +1,9 @@ #pragma once +#include "CppUnitTestFramework.hpp" + #include #include -#include #include #include @@ -22,7 +23,7 @@ inline std::filesystem::path require_path( const std::string name(environment_variable); const char * const value = std::getenv(name.c_str()); if (!value || !*value) { - throw std::runtime_error( + throw CppUnitTestFramework::TestSkippedException( "required test asset is not configured: set " + name ); } @@ -33,7 +34,7 @@ inline std::filesystem::path require_path( ? std::filesystem::is_directory(path, error) : std::filesystem::is_regular_file(path, error); if (!valid || error) { - throw std::runtime_error( + throw CppUnitTestFramework::TestSkippedException( "configured test asset is unavailable for " + name + ": " + path.string() ); } diff --git a/server/test/test_cuda_comm_api.cpp b/server/test/test_cuda_comm_api.cpp index 2c7e704c1..9417006f7 100644 --- a/server/test/test_cuda_comm_api.cpp +++ b/server/test/test_cuda_comm_api.cpp @@ -1,6 +1,8 @@ #include "ggml-backend.h" #include "ggml-cuda.h" #include "ggml.h" +#include "CppUnitTestFramework.hpp" +using CppUnitTestFramework::CommonFixture; #include #include @@ -100,16 +102,21 @@ static bool run_allreduce_test(int first, int second) { return ok; } -int main() { +namespace { +struct CudaCommApiFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(CudaCommApiFixture, cuda_communicator_api) { if (ggml_backend_cuda_get_device_count() == 0) { - std::puts("CUDA device unavailable; skipping communicator API test"); - return 0; + SKIP("CUDA device unavailable"); } ggml_backend_t backend = ggml_backend_cuda_init(0); if (backend == nullptr) { std::fputs("failed to initialize CUDA backend 0\n", stderr); - return 1; + REQUIRE_TRUE(false); } // Calling the public wrapper keeps its declaration and exported symbol @@ -124,16 +131,20 @@ int main() { if (result) { std::fputs("duplicate CUDA devices unexpectedly initialized a communicator\n", stderr); - return 1; + REQUIRE_TRUE(false); } std::puts("CUDA communicator API compatibility test passed"); +} + +TEST_CASE(CudaCommApiFixture, selected_device_nccl_allreduce) { + if (ggml_backend_cuda_get_device_count() == 0) { + SKIP("CUDA device unavailable"); + } const char * selected = std::getenv("DFLASH_TP_TEST_DEVICES"); if (!selected || !selected[0]) { - std::puts( - "DFLASH_TP_TEST_DEVICES is unset; skipping two-GPU NCCL all-reduce"); - return 0; + SKIP("DFLASH_TP_TEST_DEVICES is unset"); } int first = -1; @@ -141,11 +152,10 @@ int main() { if (!parse_test_devices(selected, first, second)) { std::fprintf(stderr, "bad DFLASH_TP_TEST_DEVICES=%s (expected e.g. 1,2)\n", selected); - return 1; + REQUIRE_TRUE(false); } - if (!run_allreduce_test(first, second)) return 1; + REQUIRE_TRUE(run_allreduce_test(first, second)); std::printf("selected-device NCCL all-reduce passed on CUDA%d,CUDA%d\n", first, second); - return 0; } diff --git a/server/test/test_deepseek4_hc_cuda.cpp b/server/test/test_deepseek4_hc_cuda.cpp index d0247b025..e36d60ac5 100644 --- a/server/test/test_deepseek4_hc_cuda.cpp +++ b/server/test/test_deepseek4_hc_cuda.cpp @@ -27,6 +27,8 @@ // Written in CUDA spellings; the hip_compat/ shim maps them onto HIP exactly as // the kernel and the rms_norm_hip / flashprefill tests do. +#include "CppUnitTestFramework.hpp" + #include #include #include @@ -37,18 +39,30 @@ #include "deepseek4/deepseek4_hc_cuda.h" using dflash::common::deepseek4_cuda_hc_pre; +using namespace CppUnitTestFramework; #define CK(call) do { \ cudaError_t e = (call); \ if (e != cudaSuccess) { \ std::fprintf(stderr, "HIP error %s at %s:%d: %s\n", #call, __FILE__, __LINE__, cudaGetErrorString(e)); \ - return 1; \ + REQUIRE(e == cudaSuccess); \ } \ } while (0) static inline float sigmoidf(float x) { return 1.0f / (1.0f + std::exp(-x)); } -int main() { +namespace { +struct Deepseek4HcCudaFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(Deepseek4HcCudaFixture, hc_pre_cpu_parity) { + int device_count = 0; + if (cudaGetDeviceCount(&device_count) != cudaSuccess || device_count == 0) { + SKIP("CUDA/HIP device unavailable"); + } + // DS4-Flash HC config: n_hc = 4 (mix_dim = 2*4 + 4*4 = 24). NOTE: the kernel's // d_mix is fixed at kMixDim=24 while deepseek4_cuda_hc_pre still admits n_hc<=8 // (mix_dim up to 80), so n_hc>4 overflows d_mix in the kernel today -- a latent @@ -97,7 +111,7 @@ int main() { working.data(), post.data(), comb.data())) { std::fprintf(stderr, "[ds4-hc-test] FAIL: deepseek4_cuda_hc_pre returned false\n"); cudaFree(d_fn); - return 1; + REQUIRE(false); } cudaFree(d_fn); @@ -194,8 +208,7 @@ int main() { const float TOL = 1e-4f; if (max_abs > TOL) { std::fprintf(stderr, "[ds4-hc-test] FAIL: max_abs_diff %.3e exceeds tol %.3e\n", max_abs, TOL); - return 1; } std::printf("[ds4-hc-test] PASS\n"); - return 0; + REQUIRE(max_abs <= TOL); } diff --git a/server/test/test_ds4_dmix_entry_validation.cpp b/server/test/test_ds4_dmix_entry_validation.cpp index 545de524d..5fe03c0b5 100644 --- a/server/test/test_ds4_dmix_entry_validation.cpp +++ b/server/test/test_ds4_dmix_entry_validation.cpp @@ -14,6 +14,8 @@ // test share one definition -- the parser reads from a FILE*, and covering each case through // it would need a hand-forged sidecar on disk per case. +#include "CppUnitTestFramework.hpp" + #include #include #include @@ -26,13 +28,13 @@ extern "C" const char * ds4_dmix_entry_reject_reason( uint32_t C, uint32_t K, uint8_t mode, uint32_t n_layers, bool already_covered); -static int g_fails = 0; -#define CHECK(cond, msg) \ - do { \ - if (!(cond)) { std::fprintf(stderr, "FAIL: %s\n", (msg)); ++g_fails; } \ - } while (0) +using namespace CppUnitTestFramework; namespace { +struct Ds4DmixEntryValidationFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; + constexpr uint32_t Q105 = 105, Q106 = 106; constexpr uint32_t N_LAYERS = 43; @@ -42,40 +44,27 @@ const char * ok106(uint8_t mode = 1, bool dup = false) { } } // namespace -int main() { +TEST_CASE(Ds4DmixEntryValidationFixture, rejects_malformed_dmix_entries) { // Baseline: a valid entry is accepted, so the rejections below are attributable. - CHECK(ok106() == nullptr, "well-formed qtype-106 entry is accepted"); - CHECK(ds4_dmix_entry_reject_reason(42, 4, Q105, 4096, 2, 8, 0, N_LAYERS, false) == nullptr, - "well-formed qtype-105 entry at the range edges is accepted"); + CHECK(ok106() == nullptr); + CHECK(ds4_dmix_entry_reject_reason(42, 4, Q105, 4096, 2, 8, 0, N_LAYERS, false) == nullptr); // THE TWO REGRESSIONS. - CHECK(ok106(/*mode=*/2) != nullptr, "mode=2 is REJECTED"); - CHECK(ok106(/*mode=*/255) != nullptr, "mode=255 is REJECTED"); - CHECK(ok106(/*mode=*/1, /*dup=*/true) != nullptr, - "duplicate (layer, class) is REJECTED"); + CHECK(ok106(/*mode=*/2) != nullptr); + CHECK(ok106(/*mode=*/255) != nullptr); + CHECK(ok106(/*mode=*/1, /*dup=*/true) != nullptr); // Both valid modes remain accepted -- the bound must not be off by one. - CHECK(ok106(/*mode=*/0) == nullptr, "mode=0 (fixed) still accepted"); - CHECK(ok106(/*mode=*/1) == nullptr, "mode=1 (adaptive) still accepted"); + CHECK(ok106(/*mode=*/0) == nullptr); + CHECK(ok106(/*mode=*/1) == nullptr); // Pre-existing rules, kept covered so the extraction did not drop any of them. - CHECK(ds4_dmix_entry_reject_reason(N_LAYERS, 0, Q106, 16, 2, 4, 1, N_LAYERS, false) != nullptr, - "layer >= n_layers is REJECTED"); - CHECK(ds4_dmix_entry_reject_reason(0, 5, Q106, 16, 2, 4, 1, N_LAYERS, false) != nullptr, - "class >= DS4_DMIX_CLASSES is REJECTED"); - CHECK(ds4_dmix_entry_reject_reason(0, 0, 107, 16, 2, 4, 1, N_LAYERS, false) != nullptr, - "a non-mix qtype is REJECTED"); - CHECK(ds4_dmix_entry_reject_reason(0, 0, Q105, 16, 2, 4, 1, N_LAYERS, false) != nullptr, - "qtype-105 with K=4 is REJECTED (105 wants K=8)"); - CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 16, 2, 8, 1, N_LAYERS, false) != nullptr, - "qtype-106 with K=8 is REJECTED (106 wants K=4)"); - CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 16, 3, 4, 1, N_LAYERS, false) != nullptr, - "C != 2 is REJECTED"); - CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 0, 2, 4, 1, N_LAYERS, false) != nullptr, - "nslices == 0 is REJECTED"); - CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 4097, 2, 4, 1, N_LAYERS, false) != nullptr, - "nslices > 4096 is REJECTED"); + CHECK(ds4_dmix_entry_reject_reason(N_LAYERS, 0, Q106, 16, 2, 4, 1, N_LAYERS, false) != nullptr); + CHECK(ds4_dmix_entry_reject_reason(0, 5, Q106, 16, 2, 4, 1, N_LAYERS, false) != nullptr); + CHECK(ds4_dmix_entry_reject_reason(0, 0, 107, 16, 2, 4, 1, N_LAYERS, false) != nullptr); + CHECK(ds4_dmix_entry_reject_reason(0, 0, Q105, 16, 2, 4, 1, N_LAYERS, false) != nullptr); + CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 16, 2, 8, 1, N_LAYERS, false) != nullptr); + CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 16, 3, 4, 1, N_LAYERS, false) != nullptr); + CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 0, 2, 4, 1, N_LAYERS, false) != nullptr); + CHECK(ds4_dmix_entry_reject_reason(0, 0, Q106, 4097, 2, 4, 1, N_LAYERS, false) != nullptr); - std::fprintf(stderr, g_fails ? "DMIX ENTRY VALIDATION TEST FAILED (%d)\n" - : "DMIX ENTRY VALIDATION TEST OK\n", g_fails); - return g_fails ? 1 : 0; } diff --git a/server/test/test_ds4_mix_registry_teardown.cpp b/server/test/test_ds4_mix_registry_teardown.cpp index 07d9bfa2f..ed866925b 100644 --- a/server/test/test_ds4_mix_registry_teardown.cpp +++ b/server/test/test_ds4_mix_registry_teardown.cpp @@ -22,6 +22,7 @@ // The tensor `data` pointers are opaque keys: the registry only does pointer-range arithmetic // on them and never dereferences, exactly as test_rocmfp3_mix_registry relies on. +#include "CppUnitTestFramework.hpp" #include "deepseek4_internal.h" #include "common/moe_hybrid_storage.h" #include "ggml-cuda.h" @@ -30,6 +31,8 @@ #include #include +using namespace CppUnitTestFramework; + using dflash::common::DeepSeek4Layer; using dflash::common::DeepSeek4Weights; using dflash::common::MoeHybridStorage; @@ -38,16 +41,14 @@ using dflash::common::free_deepseek4_weights; bool ggml_cuda_rocmfp3_mix_registered(const void * vx); bool ggml_cuda_rocmfp2_mix_registered(const void * vx); -static int g_fails = 0; -#define CHECK(cond, msg) \ - do { \ - if (!(cond)) { std::fprintf(stderr, "FAIL: %s\n", (msg)); ++g_fails; } \ - } while (0) - namespace { // Distinct, aligned, non-overlapping stand-ins for device bases. Spaced well beyond the // registered span (nb02 * n_experts) so no two ranges can be confused for one another. +struct Ds4MixRegistryTeardownFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; + const void * fake_base(int i) { return (const void *) (uintptr_t) (0x100000000ull + (uintptr_t) i * 0x1000000ull); } @@ -75,15 +76,14 @@ bool register_as(bool is105, const void * base) { } // namespace -int main() { +TEST_CASE(Ds4MixRegistryTeardownFixture, unregisters_all_mix_tensor_classes) { // Tensors live in a no_alloc context: only type and data are read by the teardown, and // free_deepseek4_weights owns the context afterwards. struct ggml_init_params ip = { /*mem_size=*/ 32u * 1024u * 1024u, /*mem_buffer=*/ nullptr, /*no_alloc=*/ true }; ggml_context * ctx = ggml_init(ip); if (!ctx) { - std::fprintf(stderr, "SKIP: ggml_init failed\n"); - return 0; + SKIP("ggml_init failed"); } DeepSeek4Weights w; @@ -98,9 +98,13 @@ int main() { // One layer carrying BOTH populations: five dense attention classes and the three expert // tensors. The dense half is what regressed; the expert half must keep working. - DeepSeek4Layer L{}; + DeepSeek4Layer & L = w.layers.emplace_back(); int slot = 0; std::vector dense105, dense106, expert105, expert106; + auto skip_after_cleanup = [&](const char * reason) { + free_deepseek4_weights(w); + SKIP(reason); + }; // Dense: mix 105 and 106 across classes deliberately -- the sidecar records a qtype per // entry precisely so an artifact may do this, and the teardown must dispatch on the @@ -113,10 +117,10 @@ int main() { (L.attn_output_b = mk(GGML_TYPE_Q3_1_ROCMFP3_MIX)), }; for (ggml_tensor * t : dense_t) { - if (!t) { std::fprintf(stderr, "SKIP: tensor alloc failed\n"); return 0; } + if (!t) skip_after_cleanup("dense tensor allocation failed"); t->data = (void *) fake_base(slot++); const bool is105 = (t->type == GGML_TYPE_Q3_1_ROCMFP3_MIX); - CHECK(register_as(is105, t->data), "dense registration succeeds"); + CHECK(register_as(is105, t->data)); (is105 ? dense105 : dense106).push_back(t->data); } @@ -126,18 +130,15 @@ int main() { L.ffn_up_exps = mk(GGML_TYPE_Q2_1_ROCMFP2_MIX); ggml_tensor * const exp_t[3] = { L.ffn_down_exps, L.ffn_gate_exps, L.ffn_up_exps }; for (ggml_tensor * t : exp_t) { - if (!t) { std::fprintf(stderr, "SKIP: tensor alloc failed\n"); return 0; } + if (!t) skip_after_cleanup("expert tensor allocation failed"); t->data = (void *) fake_base(slot++); const bool is105 = (t->type == GGML_TYPE_Q3_1_ROCMFP3_MIX); - CHECK(register_as(is105, t->data), "expert registration succeeds"); + CHECK(register_as(is105, t->data)); (is105 ? expert105 : expert106).push_back(t->data); } - w.layers.push_back(L); - // The fixture must actually contain dense entries, or everything below is vacuous. This // is the assertion the review asked for, and it is why the fixture is synthetic. - CHECK(!dense105.empty() && !dense106.empty(), - "fixture contains dense mix tensors of BOTH qtypes (else the test proves nothing)"); + CHECK(!dense105.empty() && !dense106.empty()); size_t live = 0; for (const void * b : dense105) live += ggml_cuda_rocmfp3_mix_registered(b) ? 1 : 0; @@ -146,11 +147,11 @@ int main() { for (const void * b : expert106) live += ggml_cuda_rocmfp2_mix_registered(b) ? 1 : 0; const size_t total = dense105.size() + dense106.size() + expert105.size() + expert106.size(); - CHECK(live == total, "every registration resolves before teardown"); + CHECK(live == total); free_deepseek4_weights(w); - CHECK(w.ctx == nullptr, "context released by teardown"); - CHECK(w.layers.empty(), "layers cleared by teardown"); + CHECK(w.ctx == nullptr); + CHECK(w.layers.empty()); // THE REGRESSION: dense entries must be gone, not just the expert ones. size_t stale_dense = 0, stale_expert = 0; @@ -161,8 +162,8 @@ int main() { std::fprintf(stderr, "note: after teardown %zu/%zu dense and %zu/%zu expert entries " "still resolve\n", stale_dense, dense105.size() + dense106.size(), stale_expert, expert105.size() + expert106.size()); - CHECK(stale_dense == 0, "NO dense attention mix entry survives free_deepseek4_weights"); - CHECK(stale_expert == 0, "no expert mix entry survives free_deepseek4_weights"); + CHECK(stale_dense == 0); + CHECK(stale_expert == 0); // Hybrid loading owns separate compact tensors on the primary and // secondary GPUs. MoeHybridStorage must unregister all of them before it @@ -171,8 +172,7 @@ int main() { { ggml_context * hybrid_ctx = ggml_init(ip); if (!hybrid_ctx) { - std::fprintf(stderr, "SKIP: hybrid ggml_init failed\n"); - return g_fails ? 1 : 0; + SKIP("hybrid ggml_init failed"); } MoeHybridStorage hybrid; @@ -193,14 +193,12 @@ int main() { }; for (ggml_tensor * tensor : compact) { if (!tensor) { - std::fprintf(stderr, "SKIP: hybrid tensor alloc failed\n"); - return g_fails ? 1 : 0; + SKIP("hybrid tensor allocation failed"); } tensor->data = (void *) fake_base(slot++); const bool is105 = tensor->type == GGML_TYPE_Q3_1_ROCMFP3_MIX; - CHECK(register_as(is105, tensor->data), - "hybrid registration succeeds"); + CHECK(register_as(is105, tensor->data)); (is105 ? hybrid105 : hybrid106).push_back(tensor->data); } @@ -211,8 +209,7 @@ int main() { for (const void * base : hybrid106) { live_hybrid += ggml_cuda_rocmfp2_mix_registered(base) ? 1 : 0; } - CHECK(live_hybrid == hybrid105.size() + hybrid106.size(), - "every compact owner registration resolves before teardown"); + CHECK(live_hybrid == hybrid105.size() + hybrid106.size()); } size_t stale_hybrid = 0; @@ -222,10 +219,5 @@ int main() { for (const void * base : hybrid106) { stale_hybrid += ggml_cuda_rocmfp2_mix_registered(base) ? 1 : 0; } - CHECK(stale_hybrid == 0, - "no compact mix entry survives MoeHybridStorage teardown"); - - std::fprintf(stderr, g_fails ? "MIX REGISTRY TEARDOWN TEST FAILED (%d)\n" - : "MIX REGISTRY TEARDOWN TEST OK\n", g_fails); - return g_fails ? 1 : 0; + CHECK(stale_hybrid == 0); } diff --git a/server/test/test_feature_gate.cpp b/server/test/test_feature_gate.cpp index 399197359..c38d3d78e 100644 --- a/server/test/test_feature_gate.cpp +++ b/server/test/test_feature_gate.cpp @@ -10,6 +10,7 @@ // Build: cmake --build . --target test_feature_gate // Run: ./test_feature_gate +#include "CppUnitTestFramework.hpp" #include "common/feature_gate.h" #include "common/model_capabilities.h" #include "common/paged_attention_config.h" @@ -20,31 +21,17 @@ #include #include +using namespace CppUnitTestFramework; using namespace dflash::common; -static int test_failures = 0; -static int test_count = 0; - -#define TEST_ASSERT(expr) do { \ - test_count++; \ - if (!(expr)) { \ - test_failures++; \ - std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #expr); \ - } \ -} while (0) - -#define RUN_TEST(fn) do { \ - std::fprintf(stderr, " %s ...", #fn); \ - int before = test_failures; \ - fn(); \ - if (test_failures == before) std::fprintf(stderr, " ok\n"); \ - else std::fprintf(stderr, "\n"); \ -} while (0) - // ── Backend compatibility gate ────────────────────────────────────────── // One case per rule cluster in check_feature_compatibility(). All resolved // facts are parameters, so none of this needs a model file or GPU. +namespace { +struct FeatureGateFixture : CommonFixture { + using CommonFixture::CommonFixture; + static BackendArgs gate_args_hip_deepseek4() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; @@ -72,223 +59,223 @@ static std::string gate_result_for_binary( args, features, arch, target_backend, compiled_backend); } -static void test_feature_gate_accepts_plain_launch() { +void test_feature_gate_accepts_plain_launch() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(gate_result( + CHECK(gate_result( args, "qwen35", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_rejects_undetected_arch() { +void test_feature_gate_rejects_undetected_arch() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_requires_compiled_target_backend() { +void test_feature_gate_requires_compiled_target_backend() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; args.device.backend = PlacementBackend::Hip; - TEST_ASSERT(!gate_result_for_binary( + CHECK(!gate_result_for_binary( args, "qwen35", PlacementBackend::Hip, PlacementBackend::Cuda).empty()); } -static void test_feature_gate_ipc_options_require_ipc_binary() { +void test_feature_gate_ipc_options_require_ipc_binary() { BackendArgs draft; draft.model_path = "/nonexistent/model.gguf"; draft.remote_draft.work_dir = "/tmp/draft"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( draft, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs target; target.model_path = "/nonexistent/model.gguf"; target.remote_target_shard.work_dir = "/tmp/target"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( target, "qwen35", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_mixed_draft_placement_requires_ipc() { +void test_feature_gate_mixed_draft_placement_requires_ipc() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; args.draft_path = "/nonexistent/draft.gguf"; args.device.backend = PlacementBackend::Cuda; args.draft_device.backend = PlacementBackend::Hip; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "qwen35", PlacementBackend::Cuda).empty()); args.remote_draft.ipc_bin = "/usr/bin/draft-ipc"; - TEST_ASSERT(gate_result( + CHECK(gate_result( args, "qwen35", PlacementBackend::Cuda).empty()); args.draft_device.backend = PlacementBackend::Cuda; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "qwen35", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_pflash_requires_drafter_and_supported_arch() { +void test_feature_gate_pflash_requires_drafter_and_supported_arch() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; BackendFeatureConfig features; features.pflash_enabled = true; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "qwen35", PlacementBackend::Cuda, features).empty()); features.pflash_drafter_configured = true; - TEST_ASSERT(gate_result( + CHECK(gate_result( args, "gemma4", PlacementBackend::Cuda, features).empty()); args.device.backend = PlacementBackend::Cuda; args.draft_device.backend = PlacementBackend::Hip; args.remote_draft.ipc_bin = "/usr/bin/draft-ipc"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "gemma4", PlacementBackend::Cuda, features).empty()); - TEST_ASSERT(gate_result( + CHECK(gate_result( args, "qwen35", PlacementBackend::Cuda, features).empty()); } -static void test_feature_gate_validates_target_split_topology() { +void test_feature_gate_validates_target_split_topology() { BackendArgs weights; weights.model_path = "/nonexistent/model.gguf"; weights.device.layer_split_weights = {1.0, 1.0}; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( weights, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs mixed; mixed.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(parse_placement_device_list( + CHECK(parse_placement_device_list( "cuda:0,hip:0", mixed.device)); - TEST_ASSERT(!gate_result( + CHECK(!gate_result( mixed, "qwen35", PlacementBackend::Cuda).empty()); mixed.remote_target_shard.ipc_bin = "/usr/bin/target-shard"; - TEST_ASSERT(gate_result( + CHECK(gate_result( mixed, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs two_boundaries; two_boundaries.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(parse_placement_device_list( + CHECK(parse_placement_device_list( "cuda:0,hip:0,cuda:1", two_boundaries.device)); two_boundaries.remote_target_shard.ipc_bin = "/usr/bin/target-shard"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( two_boundaries, "qwen35", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_tensor_parallel_requirements() { +void test_feature_gate_tensor_parallel_requirements() { BackendArgs valid; valid.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(parse_placement_device_list( + CHECK(parse_placement_device_list( "cuda:0,cuda:1", valid.device)); valid.device.split_mode = TargetSplitMode::Tensor; - TEST_ASSERT(gate_result( + CHECK(gate_result( valid, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs missing_devices; missing_devices.model_path = "/nonexistent/model.gguf"; missing_devices.device.split_mode = TargetSplitMode::Tensor; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( missing_devices, "qwen35", PlacementBackend::Cuda).empty()); - TEST_ASSERT(!gate_result( + CHECK(!gate_result( valid, "laguna", PlacementBackend::Cuda).empty()); BackendArgs hip; hip.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(parse_placement_device_list("hip:0,hip:1", hip.device)); + CHECK(parse_placement_device_list("hip:0,hip:1", hip.device)); hip.device.split_mode = TargetSplitMode::Tensor; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( hip, "qwen35", PlacementBackend::Hip).empty()); BackendArgs mixed = valid; - TEST_ASSERT(parse_placement_device_list( + CHECK(parse_placement_device_list( "cuda:0,hip:0", mixed.device)); mixed.device.split_mode = TargetSplitMode::Tensor; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( mixed, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs weighted = valid; weighted.device.layer_split_weights = {1.0, 1.0}; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( weighted, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs remote = valid; remote.remote_target_shard.ipc_bin = "/usr/bin/target-shard"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( remote, "qwen35", PlacementBackend::Cuda).empty()); BackendFeatureConfig pflash; pflash.pflash_enabled = true; pflash.pflash_drafter_configured = true; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( valid, "qwen35", PlacementBackend::Cuda, pflash).empty()); BackendArgs draft = valid; draft.draft_path = "/nonexistent/draft.gguf"; - TEST_ASSERT(gate_result( + CHECK(gate_result( draft, "qwen35", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_ds4_prefill_requires_deepseek4() { +void test_feature_gate_ds4_prefill_requires_deepseek4() { BackendArgs args = gate_args_hip_deepseek4(); args.ds4_prefill_mode_set = true; args.ds4_prefill_mode = PrefillAttentionMode::Dense; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "qwen35", PlacementBackend::Hip).empty()); - TEST_ASSERT(gate_result( + CHECK(gate_result( args, "deepseek4", PlacementBackend::Hip).empty()); } -static void test_feature_gate_approximate_ds4_prefill_requires_local_hip() { +void test_feature_gate_approximate_ds4_prefill_requires_local_hip() { BackendArgs args = gate_args_hip_deepseek4(); args.ds4_prefill_mode_set = true; args.ds4_prefill_mode = PrefillAttentionMode::Sparse; // CUDA has no approximate prefill path. - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "deepseek4", PlacementBackend::Cuda).empty()); // Neither does the layer-split adapter, even on HIP. BackendArgs split = args; - TEST_ASSERT(parse_placement_device_list("hip:0,hip:1", split.device)); - TEST_ASSERT(!gate_result( + CHECK(parse_placement_device_list("hip:0,hip:1", split.device)); + CHECK(!gate_result( split, "deepseek4", PlacementBackend::Hip).empty()); // Nor a remote target shard. BackendArgs remote = args; remote.remote_target_shard.ipc_bin = "/usr/bin/shard"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( remote, "deepseek4", PlacementBackend::Hip).empty()); // Single local HIP device is the supported placement. - TEST_ASSERT(gate_result( + CHECK(gate_result( args, "deepseek4", PlacementBackend::Hip).empty()); // Exact prefill is unrestricted. BackendArgs exact = gate_args_hip_deepseek4(); exact.ds4_prefill_mode_set = true; exact.ds4_prefill_mode = PrefillAttentionMode::Exact; - TEST_ASSERT(gate_result( + CHECK(gate_result( exact, "deepseek4", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_ds4_decode_options_require_monolithic_hip() { +void test_feature_gate_ds4_decode_options_require_monolithic_hip() { BackendArgs fused = gate_args_hip_deepseek4(); fused.ds4_fused_decode = true; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( fused, "deepseek4", PlacementBackend::Cuda).empty()); - TEST_ASSERT(gate_result( + CHECK(gate_result( fused, "deepseek4", PlacementBackend::Hip).empty()); BackendArgs topk = gate_args_hip_deepseek4(); topk.ds4_expert_top_k = 4; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( topk, "qwen35", PlacementBackend::Hip).empty()); - TEST_ASSERT(gate_result( + CHECK(gate_result( topk, "deepseek4", PlacementBackend::Hip).empty()); // Top-k is a model policy in the monolithic backend and is independent of @@ -296,16 +283,16 @@ static void test_feature_gate_ds4_decode_options_require_monolithic_hip() { // placement can therefore use it. BackendArgs cuda_topk = topk; cuda_topk.device.backend = PlacementBackend::Cuda; - TEST_ASSERT(gate_result( + CHECK(gate_result( cuda_topk, "deepseek4", PlacementBackend::Cuda).empty()); BackendArgs split_topk = topk; split_topk.device.layer_split_gpus = {0, 1}; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( split_topk, "deepseek4", PlacementBackend::Hip).empty()); } -static void test_feature_gate_remote_draft_requires_supported_arch() { +void test_feature_gate_remote_draft_requires_supported_arch() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; args.draft_path = "/nonexistent/draft.gguf"; @@ -313,113 +300,113 @@ static void test_feature_gate_remote_draft_requires_supported_arch() { args.draft_device.backend = PlacementBackend::Hip; args.remote_draft.ipc_bin = "/usr/bin/draft-ipc"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( args, "gemma4", PlacementBackend::Cuda).empty()); - TEST_ASSERT(gate_result( + CHECK(gate_result( args, "qwen35", PlacementBackend::Cuda).empty()); // Without a draft model or PFlash, remote draft IPC is unnecessary. BackendArgs no_draft = args; no_draft.draft_path = nullptr; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( no_draft, "gemma4", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_layer_split_requires_supported_arch() { +void test_feature_gate_layer_split_requires_supported_arch() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(parse_placement_device_list("cuda:0,cuda:1", args.device)); + CHECK(parse_placement_device_list("cuda:0,cuda:1", args.device)); // These four have a layer-split adapter. for (const char * arch : {"qwen35", "laguna", "gemma4", "deepseek4"}) { - TEST_ASSERT(gate_result(args, arch, PlacementBackend::Cuda).empty()); + CHECK(gate_result(args, arch, PlacementBackend::Cuda).empty()); } // These two do not: the factory would hand the split placement to a // monolithic backend, which reads only the primary GPU. for (const char * arch : {"qwen35moe", "qwen3"}) { - TEST_ASSERT(!gate_result(args, arch, PlacementBackend::Cuda).empty()); + CHECK(!gate_result(args, arch, PlacementBackend::Cuda).empty()); } // Single-device placement is unaffected for the same architectures. BackendArgs single; single.model_path = "/nonexistent/model.gguf"; - TEST_ASSERT(gate_result(single, "qwen35moe", PlacementBackend::Cuda).empty()); - TEST_ASSERT(gate_result(single, "qwen3", PlacementBackend::Cuda).empty()); + CHECK(gate_result(single, "qwen35moe", PlacementBackend::Cuda).empty()); + CHECK(gate_result(single, "qwen3", PlacementBackend::Cuda).empty()); } -static void test_feature_gate_paged_attention_requires_qwen35_monolithic() { +void test_feature_gate_paged_attention_requires_qwen35_monolithic() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; args.paged_attention = true; - TEST_ASSERT(gate_result(args, "qwen35", PlacementBackend::Cuda).empty()); - TEST_ASSERT(gate_result(args, "qwen35", PlacementBackend::Hip).empty()); + CHECK(gate_result(args, "qwen35", PlacementBackend::Cuda).empty()); + CHECK(gate_result(args, "qwen35", PlacementBackend::Hip).empty()); // Only qwen35 has a paged decode path. qwen35moe shares Qwen35Config, so // its rejection is this gate's job — the factory's field-presence // cross-check cannot tell the two apart. for (const char * arch : {"qwen35moe", "laguna", "qwen3", "gemma4", "deepseek4"}) { - TEST_ASSERT(!gate_result(args, arch, PlacementBackend::Cuda).empty()); + CHECK(!gate_result(args, arch, PlacementBackend::Cuda).empty()); } // Only the monolithic qwen35 backend owns a paged K/V pool. Both // placements are supported qwen35 launches without the flag, so the // rejection has to come from the paged rule. BackendArgs split = args; - TEST_ASSERT(parse_placement_device_list("cuda:0,cuda:1", split.device)); - TEST_ASSERT(!gate_result(split, "qwen35", PlacementBackend::Cuda).empty()); + CHECK(parse_placement_device_list("cuda:0,cuda:1", split.device)); + CHECK(!gate_result(split, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs remote_shard = args; remote_shard.remote_target_shard.ipc_bin = "/usr/bin/target-shard"; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( remote_shard, "qwen35", PlacementBackend::Cuda).empty()); for (BackendArgs * relaxed : {&split, &remote_shard}) { relaxed->paged_attention = false; - TEST_ASSERT(gate_result( + CHECK(gate_result( *relaxed, "qwen35", PlacementBackend::Cuda).empty()); } } -static void test_feature_gate_paged_attention_requires_plain_ar_decode() { +void test_feature_gate_paged_attention_requires_plain_ar_decode() { BackendArgs base; base.model_path = "/nonexistent/model.gguf"; base.paged_attention = true; BackendArgs draft = base; draft.draft_path = "/nonexistent/draft.gguf"; - TEST_ASSERT(!gate_result(draft, "qwen35", PlacementBackend::Cuda).empty()); + CHECK(!gate_result(draft, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs ddtree = base; ddtree.ddtree_mode = true; - TEST_ASSERT(!gate_result(ddtree, "qwen35", PlacementBackend::Cuda).empty()); + CHECK(!gate_result(ddtree, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs windowed = base; windowed.fa_window = 4096; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( windowed, "qwen35", PlacementBackend::Cuda).empty()); BackendFeatureConfig pflash; pflash.pflash_enabled = true; pflash.pflash_drafter_configured = true; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( base, "qwen35", PlacementBackend::Cuda, pflash).empty()); // The pool rounds max_ctx up to whole blocks, so both ends of the range // are rejected: nothing to allocate, and rounding that overflows int. BackendArgs empty_ctx = base; empty_ctx.device.max_ctx = 0; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( empty_ctx, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs huge_ctx = base; huge_ctx.device.max_ctx = INT_MAX; - TEST_ASSERT(!gate_result( + CHECK(!gate_result( huge_ctx, "qwen35", PlacementBackend::Cuda).empty()); BackendArgs max_ctx = base; max_ctx.device.max_ctx = INT_MAX - PAGED_BLOCK_SIZE + 1; - TEST_ASSERT(gate_result( + CHECK(gate_result( max_ctx, "qwen35", PlacementBackend::Cuda).empty()); // None of these are rules about paged attention itself: without the flag @@ -427,7 +414,7 @@ static void test_feature_gate_paged_attention_requires_plain_ar_decode() { for (BackendArgs * args : {&draft, &ddtree, &windowed, &empty_ctx, &huge_ctx}) { args->paged_attention = false; - TEST_ASSERT(gate_result(*args, "qwen35", PlacementBackend::Cuda).empty()); + CHECK(gate_result(*args, "qwen35", PlacementBackend::Cuda).empty()); } } @@ -435,11 +422,11 @@ static void test_feature_gate_paged_attention_requires_plain_ar_decode() { // Warnings must never gate admission, so each case also asserts the same // configuration passes check_feature_compatibility(). -static std::vector warn_result( +std::vector warn_result( const BackendArgs & args, const std::string & arch, const BackendFeatureConfig & features = {}) { - TEST_ASSERT(check_feature_compatibility( + CHECK(check_feature_compatibility( args, features, arch, compiled_placement_backend(), compiled_placement_backend()).empty()); return collect_feature_warnings(args, features, arch); @@ -453,7 +440,7 @@ static bool warns_about(const std::vector & warnings, return false; } -static void test_feature_warnings_silent_when_supported() { +void test_feature_warnings_silent_when_supported() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; args.draft_path = "/nonexistent/draft.gguf"; @@ -461,56 +448,56 @@ static void test_feature_warnings_silent_when_supported() { args.fa_window = 512; args.draft_swa_window = 2048; // qwen35 forwards every one of these. - TEST_ASSERT(warn_result(args, "qwen35").empty()); + CHECK(warn_result(args, "qwen35").empty()); } -static void test_feature_warnings_report_inert_draft() { +void test_feature_warnings_report_inert_draft() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; args.draft_path = "/nonexistent/draft.gguf"; // qwen3 and deepseek4 never forward a draft model. - TEST_ASSERT(warns_about(warn_result(args, "qwen3"), "--draft")); - TEST_ASSERT(warns_about(warn_result(args, "deepseek4"), "--draft")); + CHECK(warns_about(warn_result(args, "qwen3"), "--draft")); + CHECK(warns_about(warn_result(args, "deepseek4"), "--draft")); // laguna and gemma4 forward it only when monolithic. - TEST_ASSERT(!warns_about(warn_result(args, "laguna"), "--draft")); - TEST_ASSERT(!warns_about(warn_result(args, "gemma4"), "--draft")); + CHECK(!warns_about(warn_result(args, "laguna"), "--draft")); + CHECK(!warns_about(warn_result(args, "gemma4"), "--draft")); BackendArgs split = args; - TEST_ASSERT(parse_placement_device_list("cuda:0,cuda:1", split.device)); + CHECK(parse_placement_device_list("cuda:0,cuda:1", split.device)); const std::vector w = collect_feature_warnings(split, {}, "laguna"); - TEST_ASSERT(warns_about(w, "--draft")); - TEST_ASSERT(w[0].find("single-device placement") != std::string::npos); + CHECK(warns_about(w, "--draft")); + CHECK(w[0].find("single-device placement") != std::string::npos); } -static void test_feature_warnings_report_inert_decode_tunables() { +void test_feature_warnings_report_inert_decode_tunables() { BackendArgs ddtree; ddtree.model_path = "/nonexistent/model.gguf"; ddtree.ddtree_mode = true; - TEST_ASSERT(warns_about(warn_result(ddtree, "gemma4"), "--ddtree")); - TEST_ASSERT(!warns_about(warn_result(ddtree, "laguna"), "--ddtree")); + CHECK(warns_about(warn_result(ddtree, "gemma4"), "--ddtree")); + CHECK(!warns_about(warn_result(ddtree, "laguna"), "--ddtree")); BackendArgs vw; vw.model_path = "/nonexistent/model.gguf"; vw.verify_width = 8; - TEST_ASSERT(!warns_about(warn_result(vw, "laguna"), "--verify-width")); - TEST_ASSERT(warns_about(warn_result(vw, "qwen35"), "--verify-width")); + CHECK(!warns_about(warn_result(vw, "laguna"), "--verify-width")); + CHECK(warns_about(warn_result(vw, "qwen35"), "--verify-width")); BackendArgs fa; fa.model_path = "/nonexistent/model.gguf"; fa.fa_window = 4096; // gemma4 honors --fa-window on both paths; laguna has no such option. - TEST_ASSERT(!warns_about(warn_result(fa, "gemma4"), "--fa-window")); - TEST_ASSERT(warns_about(warn_result(fa, "laguna"), "--fa-window")); + CHECK(!warns_about(warn_result(fa, "gemma4"), "--fa-window")); + CHECK(warns_about(warn_result(fa, "laguna"), "--fa-window")); BackendArgs swa; swa.model_path = "/nonexistent/model.gguf"; swa.draft_swa_window = 2048; - TEST_ASSERT(!warns_about(warn_result(swa, "qwen35moe"), "--draft-swa")); - TEST_ASSERT(warns_about(warn_result(swa, "gemma4"), "--draft-swa")); + CHECK(!warns_about(warn_result(swa, "qwen35moe"), "--draft-swa")); + CHECK(warns_about(warn_result(swa, "gemma4"), "--draft-swa")); } -static void test_feature_warnings_report_inert_moe_options() { +void test_feature_warnings_report_inert_moe_options() { BackendArgs args; args.model_path = "/nonexistent/model.gguf"; @@ -518,80 +505,74 @@ static void test_feature_warnings_report_inert_moe_options() { moe_opts.routing_stats_requested = true; moe_opts.adaptive_experts_requested = true; - TEST_ASSERT(warn_result(args, "laguna", moe_opts).empty()); - TEST_ASSERT(warn_result(args, "qwen35moe", moe_opts).empty()); - TEST_ASSERT(warn_result(args, "qwen35", moe_opts).size() == 2); - TEST_ASSERT(warn_result(args, "deepseek4", moe_opts).size() == 2); + CHECK(warn_result(args, "laguna", moe_opts).empty()); + CHECK(warn_result(args, "qwen35moe", moe_opts).empty()); + CHECK(warn_result(args, "qwen35", moe_opts).size() == 2); + CHECK(warn_result(args, "deepseek4", moe_opts).size() == 2); } -static void test_model_capability_tables() { +void test_model_capability_tables() { // Table integrity: one row per architecture, no blanks, no duplicates. for (const ArchCapabilities & row : kArchCapabilities) { - TEST_ASSERT(row.arch != nullptr && row.arch[0] != '\0'); - TEST_ASSERT(find_arch_capabilities(row.arch) == &row); + CHECK(row.arch != nullptr && row.arch[0] != '\0'); + CHECK(find_arch_capabilities(row.arch) == &row); } // arch_is_supported() must match create_backend()'s dispatch chain. for (const char * arch : {"qwen35", "qwen35moe", "laguna", "qwen3", "gemma4", "deepseek4"}) { - TEST_ASSERT(arch_is_supported(arch)); + CHECK(arch_is_supported(arch)); } - TEST_ASSERT(!arch_is_supported("")); - TEST_ASSERT(!arch_is_supported("qwen36")); // model_card has a branch; the factory does not - TEST_ASSERT(!arch_is_supported("llama")); + CHECK(!arch_is_supported("")); + CHECK(!arch_is_supported("qwen36")); // model_card has a branch; the factory does not + CHECK(!arch_is_supported("llama")); - TEST_ASSERT(arch_has_expert_offload("laguna")); - TEST_ASSERT(arch_has_expert_offload("qwen35moe")); - TEST_ASSERT(!arch_has_expert_offload("qwen35")); + CHECK(arch_has_expert_offload("laguna")); + CHECK(arch_has_expert_offload("qwen35moe")); + CHECK(!arch_has_expert_offload("qwen35")); // deepseek4 is mixture-of-experts but has no hot/cold offload path. - TEST_ASSERT(!arch_has_expert_offload("deepseek4")); + CHECK(!arch_has_expert_offload("deepseek4")); // Every capability predicate must be false for an architecture the // factory cannot build, so no rule can admit an unbuildable model. - TEST_ASSERT(!arch_supports_layer_split("qwen36")); - TEST_ASSERT(!arch_supports_remote_draft("qwen36")); - TEST_ASSERT(!arch_supports_pflash_compression("qwen36")); - TEST_ASSERT(!arch_supports_decode_draft("qwen36", false)); - TEST_ASSERT(!arch_supports_ddtree("qwen36", false)); - TEST_ASSERT(!arch_supports_verify_width("qwen36", false)); - TEST_ASSERT(!arch_supports_fa_window("qwen36", false)); - TEST_ASSERT(!arch_supports_draft_swa("qwen36", false)); - TEST_ASSERT(!arch_supports_paged_attention("qwen36", false)); + CHECK(!arch_supports_layer_split("qwen36")); + CHECK(!arch_supports_remote_draft("qwen36")); + CHECK(!arch_supports_pflash_compression("qwen36")); + CHECK(!arch_supports_decode_draft("qwen36", false)); + CHECK(!arch_supports_ddtree("qwen36", false)); + CHECK(!arch_supports_verify_width("qwen36", false)); + CHECK(!arch_supports_fa_window("qwen36", false)); + CHECK(!arch_supports_draft_swa("qwen36", false)); + CHECK(!arch_supports_paged_attention("qwen36", false)); // Paged decode lives in the monolithic qwen35 backend alone. - TEST_ASSERT(arch_supports_paged_attention("qwen35", false)); - TEST_ASSERT(!arch_supports_paged_attention("qwen35", true)); - TEST_ASSERT(!arch_supports_paged_attention("qwen35moe", false)); + CHECK(arch_supports_paged_attention("qwen35", false)); + CHECK(!arch_supports_paged_attention("qwen35", true)); + CHECK(!arch_supports_paged_attention("qwen35moe", false)); } -int main() { - std::fprintf(stderr, "\n\u2500\u2500 Backend feature/architecture gate \u2500\u2500\n"); - RUN_TEST(test_feature_gate_accepts_plain_launch); - RUN_TEST(test_feature_gate_rejects_undetected_arch); - RUN_TEST(test_feature_gate_requires_compiled_target_backend); - RUN_TEST(test_feature_gate_ipc_options_require_ipc_binary); - RUN_TEST(test_feature_gate_mixed_draft_placement_requires_ipc); - RUN_TEST(test_feature_gate_pflash_requires_drafter_and_supported_arch); - RUN_TEST(test_feature_gate_validates_target_split_topology); - RUN_TEST(test_feature_gate_tensor_parallel_requirements); - RUN_TEST(test_feature_gate_ds4_prefill_requires_deepseek4); - RUN_TEST(test_feature_gate_approximate_ds4_prefill_requires_local_hip); - RUN_TEST(test_feature_gate_ds4_decode_options_require_monolithic_hip); - RUN_TEST(test_feature_gate_remote_draft_requires_supported_arch); - RUN_TEST(test_feature_gate_layer_split_requires_supported_arch); - RUN_TEST(test_feature_gate_paged_attention_requires_qwen35_monolithic); - RUN_TEST(test_feature_gate_paged_attention_requires_plain_ar_decode); - RUN_TEST(test_feature_warnings_silent_when_supported); - RUN_TEST(test_feature_warnings_report_inert_draft); - RUN_TEST(test_feature_warnings_report_inert_decode_tunables); - RUN_TEST(test_feature_warnings_report_inert_moe_options); - RUN_TEST(test_model_capability_tables); - - std::fprintf(stderr, - "\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n" - " Results: %d assertions, %d failures\n" - "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n", - test_count, test_failures); - if (test_failures == 0) std::fprintf(stderr, "ALL PASSED\n"); - return test_failures == 0 ? 0 : 1; +}; +} // namespace + +TEST_CASE(FeatureGateFixture, feature_gate_suite) { + test_feature_gate_accepts_plain_launch(); + test_feature_gate_rejects_undetected_arch(); + test_feature_gate_requires_compiled_target_backend(); + test_feature_gate_ipc_options_require_ipc_binary(); + test_feature_gate_mixed_draft_placement_requires_ipc(); + test_feature_gate_pflash_requires_drafter_and_supported_arch(); + test_feature_gate_validates_target_split_topology(); + test_feature_gate_tensor_parallel_requirements(); + test_feature_gate_ds4_prefill_requires_deepseek4(); + test_feature_gate_approximate_ds4_prefill_requires_local_hip(); + test_feature_gate_ds4_decode_options_require_monolithic_hip(); + test_feature_gate_remote_draft_requires_supported_arch(); + test_feature_gate_layer_split_requires_supported_arch(); + test_feature_gate_paged_attention_requires_qwen35_monolithic(); + test_feature_gate_paged_attention_requires_plain_ar_decode(); + test_feature_warnings_silent_when_supported(); + test_feature_warnings_report_inert_draft(); + test_feature_warnings_report_inert_decode_tunables(); + test_feature_warnings_report_inert_moe_options(); + test_model_capability_tables(); } diff --git a/server/test/test_flashprefill_kernels.cpp b/server/test/test_flashprefill_kernels.cpp index 7b3927269..34930392f 100644 --- a/server/test/test_flashprefill_kernels.cpp +++ b/server/test/test_flashprefill_kernels.cpp @@ -1,3 +1,5 @@ +#include "CppUnitTestFramework.hpp" + // Smoke test for the FlashPrefill CUDA kernels. // // Tiny shapes (B=1, S=256, H=4, Hk=2, D=128, BLOCK=128) so we can run on @@ -21,6 +23,8 @@ #include "../src/flashprefill.h" +using namespace CppUnitTestFramework; + extern "C" { void launch_compute_mean_vector_bf16( const void * K, void * mean_K, @@ -59,14 +63,25 @@ void launch_sparse_flash_forward_bf16( cudaError_t e = (call); \ if (e != cudaSuccess) { \ std::fprintf(stderr, "CUDA error %s at %s:%d: %s\n", #call, __FILE__, __LINE__, cudaGetErrorString(e)); \ - return 1; \ + REQUIRE(e == cudaSuccess); \ } \ } while (0) static __nv_bfloat16 f2b(float x) { return __float2bfloat16(x); } static float b2f(__nv_bfloat16 x) { return __bfloat162float(x); } -int main() { +namespace { +struct FlashprefillKernelsFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(FlashprefillKernelsFixture, flashprefill_kernels) { + int device_count = 0; + if (cudaGetDeviceCount(&device_count) != cudaSuccess || device_count == 0) { + SKIP("CUDA/HIP device unavailable"); + } + constexpr int B = 1; constexpr int S = 256; constexpr int H = 4; @@ -281,5 +296,4 @@ int main() { cudaFree(bdQ); cudaFree(bdK); cudaFree(bdV); cudaFree(bdO); } - return 0; } diff --git a/server/test/test_ggml_rmsnorm_batch.cpp b/server/test/test_ggml_rmsnorm_batch.cpp index 18296d3a7..1195e8505 100644 --- a/server/test/test_ggml_rmsnorm_batch.cpp +++ b/server/test/test_ggml_rmsnorm_batch.cpp @@ -1,3 +1,5 @@ +#include "CppUnitTestFramework.hpp" + #include "ggml-alloc.h" #include "ggml-backend.h" #include "ggml-cuda.h" @@ -9,17 +11,24 @@ #include #include -int main(int argc, char ** argv) { - const int n_tokens = argc > 1 ? std::max(1, std::atoi(argv[1])) : 64; - const bool initialize_peer = argc <= 2 || std::atoi(argv[2]) != 0; - const int device = argc > 3 ? std::max(0, std::atoi(argv[3])) : 0; +using namespace CppUnitTestFramework; + +namespace { +struct GgmlRmsnormBatchFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(GgmlRmsnormBatchFixture, rmsnorm_batch) { + constexpr int n_tokens = 64; + constexpr bool initialize_peer = true; + constexpr int device = 0; constexpr int n_embd = 4096; constexpr float eps = 1.0e-6f; ggml_backend_t backend = ggml_backend_cuda_init(device); if (!backend) { - std::fprintf(stderr, "failed to initialize GPU %d\n", device); - return 2; + SKIP("CUDA/HIP backend unavailable"); } // Leave a second HIP device initialized, matching the heterogeneous @@ -33,7 +42,11 @@ int main(int argc, char ** argv) { params.mem_size = 2 * 1024 * 1024; params.no_alloc = true; ggml_context * ctx = ggml_init(params); - if (!ctx) return 3; + if (!ctx) { + if (peer) ggml_backend_free(peer); + ggml_backend_free(backend); + REQUIRE(ctx != nullptr); + } ggml_tensor * input = ggml_new_tensor_2d( ctx, GGML_TYPE_F32, n_embd, n_tokens); @@ -50,8 +63,12 @@ int main(int argc, char ** argv) { ggml_gallocr_t alloc = ggml_gallocr_new( ggml_backend_get_default_buffer_type(backend)); if (!alloc || !ggml_gallocr_alloc_graph(alloc, graph)) { - std::fprintf(stderr, "graph allocation failed\n"); - return 4; + if (alloc) ggml_gallocr_free(alloc); + ggml_free(ctx); + if (peer) ggml_backend_free(peer); + ggml_backend_free(backend); + REQUIRE(alloc != nullptr); + REQUIRE(false); } std::vector input_data((size_t)n_embd * n_tokens); @@ -70,16 +87,19 @@ int main(int argc, char ** argv) { const enum ggml_status status = ggml_backend_graph_compute(backend, graph); if (status != GGML_STATUS_SUCCESS) { std::fprintf(stderr, "graph compute failed: %d\n", (int)status); - return 5; } std::vector output_data(input_data.size()); - ggml_backend_tensor_get(output, output_data.data(), 0, - output_data.size() * sizeof(float)); + if (status == GGML_STATUS_SUCCESS) { + ggml_backend_tensor_get(output, output_data.data(), 0, + output_data.size() * sizeof(float)); + } + bool finite = status == GGML_STATUS_SUCCESS; for (float value : output_data) { if (!std::isfinite(value)) { std::fprintf(stderr, "non-finite output\n"); - return 6; + finite = false; + break; } } std::printf("PASS device=%d peer=%s tokens=%d embd=%d\n", @@ -89,5 +109,5 @@ int main(int argc, char ** argv) { ggml_free(ctx); if (peer) ggml_backend_free(peer); ggml_backend_free(backend); - return 0; + REQUIRE(finite); } diff --git a/server/test/test_mmq_streamk_iq4_xs.cpp b/server/test/test_mmq_streamk_iq4_xs.cpp index 13239e836..fd163dd1c 100644 --- a/server/test/test_mmq_streamk_iq4_xs.cpp +++ b/server/test/test_mmq_streamk_iq4_xs.cpp @@ -19,6 +19,8 @@ #include "ggml-backend.h" #include "ggml-cpu.h" #include "ggml-cuda.h" +#include "CppUnitTestFramework.hpp" +using CppUnitTestFramework::CommonFixture; #include #include @@ -198,11 +200,16 @@ bool run_case(ggml_backend_t cpu, ggml_backend_t cuda, const Shape & shape, uint } // namespace -int main() { +namespace { +struct MmqStreamkIq4XsFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(MmqStreamkIq4XsFixture, iq4_xs_streamk_correctness) { const int device_count = ggml_backend_cuda_get_device_count(); if (device_count == 0) { - std::printf("SKIP: no CUDA device available\n"); - return 0; + SKIP("no CUDA device available"); } char description[256] = {}; @@ -220,7 +227,7 @@ int main() { if (cuda != nullptr) { ggml_backend_free(cuda); } - return 1; + REQUIRE_TRUE(false); } ggml_backend_cpu_set_n_threads(cpu, 4); @@ -246,10 +253,9 @@ int main() { if (failures != 0) { std::fprintf(stderr, "FAILED: %d/%zu IQ4_XS MMQ cases\n", failures, sizeof(shapes) / sizeof(shapes[0])); - return 1; + REQUIRE_TRUE(false); } std::printf("ALL PASS: %zu/%zu IQ4_XS MMQ cases\n", sizeof(shapes) / sizeof(shapes[0]), sizeof(shapes) / sizeof(shapes[0])); - return 0; } diff --git a/server/test/test_model_test_paths.cpp b/server/test/test_model_test_paths.cpp index abc61f3bb..684722745 100644 --- a/server/test/test_model_test_paths.cpp +++ b/server/test/test_model_test_paths.cpp @@ -3,8 +3,6 @@ #include "scoped_env.h" #include -#include - using namespace CppUnitTestFramework; struct ModelTestPaths : CommonFixture { @@ -15,7 +13,7 @@ TEST_CASE(ModelTestPaths, RejectsMissingRequiredAsset) { luce_test::ScopedEnvVar unset(kTestEnvironmentVariable, nullptr); REQUIRE_THROW( - std::runtime_error, + TestSkippedException, luce_test::require_model(kTestEnvironmentVariable) ); } diff --git a/server/test/test_platform_compat.cpp b/server/test/test_platform_compat.cpp index dcd92a1ca..35b9a2070 100644 --- a/server/test/test_platform_compat.cpp +++ b/server/test/test_platform_compat.cpp @@ -1,3 +1,4 @@ +#include "CppUnitTestFramework.hpp" #include "common/platform_env.h" #include "server/socket_handle.h" @@ -10,47 +11,47 @@ #include #endif +using namespace CppUnitTestFramework; using namespace dflash::common; namespace { -int fail(const char * message) { - std::fprintf(stderr, "platform compatibility test failed: %s\n", message); - return 1; -} +struct PlatformCompatFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; } // namespace -int main() { +TEST_CASE(PlatformCompatFixture, platform_environment_and_socket_compatibility) { constexpr const char * kEnvName = "DFLASH_PLATFORM_COMPAT_TEST"; if (unset_environment_variable(kEnvName) != 0) { - return fail("could not clear test environment variable"); + CHECK(false); } if (set_environment_variable(kEnvName, "original", true) != 0) { - return fail("could not set environment variable"); + CHECK(false); } if (set_environment_variable(kEnvName, "replacement", false) != 0) { - return fail("non-overwriting environment update failed"); + CHECK(false); } const char * value = std::getenv(kEnvName); if (value == nullptr || std::strcmp(value, "original") != 0) { - return fail("overwrite=false replaced the existing value"); + CHECK(false); } if (set_environment_variable(kEnvName, "replacement", true) != 0) { - return fail("overwriting environment update failed"); + CHECK(false); } value = std::getenv(kEnvName); if (value == nullptr || std::strcmp(value, "replacement") != 0) { - return fail("overwrite=true did not replace the existing value"); + CHECK(false); } if (unset_environment_variable(kEnvName) != 0 || std::getenv(kEnvName) != nullptr) { - return fail("could not remove environment variable"); + CHECK(false); } if (socket_is_valid(kInvalidSocket)) { - return fail("invalid socket sentinel reported as valid"); + CHECK(false); } #if defined(_WIN32) @@ -59,7 +60,7 @@ int main() { WSADATA wsa_data{}; const int wsa_error = WSAStartup(MAKEWORD(2, 2), &wsa_data); if (wsa_error != 0) { - return fail("WSAStartup failed"); + CHECK(false); } #endif @@ -68,7 +69,8 @@ int main() { #if defined(_WIN32) WSACleanup(); #endif - return fail("could not create a TCP socket"); + CHECK(false); + return; } #if defined(_WIN32) @@ -78,5 +80,4 @@ int main() { ::close(socket_handle); #endif - return 0; } diff --git a/server/test/test_qwen35_split_tree_guard.cpp b/server/test/test_qwen35_split_tree_guard.cpp index f348a7de3..6a1cd2bd5 100644 --- a/server/test/test_qwen35_split_tree_guard.cpp +++ b/server/test/test_qwen35_split_tree_guard.cpp @@ -1,9 +1,11 @@ +#include "CppUnitTestFramework.hpp" #include "qwen35_layer_split_tree_guard.h" #include #include #include +using namespace CppUnitTestFramework; using dflash::common::qwen35_split_run_if_root_inclusive_pure_chain; namespace { @@ -13,39 +15,32 @@ bool mark_executed(void * context) { return true; } -bool expect(const char * name, const std::vector & parents, - std::size_t n_actual, bool expected) { +struct Qwen35SplitTreeGuardFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; + +bool runs_as_expected(const std::vector & parents, std::size_t n_actual, + bool expected) { bool executed = false; const bool result = qwen35_split_run_if_root_inclusive_pure_chain( parents.data(), parents.size(), n_actual, mark_executed, &executed); - const bool ok = result == expected && executed == expected; - std::fprintf(stderr, - "qwen35_split_tree_guard case=%s result=%d sentinel_executed=%d expected=%d pass=%d\n", - name, result ? 1 : 0, executed ? 1 : 0, expected ? 1 : 0, ok ? 1 : 0); - return ok; + return result == expected && executed == expected; } } // namespace -int main() { - bool ok = true; - ok &= expect("single_root", {-1}, 1, true); - ok &= expect("pure_chain", {-1, 0, 1, 2, 3, 4}, 6, true); - ok &= expect("malformed_root", {0, 0, 1}, 3, false); - ok &= expect("skipped_parent", {-1, 0, 0}, 3, false); - ok &= expect("sibling", {-1, 0, 0, 2}, 4, false); - ok &= expect("cycle_or_forward_parent", {-1, 2, 1}, 3, false); - ok &= expect("out_of_range", {-1, 0, 7}, 3, false); - ok &= expect("truncated", {-1, 0}, 3, false); +TEST_CASE(Qwen35SplitTreeGuardFixture, root_inclusive_pure_chain_guard) { + CHECK(runs_as_expected({-1}, 1, true)); + CHECK(runs_as_expected({-1, 0, 1, 2, 3, 4}, 6, true)); + CHECK(runs_as_expected({0, 0, 1}, 3, false)); + CHECK(runs_as_expected({-1, 0, 0}, 3, false)); + CHECK(runs_as_expected({-1, 0, 0, 2}, 4, false)); + CHECK(runs_as_expected({-1, 2, 1}, 3, false)); + CHECK(runs_as_expected({-1, 0, 7}, 3, false)); + CHECK(runs_as_expected({-1, 0}, 3, false)); bool null_executed = false; - const bool null_ok = !qwen35_split_run_if_root_inclusive_pure_chain( - nullptr, 0, 1, mark_executed, &null_executed) && !null_executed; - std::fprintf(stderr, - "qwen35_split_tree_guard case=null result=%d sentinel_executed=%d pass=%d\n", - 0, null_executed ? 1 : 0, null_ok ? 1 : 0); - ok &= null_ok; - - std::fprintf(stderr, "qwen35_split_tree_guard overall=%s\n", ok ? "PASS" : "FAIL"); - return ok ? 0 : 1; + CHECK(!qwen35_split_run_if_root_inclusive_pure_chain( + nullptr, 0, 1, mark_executed, &null_executed)); + CHECK(!null_executed); } diff --git a/server/test/test_qwen35_tensor_parallel.cpp b/server/test/test_qwen35_tensor_parallel.cpp index ba01cf192..f3a5294d8 100644 --- a/server/test/test_qwen35_tensor_parallel.cpp +++ b/server/test/test_qwen35_tensor_parallel.cpp @@ -1,23 +1,21 @@ +#include "CppUnitTestFramework.hpp" + #include "qwen35/qwen35_tensor_parallel.h" #include "ggml.h" -#include #include -#include -#include using namespace dflash::common; +using namespace CppUnitTestFramework; -#define CHECK(condition) do { \ - if (!(condition)) { \ - std::fprintf(stderr, "CHECK failed: %s (%s:%d)\n", \ - #condition, __FILE__, __LINE__); \ - std::abort(); \ - } \ -} while (0) +namespace { +struct Qwen35TensorParallelFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} -static void expect_split(const TargetWeights & weights, +static bool expect_split(const TargetWeights & weights, ggml_tensor * tensor, const char * name, ggml_backend_meta_split_axis axis, @@ -25,20 +23,20 @@ static void expect_split(const TargetWeights & weights, uint32_t repeat) { ggml_set_name(tensor, name); const auto state = qwen35_tensor_parallel_split_state(tensor, weights, 2); - CHECK(state.axis == axis); - CHECK(state.n_segments == 1); - CHECK(state.ne[0] == per_device); - CHECK(state.ne[1] == per_device); - CHECK(state.nr[0] == repeat); - CHECK((state.ne[0] + state.ne[1]) * state.nr[0] == tensor->ne[axis]); + return state.axis == axis && + state.n_segments == 1 && + state.ne[0] == per_device && + state.ne[1] == per_device && + state.nr[0] == repeat && + (state.ne[0] + state.ne[1]) * state.nr[0] == tensor->ne[axis]; } -int main() { +TEST_CASE(Qwen35TensorParallelFixture, tensor_parallel_split_state) { ggml_init_params params{}; params.mem_size = 32 * ggml_tensor_overhead(); params.no_alloc = true; ggml_context * ctx = ggml_init(params); - CHECK(ctx); + REQUIRE(ctx != nullptr); TargetWeights weights; @@ -50,30 +48,30 @@ int main() { CHECK(zero_device_state.axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); CHECK(zero_device_state.n_segments == 1); - expect_split(weights, + REQUIRE(expect_split(weights, ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 5120, 10240), - "blk.0.attn_qkv.weight", GGML_BACKEND_SPLIT_AXIS_1, 1024, 5); - expect_split(weights, + "blk.0.attn_qkv.weight", GGML_BACKEND_SPLIT_AXIS_1, 1024, 5)); + REQUIRE(expect_split(weights, ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 4, 10240), - "blk.0.ssm_conv1d.weight", GGML_BACKEND_SPLIT_AXIS_1, 1024, 5); - expect_split(weights, + "blk.0.ssm_conv1d.weight", GGML_BACKEND_SPLIT_AXIS_1, 1024, 5)); + REQUIRE(expect_split(weights, ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 6144, 5120), - "blk.0.ssm_out.weight", GGML_BACKEND_SPLIT_AXIS_0, 1024, 3); - expect_split(weights, + "blk.0.ssm_out.weight", GGML_BACKEND_SPLIT_AXIS_0, 1024, 3)); + REQUIRE(expect_split(weights, ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 5120, 12288), - "blk.3.attn_q.weight", GGML_BACKEND_SPLIT_AXIS_1, 6144, 1); - expect_split(weights, + "blk.3.attn_q.weight", GGML_BACKEND_SPLIT_AXIS_1, 6144, 1)); + REQUIRE(expect_split(weights, ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 6144, 5120), - "blk.3.attn_output.weight", GGML_BACKEND_SPLIT_AXIS_0, 3072, 1); - expect_split(weights, + "blk.3.attn_output.weight", GGML_BACKEND_SPLIT_AXIS_0, 3072, 1)); + REQUIRE(expect_split(weights, ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 256, 4096, 4), - "cache_k_3", GGML_BACKEND_SPLIT_AXIS_2, 2, 1); - expect_split(weights, + "cache_k_3", GGML_BACKEND_SPLIT_AXIS_2, 2, 1)); + REQUIRE(expect_split(weights, ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 128, 128, 48), - "ssm_state_0", GGML_BACKEND_SPLIT_AXIS_2, 8, 3); - expect_split(weights, + "ssm_state_0", GGML_BACKEND_SPLIT_AXIS_2, 8, 3)); + REQUIRE(expect_split(weights, ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 3, 10240), - "conv_state_0", GGML_BACKEND_SPLIT_AXIS_1, 1024, 5); + "conv_state_0", GGML_BACKEND_SPLIT_AXIS_1, 1024, 5)); ggml_tensor * norm = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 5120); ggml_set_name(norm, "blk.0.attn_norm.weight"); @@ -115,6 +113,4 @@ int main() { CHECK(!Qwen35TensorParallelContext::create(too_many_devices, weights)); ggml_free(ctx); - std::puts("qwen35 tensor-parallel split tests passed"); - return 0; } diff --git a/server/test/test_recurrent_snapshot.cpp b/server/test/test_recurrent_snapshot.cpp index a9b247caf..6fb5de4d6 100644 --- a/server/test/test_recurrent_snapshot.cpp +++ b/server/test/test_recurrent_snapshot.cpp @@ -1,3 +1,4 @@ +#include "CppUnitTestFramework.hpp" #include "internal.h" #include "ggml-backend.h" @@ -7,21 +8,19 @@ #include #include +using namespace CppUnitTestFramework; + using dflash::common::TargetCache; using dflash::common::restore_ssm_state; using dflash::common::snapshot_ssm_state; -static int failures = 0; - -#define CHECK(expr) do { \ - if (!(expr)) { \ - std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \ - failures++; \ - } \ -} while (0) +namespace { +struct RecurrentSnapshotFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} static void set_tensor(ggml_tensor * tensor, const std::vector & values) { - CHECK(ggml_nelements(tensor) == (int64_t)values.size()); ggml_backend_tensor_set(tensor, values.data(), 0, values.size() * sizeof(float)); } @@ -33,10 +32,10 @@ static std::vector get_tensor(const ggml_tensor * tensor) { return values; } -int main() { +TEST_CASE(RecurrentSnapshotFixture, snapshot_and_restore_recurrent_state) { ggml_backend_t backend = ggml_backend_cpu_init(); CHECK(backend != nullptr); - if (!backend) return 1; + if (!backend) SKIP("CPU backend is unavailable"); ggml_init_params params{}; params.mem_size = 8 * ggml_tensor_overhead(); @@ -45,7 +44,7 @@ int main() { CHECK(ctx != nullptr); if (!ctx) { ggml_backend_free(backend); - return 1; + SKIP("could not initialize ggml context"); } ggml_tensor * ssm = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 4, 3); @@ -57,7 +56,7 @@ int main() { if (!buffer) { ggml_free(ctx); ggml_backend_free(backend); - return 1; + SKIP("could not allocate CPU backend tensors"); } TargetCache cache; @@ -75,6 +74,8 @@ int main() { const std::vector ssm_mutated(ssm_original.size(), -1.0f); const std::vector conv_mutated(conv_original.size(), -2.0f); + CHECK(ggml_nelements(ssm) == (int64_t) ssm_original.size()); + CHECK(ggml_nelements(conv) == (int64_t) conv_original.size()); set_tensor(ssm, ssm_original); set_tensor(conv, conv_original); CHECK(snapshot_ssm_state(cache, backend)); @@ -99,11 +100,4 @@ int main() { ggml_backend_buffer_free(buffer); ggml_free(ctx); ggml_backend_free(backend); - - if (failures != 0) { - std::fprintf(stderr, "%d recurrent snapshot test(s) failed\n", failures); - return 1; - } - std::printf("recurrent snapshot tests passed\n"); - return 0; } diff --git a/server/test/test_rms_norm_hip.cpp b/server/test/test_rms_norm_hip.cpp index 075df4d45..f84149f5e 100644 --- a/server/test/test_rms_norm_hip.cpp +++ b/server/test/test_rms_norm_hip.cpp @@ -18,6 +18,8 @@ #include #include #include +#include "CppUnitTestFramework.hpp" +using CppUnitTestFramework::CommonFixture; extern "C" void launch_rms_norm_mul_w_f32( const float * src, const float * w, float * dst, @@ -28,15 +30,28 @@ extern "C" void launch_rms_norm_mul_w_f32( cudaError_t e = (call); \ if (e != cudaSuccess) { \ std::fprintf(stderr, "HIP error %s at %s:%d: %s\n", #call, __FILE__, __LINE__, cudaGetErrorString(e)); \ - return 1; \ + REQUIRE_TRUE(false); \ } \ } while (0) -int main() { +namespace { +struct RmsNormHipFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(RmsNormHipFixture, rms_norm_matches_cpu) { constexpr int N_TOK = 64; constexpr int HIDDEN = 2048; // > block(256): forces the strided load loop constexpr float EPS = 1e-5f; + int device_count = 0; + const cudaError_t device_status = cudaGetDeviceCount(&device_count); + if (device_status == cudaErrorNoDevice || device_count == 0) { + SKIP("no HIP device available"); + } + CK(device_status); + cudaDeviceProp prop; CK(cudaGetDeviceProperties(&prop, 0)); std::printf("[rmsnorm-test] device=%s warpSize=%d hidden=%d n_tok=%d\n", @@ -89,8 +104,7 @@ int main() { const float TOL = 1e-4f; if (max_abs > TOL) { std::fprintf(stderr, "[rmsnorm-test] FAIL: max_abs_diff %.3e exceeds tol %.3e\n", max_abs, TOL); - return 1; + REQUIRE_TRUE(false); } std::printf("[rmsnorm-test] PASS\n"); - return 0; } diff --git a/server/test/test_rocmfp3_mix_registry.cpp b/server/test/test_rocmfp3_mix_registry.cpp index 09947982b..ab496064d 100644 --- a/server/test/test_rocmfp3_mix_registry.cpp +++ b/server/test/test_rocmfp3_mix_registry.cpp @@ -9,8 +9,11 @@ // the register_host allocations) fails the leak assertion below. #include "ds4_test_gpu_runtime.h" +#include "CppUnitTestFramework.hpp" #include "ggml-cuda.h" #include "rocmfp3_mix.cuh" +using CppUnitTestFramework::CommonFixture; +#undef CHECK #include #include @@ -25,7 +28,20 @@ static int g_fails = 0; if (!(cond)) { std::fprintf(stderr, "FAIL: %s\n", (msg)); ++g_fails; } \ } while (0) -int main() { +namespace { +struct Rocmfp3MixRegistryFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(Rocmfp3MixRegistryFixture, registry_lifecycle) { + int device_count = 0; + const cudaError_t device_status = cudaGetDeviceCount(&device_count); + if (device_status == cudaErrorNoDevice || device_count == 0) { + SKIP("no CUDA/HIP device available"); + } + REQUIRE_TRUE(device_status == cudaSuccess); + const int E = 8, out = 64, in = 64; const size_t expert_bytes = (size_t) out * (in / 32) * 14; const size_t nb02 = 4096; // includes alignment padding after each payload @@ -152,5 +168,5 @@ int main() { std::fprintf(stderr, g_fails ? "REGISTRY TEST FAILED (%d)\n" : "REGISTRY TEST OK\n", g_fails); - return g_fails ? 1 : 0; + REQUIRE_TRUE(g_fails == 0); } diff --git a/server/test/test_rocmfp_mix_gateup_glu.cpp b/server/test/test_rocmfp_mix_gateup_glu.cpp index 9f1b35a5e..8c66aa190 100644 --- a/server/test/test_rocmfp_mix_gateup_glu.cpp +++ b/server/test/test_rocmfp_mix_gateup_glu.cpp @@ -33,7 +33,10 @@ // tensor with an undecoded one. #include "ds4_test_gpu_runtime.h" +#include "CppUnitTestFramework.hpp" #include "ggml-cuda.h" +using CppUnitTestFramework::CommonFixture; +#undef CHECK #include #include @@ -105,11 +108,16 @@ static float host_swiglu_ds4(float gate, float up, float limit) { return silu * up; } -int main() { +namespace { +struct RocmfpMixGateupGluFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(RocmfpMixGateupGluFixture, fused_gateup_glu) { int ndev = 0; if (cudaGetDeviceCount(&ndev) != cudaSuccess || ndev == 0) { - std::fprintf(stderr, "SKIP: no HIP device\n"); - return 0; + SKIP("no HIP device available"); } // in must be a multiple of 128: the wide block load reads 128 weights at a time and would @@ -298,8 +306,7 @@ int main() { HIP_OK(cudaFree(d_up_out)); HIP_OK(cudaFree(d_gate_out)); HIP_OK(cudaFree(d_fused)); HIP_OK(cudaFree(d_swapped)); - if (g_fails) { std::fprintf(stderr, "%d FAILURE(S)\n", g_fails); return 1; } + if (g_fails) { std::fprintf(stderr, "%d FAILURE(S)\n", g_fails); REQUIRE_TRUE(false); } std::fprintf(stderr, "OK: fused gate/up GLU matches the unfused pair, order is respected, " "half-registered/mismatched pairs are refused, and out-of-range ids zero\n"); - return 0; } diff --git a/server/test/test_rocmfp_mix_glu_fusable.cpp b/server/test/test_rocmfp_mix_glu_fusable.cpp index 72835d4d5..93491310c 100644 --- a/server/test/test_rocmfp_mix_glu_fusable.cpp +++ b/server/test/test_rocmfp_mix_glu_fusable.cpp @@ -15,6 +15,9 @@ // No device work: every case is pure admission logic on tensor metadata. #include "ggml.h" +#include "CppUnitTestFramework.hpp" +using CppUnitTestFramework::CommonFixture; +#undef CHECK #include #include @@ -60,7 +63,13 @@ struct Triple { } // namespace -int main() { +namespace { +struct RocmfpMixGluFusableFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(RocmfpMixGluFusableFixture, admission_predicate) { // The happy path: everything matched, layout direct. Establishes that the negative // cases below fail for the reason under test and not because the fixture is malformed. { @@ -147,5 +156,5 @@ int main() { std::fprintf(stderr, g_fails ? "MIX GLU FUSABLE TEST FAILED (%d)\n" : "MIX GLU FUSABLE TEST OK\n", g_fails); - return g_fails ? 1 : 0; + REQUIRE_TRUE(g_fails == 0); } diff --git a/server/test/test_rocmfp_mix_slice_matvec.cpp b/server/test/test_rocmfp_mix_slice_matvec.cpp index 19e33e71e..30302e0a6 100644 --- a/server/test/test_rocmfp_mix_slice_matvec.cpp +++ b/server/test/test_rocmfp_mix_slice_matvec.cpp @@ -21,7 +21,10 @@ // neighbouring tensor's codebook. That failure mode is wrong numbers, not a crash. #include "ds4_test_gpu_runtime.h" +#include "CppUnitTestFramework.hpp" #include "ggml-cuda.h" +using CppUnitTestFramework::CommonFixture; +#undef CHECK #include #include @@ -68,11 +71,16 @@ static constexpr int BLOCK_BYTES = 14; static uint32_t xs = 0x2545F491u; static uint32_t rnd() { xs ^= xs << 13; xs ^= xs >> 17; xs ^= xs << 5; return xs; } -int main() { +namespace { +struct RocmfpMixSliceMatvecFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; +} + +TEST_CASE(RocmfpMixSliceMatvecFixture, slice_matvec_matches_reference) { int ndev = 0; if (cudaGetDeviceCount(&ndev) != cudaSuccess || ndev == 0) { - std::fprintf(stderr, "SKIP: no HIP device\n"); - return 0; + SKIP("no HIP device available"); } // Shapes echo the real case in miniature: attn_output_a is [group_dim, n_lora_o] @@ -211,5 +219,5 @@ int main() { "(%d slices x %d tokens x %d rows) and refuses under-registration\n", nslices, ntokens, out); } - return g_fails == 0 ? 0 : 1; + REQUIRE_TRUE(g_fails == 0); } From 126ad138cdac024d73bd2fa35ccba5ebbd440dd5 Mon Sep 17 00:00:00 2001 From: Howard Su Date: Sat, 15 Aug 2026 09:15:39 +0800 Subject: [PATCH 3/3] Handle unavailable test runtimes as skips --- server/CMakeLists.txt | 1 + server/test/test_rms_norm_hip.cpp | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 716e5c570..9b57600ad 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1575,6 +1575,7 @@ if(DFLASH27B_TESTS) endif() if(CMAKE_CROSSCOMPILING) add_test(NAME model_smoke COMMAND test_model_smoke) + set_tests_properties(model_smoke PROPERTIES SKIP_RETURN_CODE 77) else() set(_model_smoke_ctest_generated "${CMAKE_CURRENT_BINARY_DIR}/test_model_smoke_discovered_tests.cmake") diff --git a/server/test/test_rms_norm_hip.cpp b/server/test/test_rms_norm_hip.cpp index f84149f5e..5326f90b7 100644 --- a/server/test/test_rms_norm_hip.cpp +++ b/server/test/test_rms_norm_hip.cpp @@ -47,10 +47,13 @@ TEST_CASE(RmsNormHipFixture, rms_norm_matches_cpu) { int device_count = 0; const cudaError_t device_status = cudaGetDeviceCount(&device_count); - if (device_status == cudaErrorNoDevice || device_count == 0) { + if (device_status == cudaErrorNoDevice) { SKIP("no HIP device available"); } CK(device_status); + if (device_count == 0) { + SKIP("no HIP device available"); + } cudaDeviceProp prop; CK(cudaGetDeviceProperties(&prop, 0));