diff --git a/.codecov.yml b/.codecov.yml index 2620c51c..5dc2d51f 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -25,4 +25,5 @@ ignore: - "*/examples/*" - "*/python/unittest/*" - "*/ark/unittest/*" + - "*/ark/ops/ops_test_common.*" - "*/ark/include/kernels/*" diff --git a/.github/workflows/ut.yml b/.github/workflows/ut.yml index 1dd1a854..2fbe2733 100644 --- a/.github/workflows/ut.yml +++ b/.github/workflows/ut.yml @@ -76,7 +76,7 @@ jobs: if: github.event_name != 'schedule' run: | cd build - lcov --capture --directory . --output-file cpp_coverage.info + lcov --ignore-errors negative --capture --directory . --output-file cpp_coverage.info lcov --ignore-errors unused --remove cpp_coverage.info \ '/usr/*' \ '/tmp/*' \ @@ -86,6 +86,7 @@ jobs: '*/examples/*' \ '*/python/*' \ '*/ark/unittest/unittest_utils.cpp' \ + '*/ark/ops/ops_test_common.*' \ --output-file cpp_coverage.info lcov --list cpp_coverage.info diff --git a/ark/gpu/gpu.hpp b/ark/gpu/gpu.hpp index 1010683c..176e60f7 100644 --- a/ark/gpu/gpu.hpp +++ b/ark/gpu/gpu.hpp @@ -125,6 +125,8 @@ ARK_GPU_DEFINE_CONSTANT_ALIAS(gpuPointerAttributeSyncMemops, HIP_POINTER_ATTRIBUTE_SYNC_MEMOPS); // runtime API +ARK_GPU_DEFINE_FUNC_ALIAS(gpuGetDeviceCount, cudaGetDeviceCount, + hipGetDeviceCount); ARK_GPU_DEFINE_FUNC_ALIAS(gpuGetErrorString, cudaGetErrorString, hipGetErrorString); ARK_GPU_DEFINE_FUNC_ALIAS(gpuGetLastError, cudaGetLastError, hipGetLastError); diff --git a/ark/ops/ops_all_reduce.cpp b/ark/ops/ops_all_reduce.cpp index 08f06257..49ee60ac 100644 --- a/ark/ops/ops_all_reduce.cpp +++ b/ark/ops/ops_all_reduce.cpp @@ -13,6 +13,12 @@ Tensor Model::all_reduce(Tensor input, int gpu_id, int gpu_num, Tensor output, } if (output.is_null()) { output = this->copy(input); + } else if (output.ref()->buffer()->id() == input.ref()->buffer()->id()) { + // In-place: copy input so the ring loop does not mutate send data. + // TODO: This catches the common case (output IS input). Sub-buffer + // offset aliasing or aliasing through different buffer objects backed + // by the same allocation is not currently detected. + input = this->copy(input); } Tensor prev_recv = NullTensor; Tensor cumulate = output; diff --git a/ark/ops/ops_all_reduce_test.cpp b/ark/ops/ops_all_reduce_test.cpp index dc0ad821..14b9835c 100644 --- a/ark/ops/ops_all_reduce_test.cpp +++ b/ark/ops/ops_all_reduce_test.cpp @@ -4,6 +4,7 @@ #include "model/model_buffer.hpp" #include "model/model_node.hpp" #include "model/model_op.hpp" +#include "model/model_tensor.hpp" #include "ops_test_common.hpp" template @@ -27,6 +28,7 @@ template void test_all_reduce_internal(ark::DimType nelem) { for (int gpu_id = 0; gpu_id < NumGpus; ++gpu_id) { ark::unittest::spawn_process([gpu_id, nelem]() { + UNITTEST_SKIP(ark::unittest::get_gpu_count() < NumGpus); // Each GPU's data is equal to its GPU ID + 1. ark::Model m(gpu_id, NumGpus); ark::Tensor ones = m.tensor({nelem}, ark::FP16); @@ -118,6 +120,7 @@ template void test_all_reduce_packet_internal(ark::DimType nelem) { for (int gpu_id = 0; gpu_id < NumGpus; ++gpu_id) { ark::unittest::spawn_process([gpu_id, nelem]() { + UNITTEST_SKIP(ark::unittest::get_gpu_count() < NumGpus); // Each GPU's data is equal to its GPU ID + 1. ark::Model m(gpu_id, NumGpus); ark::Tensor ones = m.tensor({nelem}, ark::FP16); @@ -224,6 +227,7 @@ void test_all_reduce_sm_internal(ark::DimType nelem) { }; for (int gpu_id = 0; gpu_id < NumGpus; ++gpu_id) { ark::unittest::spawn_process([gpu_id, nelem, config_rule]() { + UNITTEST_SKIP(ark::unittest::get_gpu_count() < NumGpus); // Each GPU's data is equal to its GPU ID + 1. ark::Model m(gpu_id, NumGpus); ark::Tensor ones = m.tensor({nelem}, ark::FP16); @@ -244,6 +248,56 @@ void test_all_reduce_sm_internal(ark::DimType nelem) { ark::unittest::wait_all_processes(); } +template +void test_all_reduce_inplace_internal(ark::DimType nelem) { + for (int gpu_id = 0; gpu_id < NumGpus; ++gpu_id) { + ark::unittest::spawn_process([gpu_id, nelem]() { + UNITTEST_SKIP(ark::unittest::get_gpu_count() < NumGpus); + // Each GPU's data is equal to its GPU ID + 1. + ark::Model m(gpu_id, NumGpus); + ark::Tensor ones = m.tensor({nelem}, ark::FP16); + ark::Tensor data = m.mul(ones, float(gpu_id + 1)); + // In-place: pass the same tensor as both input and output. + ark::Tensor output = m.all_reduce(data, gpu_id, NumGpus, data); + + // Verify the output is truly in-place (same buffer as input). + UNITTEST_EQ(output.ref()->buffer()->id(), + data.ref()->buffer()->id()); + + std::vector ones_vec(ones.shape().nelems(), + ark::half_t(1.0f)); + auto result = ark::op_test( + "all_reduce_inplace", m, {ones}, {output}, + baseline_all_reduce, {ones_vec.data()}); + UNITTEST_LOG(result); + UNITTEST_EQ(result.max_diff[0], 0.0f); + return ark::unittest::SUCCESS; + }); + } + ark::unittest::wait_all_processes(); +} + +// The corruption was most visible with gpu_num >= 3 (multiple ring +// iterations); this 2-GPU case validates the in-place copy path still +// works for the simpler ring. +ark::unittest::State test_all_reduce_inplace_2gpus() { + test_all_reduce_inplace_internal<2>(64); + test_all_reduce_inplace_internal<2>(8192); + return ark::unittest::SUCCESS; +} + +ark::unittest::State test_all_reduce_inplace_3gpus() { + test_all_reduce_inplace_internal<3>(64); + test_all_reduce_inplace_internal<3>(8192); + return ark::unittest::SUCCESS; +} + +ark::unittest::State test_all_reduce_inplace_4gpus() { + test_all_reduce_inplace_internal<4>(64); + test_all_reduce_inplace_internal<4>(8192); + return ark::unittest::SUCCESS; +} + ark::unittest::State test_all_reduce_4gpus() { test_all_reduce_internal<4>(64); test_all_reduce_internal<4>(8192); @@ -287,5 +341,8 @@ int main() { UNITTEST(test_all_reduce_packet_8gpus); UNITTEST(test_all_reduce_sm_4gpus); UNITTEST(test_all_reduce_sm_8gpus); + UNITTEST(test_all_reduce_inplace_2gpus); + UNITTEST(test_all_reduce_inplace_3gpus); + UNITTEST(test_all_reduce_inplace_4gpus); return 0; } diff --git a/ark/ops/ops_test_common.cpp b/ark/ops/ops_test_common.cpp index f4139a0e..12922f45 100644 --- a/ark/ops/ops_test_common.cpp +++ b/ark/ops/ops_test_common.cpp @@ -11,7 +11,6 @@ #include "ark/planner.hpp" #include "ark/random.hpp" #include "cpu_timer.h" -#include "env.h" #include "gpu/gpu_logging.hpp" #include "logging.hpp" #include "model/model_data_type.hpp" @@ -40,11 +39,7 @@ OpsTestResult op_test(const std::string &test_name_prefix, const Model &model, const std::vector &inputs_data, const std::vector &config_rules, bool print_on_error) { - const char *env = std::getenv("ARK_UT_CORRECTNESS"); - if (!env || std::string(env) != "1") { - // Zero values let callers pass without GPU work. - LOG(INFO, "[SKIP] %s: ARK_UT_CORRECTNESS not set", - test_name_prefix.c_str()); + auto make_skipped = [&]() { OpsTestResult skipped; skipped.test_name = test_name_prefix; skipped.iter = 0; @@ -56,6 +51,17 @@ OpsTestResult op_test(const std::string &test_name_prefix, const Model &model, skipped.num_wrong.resize(n, 0); skipped.num_total.resize(n, 0); return skipped; + }; + const char *env = std::getenv("ARK_UT_CORRECTNESS"); + if (!env || std::string(env) != "1") { + // Zero values let callers pass without GPU work. + LOG(INFO, "[SKIP] %s: ARK_UT_CORRECTNESS not set", + test_name_prefix.c_str()); + return make_skipped(); + } + if (ark::unittest::get_gpu_count() < 1) { + LOG(INFO, "[SKIP] %s: no GPU available", test_name_prefix.c_str()); + return make_skipped(); } DefaultExecutor exe(model, -1, nullptr, config_rules); diff --git a/ark/unittest/unittest_utils.cpp b/ark/unittest/unittest_utils.cpp index 4b74f951..24b47d4b 100644 --- a/ark/unittest/unittest_utils.cpp +++ b/ark/unittest/unittest_utils.cpp @@ -11,6 +11,7 @@ #include #include "file_io.h" +#include "gpu/gpu.hpp" #include "logging.hpp" // Grep SIGALRM and exit. @@ -22,6 +23,14 @@ static void sigalrm_timeout_handler(int) { namespace ark { namespace unittest { +int get_gpu_count() { + int count = 0; + if (gpuGetDeviceCount(&count) != gpuSuccess) { + return 0; + } + return count; +} + // Temporal unittest states. struct TempStates { std::vector pids; diff --git a/ark/unittest/unittest_utils.h b/ark/unittest/unittest_utils.h index 383f49b6..b0a31f95 100644 --- a/ark/unittest/unittest_utils.h +++ b/ark/unittest/unittest_utils.h @@ -22,6 +22,9 @@ namespace unittest { typedef enum { SUCCESS = 0, FAILURE, UNEXPECTED } State; +// Return the number of visible GPUs (calls gpuGetDeviceCount). +int get_gpu_count(); + void exit(State s, const std::string &errmsg); void fexit(const std::string &errmsg = ""); void uexit(const std::string &errmsg = ""); @@ -240,4 +243,15 @@ std::string get_kernel_code(const std::string &name); // Log a message. #define UNITTEST_LOG(...) LOG(ark::INFO, __VA_ARGS__) +// Skip the current test (exit with SUCCESS) if @p cond is true. +// WARNING: calls std::exit(), which skips local destructors. +// Only safe inside spawn_process() child lambdas. +#define UNITTEST_SKIP(cond) \ + do { \ + if (cond) { \ + LOG(ark::INFO, "unittest skip: " #cond); \ + std::exit(ark::unittest::SUCCESS); \ + } \ + } while (0) + #endif // ARK_UNITTEST_UNITTEST_UTILS_H_