diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e53264e3..16042017 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,13 @@ jobs: matrix: with_caliper: [ON, OFF] with_mpi: [ON, OFF] + with_torch: [ON, OFF] steps: - uses: actions/checkout@v7 - name: Ensure git safe directory run: git config --global --add safe.directory '*' - - name: Build ENABLE_CALIPER=${{ matrix.with_caliper }} ENABLE_MPI=${{ matrix.with_mpi }} + - name: Build ENABLE_TORCH=${{ matrix.with_torch }} ENABLE_CALIPER=${{ matrix.with_caliper }} ENABLE_MPI=${{ matrix.with_mpi }} shell: bash -l {0} run: | source /etc/profile @@ -42,6 +43,7 @@ jobs: -DENABLE_TESTS=On \ -DAMS_ENABLE_DEBUG=On \ -DENABLE_WORKFLOW=Off \ + -DENABLE_TORCH=${{ matrix.with_torch }} \ -DTorch_DIR=$AMS_TORCH_PATH \ -Dcaliper_DIR=$AMS_CALIPER_PATH \ -DAMS_FMT_DIR=$AMS_FMT_DIR \ diff --git a/.gitlab/jobs/dane.yml b/.gitlab/jobs/dane.yml index 1c80503d..f7b41bce 100644 --- a/.gitlab/jobs/dane.yml +++ b/.gitlab/jobs/dane.yml @@ -61,8 +61,9 @@ build-run-dane: .build-variants: parallel: matrix: - - WITH_MPI: ["on", "off"] - WITH_WORKFLOW: ["on", "off"] + - WITH_MPI: ["ON", "OFF"] + WITH_WORKFLOW: ["ON", "OFF"] + WITH_TORCH: ["ON", "OFF"] build-run-dane: extends: [.base-job, .build-variants] diff --git a/.gitlab/jobs/tioga.yml b/.gitlab/jobs/tioga.yml index 2d07a211..4d369899 100644 --- a/.gitlab/jobs/tioga.yml +++ b/.gitlab/jobs/tioga.yml @@ -36,8 +36,9 @@ variables: .build-variants: parallel: matrix: - - WITH_MPI: ["on", "off"] - WITH_WORKFLOW: ["on", "off"] + - WITH_MPI: ["ON", "OFF"] + WITH_WORKFLOW: ["ON", "OFF"] + WITH_TORCH: ["ON", "OFF"] build-run-tioga: extends: [.base-job, .build-variants] diff --git a/.gitlab/jobs/tuolumne.yml b/.gitlab/jobs/tuolumne.yml index 74a7064c..7c121c61 100644 --- a/.gitlab/jobs/tuolumne.yml +++ b/.gitlab/jobs/tuolumne.yml @@ -40,8 +40,9 @@ build-run-tuolumne: .build-variants: parallel: matrix: - - WITH_MPI: ["on", "off"] - WITH_WORKFLOW: ["on", "off"] + - WITH_MPI: ["ON", "OFF"] + WITH_WORKFLOW: ["ON", "OFF"] + WITH_TORCH: ["ON", "OFF"] build-run-tuolumne: extends: [.base-job, .build-variants] diff --git a/CMakeLists.txt b/CMakeLists.txt index e3a2a04c..8a7da45d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ endif() option(ENABLE_WORKFLOW "Install python drivers used by the outer workflow" OFF) option(ENABLE_RMQ "Use RabbitMQ as a database back end" OFF) option(ENABLE_PERFFLOWASPECT "Use PerfFlowAspect for profiling" OFF) +option(ENABLE_TORCH "Enable PyTorch ML inference support" ON) option(AMS_ENABLE_DEBUG "Enable verbose AMS messages" OFF) option(AMS_INSTALL_FLUX_PYTHON "Install AMS Workflow Python package with the flux-python optional dependency" @@ -240,6 +241,7 @@ if (AMS_DEFER_STATIC_TPL_RESOLUTION AND NOT BUILD_SHARED_LIBS) "AMS_DEFER_STATIC_TPL_RESOLUTION is ignored when BUILD_SHARED_LIBS=OFF.") endif() +# set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) include(cmake/FetchAndAddFmt.cmake) @@ -289,7 +291,17 @@ if (ENABLE_CUDA) find_package(CUDAToolkit REQUIRED) list(APPEND AMS_APP_DEFINES "__AMS_ENABLE_CUDA__") elseif (ENABLE_HIP) - find_package(HIP REQUIRED) + find_package(hip REQUIRED) + find_package(hiprtc QUIET) # can be needed down the line + + # a bit ugly here + if (NOT TARGET hiprtc::hiprtc) + message(STATUS "hiprtc not found, linking directly to ${ROCM_PATH}/lib/libhiprtc.so") + add_library(hiprtc::hiprtc UNKNOWN IMPORTED) + set_target_properties(hiprtc::hiprtc PROPERTIES + IMPORTED_LOCATION "${ROCM_PATH}/lib/libhiprtc.so") + endif() + list(APPEND AMS_APP_DEFINES "__AMS_ENABLE_HIP__") if (DEFINED ROCM_PATH) string(APPEND CMAKE_CXX_FLAGS "-I${ROCM_PATH}/include/") @@ -390,11 +402,46 @@ if (ENABLE_RMQ) find_package(libevent REQUIRED) endif() -find_package(Torch REQUIRED) -# This is annoying, torch populates all my cuda flags -# and resets them -set(CMAKE_CUDA_FLAGS "") -set(CMAKE_CUDA_ARCHITECTURES ON) +# ------------------------------------------------------------------------------ +if (ENABLE_TORCH) + find_package(Torch REQUIRED) + # This is annoying, torch populates all my cuda flags + # and resets them + set(CMAKE_CUDA_FLAGS "") + set(CMAKE_CUDA_ARCHITECTURES ON) + # Torch/PyTorch propagates C-only warning flags (e.g. -Wno-duplicate-decl-specifier) + # via imported target interface options and HIP variables. + # See: https://github.com/pytorch/pytorch/pull/164552 + # Strip them so they don't get passed to the C++ compiler. + foreach(_target torch torch_cpu torch_cuda torch_hip c10 c10_cuda c10_hip) + if(TARGET ${_target}) + get_target_property(_opts ${_target} INTERFACE_COMPILE_OPTIONS) + if(_opts) + string(REGEX REPLACE "-Wno-duplicate-decl-specifier" "" _opts "${_opts}") + set_target_properties(${_target} PROPERTIES INTERFACE_COMPILE_OPTIONS "${_opts}") + endif() + endif() + endforeach() + # Also strip from HIP-specific CMake variables that torch may have polluted + foreach(_var CMAKE_CXX_FLAGS CMAKE_HIP_FLAGS HIP_CXX_FLAGS HIP_HIPCC_FLAGS) + if(DEFINED ${_var}) + string(REPLACE "-Wno-duplicate-decl-specifier" "" ${_var} "${${_var}}") + endif() + endforeach() + list(APPEND AMS_APP_DEFINES "__AMS_ENABLE_TORCH__") +else() + message(STATUS "PyTorch support disabled (ENABLE_TORCH=OFF). ML inference will not be available.") +endif() + +# ------------------------------------------------------------------------------ +if (WITH_RZ) + find_package(MPI REQUIRED) + add_subdirectory(rz) + list(APPEND AMS_APP_INCLUDES "${RZ_AMS_INCLUDES}" "${MPI_INCLUDE_PATH}") + list(APPEND AMS_APP_LIB_DIRS "${RZ_AMS_LIBDIRS}") + list(APPEND AMS_APP_LIBRARIES "${RZ_AMS_LIBRARIES}" "${MPI_C_LIBRARIES}") + list(APPEND AMS_APP_DEFINES "${RZ_AMS_DEFINES}") +endif() if (ENABLE_PERFFLOWASPECT) find_package(perfflowaspect CONFIG REQUIRED) @@ -406,9 +453,12 @@ endif() if (NOT BUILD_SHARED_LIBS) # Pin the location hints (see ams_append_pinned_dependency above) ams_append_pinned_dependency("find_dependency(fmt CONFIG REQUIRED)" fmt_DIR) - ams_append_pinned_dependency("find_dependency(Torch REQUIRED)" Torch_DIR) ams_append_pinned_dependency("find_dependency(nlohmann_json REQUIRED)" nlohmann_json_DIR) ams_append_pinned_dependency("find_dependency(tl-expected REQUIRED)" tl-expected_DIR) + + if (ENABLE_TORCH) + ams_append_pinned_dependency("find_dependency(Torch CONFIG REQUIRED)" Torch_DIR) + endif() if (ENABLE_MPI) ams_append_package_dependency("find_dependency(MPI REQUIRED COMPONENTS C CXX)") endif() @@ -419,7 +469,7 @@ if (NOT BUILD_SHARED_LIBS) ams_append_pinned_dependency("find_dependency(HIP REQUIRED)" hip_DIR) endif() if (ENABLE_CALIPER) - ams_append_pinned_dependency("find_dependency(caliper REQUIRED)" caliper_DIR) + ams_append_pinned_dependency("find_dependency(caliper CONFIG REQUIRED)" caliper_DIR) endif() if (AMS_HDF5_MODE STREQUAL "HDF5_STATIC_TARGET") ams_append_pinned_dependency("find_dependency(HDF5 CONFIG REQUIRED COMPONENTS C static)" HDF5_DIR) diff --git a/INSTALL.md b/INSTALL.md index ef7535fb..cd590c0f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -198,6 +198,7 @@ source scripts/gitlab/setup-env.sh cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ +<<<<<<< HEAD -DBUILD_SHARED_LIBS=On \ -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=On \ -DENABLE_MPI=On \ @@ -213,6 +214,20 @@ cmake -S . -B build \ -DAMS_FMT_DIR="$AMS_FMT_DIR" \ -Dnlohmann_json_DIR="$AMS_NLOHMANN_JSON_DIR" \ -Dtl-expected_DIR="$AMS_TL_EXPECTED_DIR" +======= + -DWITH_CUDA=On \ + -DUMPIRE_DIR=$AMS_UMPIRE_PATH \ + -DMFEM_DIR=$AMS_MFEM_PATH \ + -DWITH_FAISS=On \ + -DWITH_MPI=On \ + -DENABLE_TORCH=On \ + -DWITH_TESTS=Off \ + -DTorch_DIR=$AMS_TORCH_PATH \ + -DFAISS_DIR=$AMS_FAISS_PATH \ + -DAMS_CUDA_ARCH=${AMS_CUDA_ARCH} \ + -DWITH_AMS_DEBUG=On \ + ../ +>>>>>>> 4598085 (WIP. Removing Torch from the main path (all tests green with WITH_TORCH=On except integration tests)) cmake --build build -j 6 cmake --install build diff --git a/cmake/AMSConfig.cmake.in b/cmake/AMSConfig.cmake.in index 7a037b81..0804b14c 100644 --- a/cmake/AMSConfig.cmake.in +++ b/cmake/AMSConfig.cmake.in @@ -7,6 +7,7 @@ find_dependency(Threads REQUIRED) set(AMS_ENABLE_MPI @ENABLE_MPI@) set(AMS_ENABLE_CUDA @ENABLE_CUDA@) set(AMS_ENABLE_HIP @ENABLE_HIP@) +set(AMS_ENABLE_TORCH @ENABLE_TORCH@) set(AMS_ENABLE_CALIPER @ENABLE_CALIPER@) set(AMS_ENABLE_WORKFLOW @ENABLE_WORKFLOW@) set(AMS_ENABLE_RMQ @ENABLE_RMQ@) diff --git a/examples/bnm_opt/kernel.cpp b/examples/bnm_opt/kernel.cpp index 0d77d26e..8327d665 100644 --- a/examples/bnm_opt/kernel.cpp +++ b/examples/bnm_opt/kernel.cpp @@ -9,6 +9,8 @@ * */ +#include "kernel.hpp" + #include #include @@ -19,7 +21,6 @@ #include #include "binomialOptions.h" -#include "kernel.hpp" #include "realtype.h" #ifdef USE_AMS @@ -52,12 +53,12 @@ __device__ inline double expiryCallValue(double S, double X, double vDt, int i) #error Bad constants #endif -__global__ void static binomialOptionsKernel(const real *_S, - const real *_X, - const real *_vDt, - const real *_puByDf, - const real *_pdByDf, - real *callValue) +__global__ void static binomialOptionsKernel(const real* _S, + const real* _X, + const real* _vDt, + const real* _puByDf, + const real* _pdByDf, + real* callValue) { __shared__ real call_exchange[THREADBLOCK_SIZE + 1]; @@ -97,12 +98,12 @@ __global__ void static binomialOptionsKernel(const real *_S, } } -__global__ static void preProcessKernel(real *d_T, - real *d_R, - real *d_V, - real *d_puByDf, - real *d_pdByDf, - real *d_vDt, +__global__ static void preProcessKernel(const real* d_T, + const real* d_R, + const real* d_V, + real* d_puByDf, + real* d_pdByDf, + real* d_vDt, size_t optN) { int i = threadIdx.x + blockIdx.x * blockDim.x; @@ -131,15 +132,15 @@ __global__ static void preProcessKernel(real *d_T, // Host-side interface to GPU binomialOptions -static void binomialOptionsGPU(real *d_CallValue, - real *d_S, - real *d_X, - real *d_R, - real *d_V, - real *d_T, - real *d_puByDf, - real *d_pdByDf, - real *d_vDt, +static void binomialOptionsGPU(real* d_CallValue, + const real* d_S, + const real* d_X, + const real* d_R, + const real* d_V, + const real* d_T, + real* d_puByDf, + real* d_pdByDf, + real* d_vDt, size_t optN) { int blockSize = 256; @@ -159,18 +160,18 @@ BinomialOptions::BinomialOptions(unsigned int batchSize, int worldSize) : batchSize(batchSize), rank(rank), worldSize(worldSize) { - cudaMalloc((void **)&d_CallValue, sizeof(real) * batchSize); - cudaMalloc((void **)&d_S, sizeof(real) * batchSize); - cudaMalloc((void **)&d_X, sizeof(real) * batchSize); - cudaMalloc((void **)&d_R, sizeof(real) * batchSize); - cudaMalloc((void **)&d_V, sizeof(real) * batchSize); - cudaMalloc((void **)&d_T, sizeof(real) * batchSize); - cudaMalloc((void **)&d_puByDf, sizeof(real) * batchSize); - cudaMalloc((void **)&d_pdByDf, sizeof(real) * batchSize); - cudaMalloc((void **)&d_vDt, sizeof(real) * batchSize); + cudaMalloc((void**)&d_CallValue, sizeof(real) * batchSize); + cudaMalloc((void**)&d_S, sizeof(real) * batchSize); + cudaMalloc((void**)&d_X, sizeof(real) * batchSize); + cudaMalloc((void**)&d_R, sizeof(real) * batchSize); + cudaMalloc((void**)&d_V, sizeof(real) * batchSize); + cudaMalloc((void**)&d_T, sizeof(real) * batchSize); + cudaMalloc((void**)&d_puByDf, sizeof(real) * batchSize); + cudaMalloc((void**)&d_pdByDf, sizeof(real) * batchSize); + cudaMalloc((void**)&d_vDt, sizeof(real) * batchSize); #ifdef USE_AMS - const char *model_name = std::getenv("BO_MODEL_NAME"); + const char* model_name = std::getenv("BO_MODEL_NAME"); std::cout << "Model name is " << model_name << "\n"; if (model_name) { model = AMSQueryModel(model_name); @@ -178,38 +179,36 @@ BinomialOptions::BinomialOptions(unsigned int batchSize, model = AMSQueryModel("binomialOptions"); } - wf = AMSCreateExecutor(model, - rank, - worldSize); + wf = AMSCreateExecutor(model, rank, worldSize); #endif } #ifdef USE_AMS -void BinomialOptions::AMSRun(void *cls, +void BinomialOptions::AMSRun(void* cls, long numOptions, - void **inputs, - void **outputs) + void** inputs, + void** outputs) { - BinomialOptions *BO = reinterpret_cast(cls); - binomialOptionsGPU((real *)outputs[0], - (real *)inputs[0], - (real *)inputs[1], - (real *)inputs[2], - (real *)inputs[3], - (real *)inputs[4], - BO->d_vDt, + BinomialOptions* BO = reinterpret_cast(cls); + binomialOptionsGPU((real*)outputs[0], + (const real*)inputs[0], + (const real*)inputs[1], + (const real*)inputs[2], + (const real*)inputs[3], + (const real*)inputs[4], BO->d_puByDf, BO->d_pdByDf, + BO->d_vDt, numOptions); } #endif -void BinomialOptions::run(real *callValue, - real *_S, - real *_X, - real *_R, - real *_V, - real *_T, +void BinomialOptions::run(real* callValue, + real* _S, + real* _X, + real* _R, + real* _V, + real* _T, size_t optN) { cudaMemcpy(d_R, _R, sizeof(real) * optN, cudaMemcpyHostToDevice); @@ -219,38 +218,55 @@ void BinomialOptions::run(real *callValue, cudaMemcpy(d_X, _X, sizeof(real) * optN, cudaMemcpyHostToDevice); #ifdef USE_AMS - + SmallVector inputs; SmallVector inout; SmallVector outputs; - inputs.push_back(std::move(AMSTensor::view(d_S, {static_cast(optN), 1L}, {1, 1}, AMSResourceType::AMS_DEVICE))); - inputs.push_back(std::move(AMSTensor::view(d_X, {static_cast(optN), 1L}, {1, 1}, AMSResourceType::AMS_DEVICE))); - inputs.push_back(std::move(AMSTensor::view(d_R, {static_cast(optN), 1L}, {1, 1}, AMSResourceType::AMS_DEVICE))); - inputs.push_back(std::move(AMSTensor::view(d_V, {static_cast(optN), 1L}, {1, 1}, AMSResourceType::AMS_DEVICE))); - inputs.push_back(std::move(AMSTensor::view(d_T, {static_cast(optN), 1L}, {1, 1}, AMSResourceType::AMS_DEVICE))); - - - outputs.push_back(std::move(AMSTensor::view(d_CallValue, {static_cast(optN), 1}, {1, 1}, AMSResourceType::AMS_DEVICE))); - - DomainLambda OrigComputation = [&, this](const SmallVector &ams_ins, - SmallVector &ams_inouts, - SmallVector &ams_outs) { - binomialOptionsGPU(ams_outs[0].data(), - ams_ins[0].data(), - ams_ins[1].data(), - ams_ins[2].data(), - ams_ins[3].data(), - ams_ins[4].data(), - d_vDt, - d_puByDf, - d_pdByDf, - ams_outs[0].shape()[0]); + inputs.push_back(std::move(AMSTensor::view(d_S, + {static_cast(optN), 1L}, + {1, 1}, + AMSResourceType::AMS_DEVICE))); + inputs.push_back(std::move(AMSTensor::view(d_X, + {static_cast(optN), 1L}, + {1, 1}, + AMSResourceType::AMS_DEVICE))); + inputs.push_back(std::move(AMSTensor::view(d_R, + {static_cast(optN), 1L}, + {1, 1}, + AMSResourceType::AMS_DEVICE))); + inputs.push_back(std::move(AMSTensor::view(d_V, + {static_cast(optN), 1L}, + {1, 1}, + AMSResourceType::AMS_DEVICE))); + inputs.push_back(std::move(AMSTensor::view(d_T, + {static_cast(optN), 1L}, + {1, 1}, + AMSResourceType::AMS_DEVICE))); + + + outputs.push_back(std::move(AMSTensor::view(d_CallValue, + {static_cast(optN), 1}, + {1, 1}, + AMSResourceType::AMS_DEVICE))); + + DomainLambda OrigComputation = [&, + this](const SmallVector& ams_ins, + SmallVector& ams_inouts, + SmallVector& ams_outs) { + binomialOptionsGPU(ams_outs[0].data(), + ams_ins[0].data(), + ams_ins[1].data(), + ams_ins[2].data(), + ams_ins[3].data(), + ams_ins[4].data(), + d_puByDf, + d_pdByDf, + d_vDt, + ams_outs[0].shape()[0]); }; - AMSExecute(wf, - OrigComputation, - inputs, inout, outputs); + AMSExecute(wf, OrigComputation, inputs, inout, outputs); #else binomialOptionsGPU( d_CallValue, d_S, d_X, d_R, d_V, d_T, d_puByDf, d_pdByDf, d_vDt, optN); diff --git a/scripts/gitlab/ci-build-test.sh b/scripts/gitlab/ci-build-test.sh index e8656db4..3370a639 100755 --- a/scripts/gitlab/ci-build-test.sh +++ b/scripts/gitlab/ci-build-test.sh @@ -22,7 +22,8 @@ build_and_test() { "WITH_MPI=${WITH_MPI}" \ "WITH_WORKFLOW=${WITH_WORKFLOW}" \ "WITH_CUDA=${WITH_CUDA}" \ - "WITH_HIP=${WITH_HIP}" + "WITH_HIP=${WITH_HIP}" \ + "WITH_TORCH ${WITH_TORCH}" echo "*******************************************************************************************" build_dir="/tmp/ams/$(uuidgen)" @@ -76,6 +77,7 @@ build_and_test() { -DENABLE_HIP=${WITH_HIP} \ -DENABLE_MPI=${WITH_MPI} \ -DAMS_ENABLE_DEBUG=On \ + -DWITH_TORCH=${WITH_TORCH} \ -DTorch_DIR="$AMS_TORCH_PATH" \ -DZLIB_DIR="$AMS_ZLIB_PATH" \ -Dcaliper_DIR="$AMS_CALIPER_PATH" \ diff --git a/src/AMSlib/AMS.cpp b/src/AMSlib/AMS.cpp index 9c480815..6382a1c3 100644 --- a/src/AMSlib/AMS.cpp +++ b/src/AMSlib/AMS.cpp @@ -22,7 +22,9 @@ #include #include "AMS.h" +#ifdef __AMS_ENABLE_TORCH__ #include "ml/surrogate.hpp" +#endif #include "wf/basedb.hpp" #include "wf/debug.h" #include "wf/logger.hpp" @@ -408,7 +410,9 @@ void AMSFinalize() std::call_once(_amsFinalizeFlag, [&]() { AMS_DBG(AMS, "Finalization of AMS") _amsWrap.reset(); +#ifdef __AMS_ENABLE_TORCH__ ::SurrogateModel::clearCache(); +#endif }); } @@ -431,16 +435,20 @@ void AMSExecute(AMSExecutor executor, int64_t index = static_cast(executor); if (index >= _amsWrap->executors.size()) throw std::runtime_error("AMS Executor identifier does not exist\n"); - auto currExec = _amsWrap->executors[index]; - ams::AMSWorkflow* workflow = reinterpret_cast(currExec); AMS_DBG(AMS, "Calling AMS with in:{}, inout:{}, out:{}", ins.size(), inouts.size(), outs.size()); +#ifdef __AMS_ENABLE_TORCH__ + auto currExec = _amsWrap->executors[index]; + ams::AMSWorkflow* workflow = reinterpret_cast(currExec); callAMS(workflow, OrigComputation, ins, inouts, outs); +#else + OrigComputation(ins, inouts, outs); +#endif } void AMSExecute(AMSExecutor executor, @@ -451,12 +459,16 @@ void AMSExecute(AMSExecutor executor, int64_t index = static_cast(executor); if (index >= _amsWrap->executors.size()) throw std::runtime_error("AMS Executor identifier does not exist\n"); - auto currExec = _amsWrap->executors[index]; - ams::AMSWorkflow* workflow = reinterpret_cast(currExec); AMS_DBG(AMS, "Calling AMS with homogeneous graph"); +#ifdef __AMS_ENABLE_TORCH__ + auto currExec = _amsWrap->executors[index]; + ams::AMSWorkflow* workflow = reinterpret_cast(currExec); callAMS(workflow, OrigComputation, graph_input, outputs); +#else + OrigComputation(graph_input, outputs); +#endif } void AMSExecute(AMSExecutor executor, @@ -467,12 +479,16 @@ void AMSExecute(AMSExecutor executor, int64_t index = static_cast(executor); if (index >= _amsWrap->executors.size()) throw std::runtime_error("AMS Executor identifier does not exist\n"); - auto currExec = _amsWrap->executors[index]; - ams::AMSWorkflow* workflow = reinterpret_cast(currExec); AMS_DBG(AMS, "Calling AMS with heterogeneous graph"); +#ifdef __AMS_ENABLE_TORCH__ + auto currExec = _amsWrap->executors[index]; + ams::AMSWorkflow* workflow = reinterpret_cast(currExec); callAMS(workflow, OrigComputation, graph_input, outputs); +#else + OrigComputation(graph_input, outputs); +#endif } void AMSCExecute(AMSExecutor executor, @@ -482,7 +498,6 @@ void AMSCExecute(AMSExecutor executor, ams::SmallVector& inouts, ams::SmallVector& outs) { - // Define the lambda and let the compiler deduce the type conversion to std::function DomainLambda OrigComputation = [&](const ams::SmallVector& ams_ins, diff --git a/src/AMSlib/AMSGraph.cpp b/src/AMSlib/AMSGraph.cpp index e2f89b14..919252e3 100644 --- a/src/AMSlib/AMSGraph.cpp +++ b/src/AMSlib/AMSGraph.cpp @@ -52,10 +52,10 @@ static void requireRank(const AMSTensor& tensor, static void requireFloating(const AMSTensor& tensor, const std::string& name) { - if (!isFloatingDType(tensor.dType())) { + if (!isFloatingDType(tensor.dtype())) { throw std::runtime_error("AMSHomogeneousGraph " + name + " must be floating point, got " + - dtypeName(tensor.dType()) + "."); + dtypeName(tensor.dtype()) + "."); } } @@ -65,7 +65,7 @@ static AMSTensor makeEmptyGlobalFeatures(const AMSTensor& node_features) const Dim shape[] = {0}; const Dim strides[] = {1}; - switch (node_features.dType()) { + switch (node_features.dtype()) { case AMS_SINGLE: return AMSTensor::create(shape, strides, node_features.location()); case AMS_DOUBLE: @@ -76,7 +76,7 @@ static AMSTensor makeEmptyGlobalFeatures(const AMSTensor& node_features) throw std::runtime_error( "AMSHomogeneousGraph cannot create empty global_features from " "non-floating node_features dtype " + - dtypeName(node_features.dType()) + "."); + dtypeName(node_features.dtype()) + "."); } } @@ -166,11 +166,11 @@ void AMSHomogeneousGraph::validate() const requireFloating(node_features, "node_features"); requireRank(edge_index, 2, "edge_index"); - if (!isIntegerDType(edge_index.dType())) { + if (!isIntegerDType(edge_index.dtype())) { throw std::runtime_error( "AMSHomogeneousGraph edge_index must have integer dtype " "(int64 preferred, int32 supported), got " + - dtypeName(edge_index.dType()) + "."); + dtypeName(edge_index.dtype()) + "."); } if (edge_index.shape()[0] != 2) { throw std::runtime_error( diff --git a/src/AMSlib/AMSTensor.cpp b/src/AMSlib/AMSTensor.cpp index 4cbd6f5b..ebb37247 100644 --- a/src/AMSlib/AMSTensor.cpp +++ b/src/AMSlib/AMSTensor.cpp @@ -1,149 +1,289 @@ #include "AMSTensor.hpp" +#include +#include +#include #include +#include +#include -#include "AMS.h" -#include "ArrayRef.hpp" -#include "SmallVector.hpp" -#include "include/AMSTensor.hpp" #include "wf/resource_manager.hpp" -#include "wf/utils.hpp" using namespace ams; -/** - * @brief Computes the number of elements in the tensor given its shape. - * @param[in] shapes The shape of the tensor as an array reference. - * @return The total number of elements in the tensor. - */ -template -static inline AMSTensor::IntDimType computeNumElements(ams::ArrayRef shapes) +namespace +{ +using Dim = AMSTensor::IntDimType; + +size_t elementSize(AMSDType dtype) { - return std::accumulate(shapes.begin(), - shapes.end(), - 1, - std::multiplies()); + switch (dtype) { + case AMS_SINGLE: + return sizeof(float); + case AMS_DOUBLE: + return sizeof(double); + case AMS_INT32: + return sizeof(int32_t); + case AMS_INT64: + return sizeof(int64_t); + default: + throw std::invalid_argument("Unsupported AMSTensor dtype"); + } } -// Helper function to check if the tensor is contiguous in memory -bool AMSTensor::isContiguous(AMSTensor::IntDimType expected_stride) const + +size_t checkedAdd(size_t a, size_t b) { - for (int i = _shape.size() - 1; i >= 0; --i) { - if (_strides[i] != expected_stride) return false; - expected_stride *= _shape[i]; + if (b > std::numeric_limits::max() - a) + throw std::overflow_error("AMSTensor size addition overflow"); + return a + b; +} + +size_t checkedMul(size_t a, size_t b) +{ + if (a && b > std::numeric_limits::max() / a) + throw std::overflow_error("AMSTensor size multiplication overflow"); + return a * b; +} + +struct Metadata { + Dim elements; + size_t logicalBytes; + size_t spanElements; + size_t storageBytes; + bool contiguous; +}; + +Metadata validateMetadata(ArrayRef shape, + ArrayRef strides, + AMSDType dtype, + AMSResourceType location) +{ + if (shape.size() != strides.size()) + throw std::invalid_argument("AMSTensor shape/stride ranks differ"); + if (location != AMS_HOST && location != AMS_DEVICE && location != AMS_PINNED) + throw std::invalid_argument("Invalid AMSTensor memory resource"); + + const size_t itemSize = elementSize(dtype); + size_t elements = 1; + bool empty = false; + for (size_t i = 0; i < shape.size(); ++i) { + if (shape[i] < 0) + throw std::invalid_argument("AMSTensor dimensions cannot be negative"); + if (strides[i] <= 0) + throw std::invalid_argument("AMSTensor strides must be positive"); + if (shape[i] == 0) empty = true; + elements = checkedMul(elements, static_cast(shape[i])); + } + if (empty) elements = 0; + if (elements > static_cast(std::numeric_limits::max())) + throw std::overflow_error("AMSTensor element count exceeds IntDimType"); + + size_t span = elements == 0 ? 0 : 1; + if (elements != 0) { + std::vector order; + for (size_t i = 0; i < shape.size(); ++i) + if (shape[i] > 1) order.push_back(i); + std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { + return strides[a] < strides[b]; + }); + size_t required = 1; + for (size_t axis : order) { + const size_t stride = static_cast(strides[axis]); + if (stride < required) + throw std::invalid_argument( + "AMSTensor strides describe overlapping storage"); + required = + checkedAdd(required, + checkedMul(static_cast(shape[axis] - 1), stride)); + } + span = required; } - return true; + + bool contiguous = true; + size_t expected = 1; + for (size_t i = shape.size(); i-- > 0;) { + if (shape[i] > 1 && static_cast(strides[i]) != expected) + contiguous = false; + expected = checkedMul(expected, static_cast(shape[i])); + } + if (empty) contiguous = true; + + return {static_cast(elements), + checkedMul(elements, itemSize), + span, + checkedMul(span, itemSize), + contiguous}; } -namespace +SmallVector contiguousStrides(ArrayRef shape) { -template -constexpr AMSDType scalar_to_ams_dtype() + SmallVector result(shape.size(), 1); + size_t stride = 1; + for (size_t i = shape.size(); i-- > 0;) { + if (stride > static_cast(std::numeric_limits::max())) + throw std::overflow_error("AMSTensor stride exceeds IntDimType"); + result[i] = static_cast(stride); + stride = checkedMul(stride, static_cast(shape[i])); + } + return result; +} + +AMSTensor::LifetimeToken allocateStorage(size_t bytes, + size_t alignment, + AMSResourceType location, + uint8_t*& data) { - using U = std::remove_cv_t; - if constexpr (std::is_same_v) { - return AMS_SINGLE; - } else if constexpr (std::is_same_v) { - return AMS_DOUBLE; - } else if constexpr (std::is_same_v) { - return AMS_INT32; - } else if constexpr (std::is_same_v) { - return AMS_INT64; - } else { - static_assert(!sizeof(T), "Unsupported AMS scalar type"); + auto allocator = ResourceManager::getInstance().getAllocator(location); + const size_t allocationBytes = bytes == 0 ? alignment : bytes; + void* ptr = allocator->allocate(allocationBytes, alignment); + if (!ptr) throw std::bad_alloc(); + data = static_cast(ptr); + return AMSTensor::LifetimeToken(ptr, [allocator](void* p) { + if (p) allocator->deallocate(p); + }); +} + +size_t logicalOffset(size_t linear, ArrayRef shape, ArrayRef strides) +{ + size_t offset = 0; + for (size_t axis = shape.size(); axis-- > 0;) { + const size_t dim = static_cast(shape[axis]); + const size_t index = dim == 0 ? 0 : linear % dim; + if (dim != 0) linear /= dim; + offset = checkedAdd(offset, + checkedMul(index, static_cast(strides[axis]))); } + return offset; } } // namespace AMSTensor::AMSTensor(uint8_t* data, - ams::ArrayRef shapes, - ams::ArrayRef strides, - AMSDType dType, + ArrayRef shapes, + ArrayRef strides, + AMSDType dtype, AMSResourceType location, - bool view) + bool writable, + LifetimeToken lifetime) : _data(data), - _element_size(dtype_to_size(dType)), _shape(shapes), _strides(strides), - _dType(dType), + _dType(dtype), _location(location), - _owned(!view) + _writable(writable), + _valid(true), + _lifetime(std::move(lifetime)) { - _elements = computeNumElements(shapes); - _bytes = _elements * _element_size; - _contiguous = isContiguous(1); - if (!_data) { - throw std::runtime_error("Generating tensor with Null Pointer AMSTensor."); - } + const Metadata metadata = validateMetadata(shapes, strides, dtype, location); + _elements = metadata.elements; + _element_size = static_cast(elementSize(dtype)); + _bytes = metadata.logicalBytes; + _storage_bytes = metadata.storageBytes; + _contiguous = metadata.contiguous; + if (!_data && _elements != 0) + throw std::invalid_argument("Non-empty AMSTensor requires a data pointer"); + if (_data && + reinterpret_cast(_data) % static_cast(_element_size) != + 0) + throw std::invalid_argument("AMSTensor data pointer is improperly aligned"); +} + +void AMSTensor::requireValid() const +{ + if (!_valid) throw std::logic_error("AMSTensor is moved-from"); +} + +void AMSTensor::requireDataType(AMSDType requested) const +{ + if (_dType != requested) + throw std::invalid_argument("AMSTensor dtype mismatch"); +} + +void AMSTensor::resetMovedFrom() noexcept +{ + _data = nullptr; + _elements = 0; + _element_size = 0; + _shape.clear(); + _strides.clear(); + _dType = AMS_UNKNOWN_TYPE; + _location = AMS_UNKNOWN; + _contiguous = false; + _writable = false; + _valid = false; + _bytes = 0; + _storage_bytes = 0; + _lifetime.reset(); } template -AMSTensor AMSTensor::create(ams::ArrayRef shapes, - ams::ArrayRef strides, +AMSTensor AMSTensor::create(ArrayRef shapes, + ArrayRef strides, AMSResourceType location) { - auto numElements = computeNumElements(shapes); - auto& rm = ams::ResourceManager::getInstance(); - using U = std::remove_cv_t; - auto allocationElements = numElements == 0 ? 1 : numElements; - U* data = rm.allocate(allocationElements, location, sizeof(U)); - return AMSTensor(reinterpret_cast(data), + const Metadata metadata = + validateMetadata(shapes, strides, dtypeFor(), location); + uint8_t* data = nullptr; + auto owner = allocateStorage(metadata.storageBytes, + alignof(std::remove_cv_t), + location, + data); + return AMSTensor(data, shapes, strides, - scalar_to_ams_dtype(), - location); + dtypeFor(), + location, + true, + std::move(owner)); } - template AMSTensor AMSTensor::view(ScalarType* data, - ams::ArrayRef shapes, - ams::ArrayRef strides, + ArrayRef shapes, + ArrayRef strides, AMSResourceType location) +{ + return view(data, shapes, strides, location, {}); +} + +template +AMSTensor AMSTensor::view(ScalarType* data, + ArrayRef shapes, + ArrayRef strides, + AMSResourceType location, + LifetimeToken lifetime) { using U = std::remove_cv_t; return AMSTensor(reinterpret_cast(const_cast(data)), shapes, strides, - scalar_to_ams_dtype(), + dtypeFor(), location, - true); + !std::is_const_v, + std::move(lifetime)); } AMSTensor AMSTensor::view(AMSTensor& tensor) { - if (tensor._dType == AMS_DOUBLE) - return AMSTensor::view((double*)tensor._data, - tensor._shape, - tensor._strides, - tensor._location); - else if (tensor._dType == AMS_SINGLE) - return AMSTensor::view((float*)tensor._data, - tensor._shape, - tensor._strides, - tensor._location); - else if (tensor._dType == AMS_INT32) - return AMSTensor::view((int32_t*)tensor._data, - tensor._shape, - tensor._strides, - tensor._location); - else if (tensor._dType == AMS_INT64) - return AMSTensor::view((int64_t*)tensor._data, - tensor._shape, - tensor._strides, - tensor._location); - throw std::runtime_error( - "Creating view through copying constructor has incorrect dtype"); + tensor.requireValid(); + return AMSTensor(tensor._data, + tensor._shape, + tensor._strides, + tensor._dType, + tensor._location, + tensor._writable, + tensor._lifetime); } -AMSTensor::~AMSTensor() +AMSTensor AMSTensor::view(const AMSTensor& tensor) { - // Only release whenwe own the pointer - if (_owned && _data) { - auto& rm = ams::ResourceManager::getInstance(); - rm.deallocate(_data, _location); - _data = nullptr; - _owned = false; - } + tensor.requireValid(); + return AMSTensor(tensor._data, + tensor._shape, + tensor._strides, + tensor._dType, + tensor._location, + false, + tensor._lifetime); } AMSTensor::AMSTensor(AMSTensor&& other) noexcept @@ -154,23 +294,19 @@ AMSTensor::AMSTensor(AMSTensor&& other) noexcept _strides(std::move(other._strides)), _dType(other._dType), _location(other._location), - _owned(other._owned), _contiguous(other._contiguous), - _bytes(other._bytes) + _writable(other._writable), + _valid(other._valid), + _bytes(other._bytes), + _storage_bytes(other._storage_bytes), + _lifetime(std::move(other._lifetime)) { - other._data = nullptr; - other._owned = false; + other.resetMovedFrom(); } AMSTensor& AMSTensor::operator=(AMSTensor&& other) noexcept { if (this != &other) { - if (_owned && _data) { - auto& rm = ams::ResourceManager::getInstance(); - rm.deallocate(_data, _location); - } - - // Steal resources from `other` _data = other._data; _elements = other._elements; _element_size = other._element_size; @@ -178,89 +314,173 @@ AMSTensor& AMSTensor::operator=(AMSTensor&& other) noexcept _strides = std::move(other._strides); _dType = other._dType; _location = other._location; - _owned = other._owned; _contiguous = other._contiguous; + _writable = other._writable; + _valid = other._valid; _bytes = other._bytes; - - other._data = nullptr; - other._owned = false; + _storage_bytes = other._storage_bytes; + _lifetime = std::move(other._lifetime); + other.resetMovedFrom(); } return *this; } -AMSTensor AMSTensor::transpose(AMSTensor::IntDimType axis1, - AMSTensor::IntDimType axis2) const +AMSTensor AMSTensor::transpose(IntDimType axis1, IntDimType axis2) { - // Ensure the axes are within bounds - if (axis1 >= _shape.size() || axis2 >= _shape.size()) { - throw std::out_of_range("Transpose axes are out of bounds"); + requireValid(); + if (axis1 < 0 || axis2 < 0 || static_cast(axis1) >= _shape.size() || + static_cast(axis2) >= _shape.size()) + throw std::out_of_range("AMSTensor transpose axis is out of range"); + auto shape = _shape; + auto strides = _strides; + std::swap(shape[axis1], shape[axis2]); + std::swap(strides[axis1], strides[axis2]); + return AMSTensor( + _data, shape, strides, _dType, _location, _writable, _lifetime); +} + +AMSTensor AMSTensor::transpose(IntDimType axis1, IntDimType axis2) const +{ + requireValid(); + if (axis1 < 0 || axis2 < 0 || static_cast(axis1) >= _shape.size() || + static_cast(axis2) >= _shape.size()) + throw std::out_of_range("AMSTensor transpose axis is out of range"); + auto shape = _shape; + auto strides = _strides; + std::swap(shape[axis1], shape[axis2]); + std::swap(strides[axis1], strides[axis2]); + return AMSTensor(_data, shape, strides, _dType, _location, false, _lifetime); +} + +AMSTensor AMSTensor::clone() const +{ + requireValid(); + auto strides = contiguousStrides(_shape); + AMSTensor result = [&]() { + switch (_dType) { + case AMS_SINGLE: + return create(_shape, strides, _location); + case AMS_DOUBLE: + return create(_shape, strides, _location); + case AMS_INT32: + return create(_shape, strides, _location); + case AMS_INT64: + return create(_shape, strides, _location); + default: + throw std::invalid_argument("Unsupported AMSTensor dtype"); + } + }(); + if (_elements == 0) return result; + if (_contiguous) { + internal::_raw_copy(_data, _location, result._data, _location, _bytes); + } else { + for (size_t i = 0; i < static_cast(_elements); ++i) { + const size_t src = checkedMul(logicalOffset(i, _shape, _strides), + static_cast(_element_size)); + const size_t dst = checkedMul(i, static_cast(_element_size)); + internal::_raw_copy(_data + src, + _location, + result._data + dst, + _location, + static_cast(_element_size)); + } } + return result; +} - // Create new shape and strides for the transposed tensor - auto newShape = _shape; - auto newStrides = _strides; - - // Swap the specified axes in both shape and strides - std::swap(newShape[axis1], newShape[axis2]); - std::swap(newStrides[axis1], newStrides[axis2]); - - // Create a new tensor with the same data, new shape, and strides - if (dType() == AMSDType::AMS_DOUBLE) - return view((double*)_data, newShape, newStrides, _location); - else if (dType() == AMSDType::AMS_SINGLE) - return view((float*)_data, newShape, newStrides, _location); - else if (dType() == AMSDType::AMS_INT32) - return view((int32_t*)_data, newShape, newStrides, _location); - else if (dType() == AMSDType::AMS_INT64) - return view((int64_t*)_data, newShape, newStrides, _location); - // NOTE: Use defensive programming here and just crash. We can fix a better interface later - // for error handling. - throw std::runtime_error("Unknow data type in transpose\n"); +AMSTensor AMSTensor::concat(ArrayRef tensors, AMSDType inputDType) +{ + if (tensors.empty()) + throw std::invalid_argument("AMSTensor::concat requires input tensors"); + elementSize(inputDType); + const AMSTensor& first = tensors[0]; + first.requireValid(); + if (first._shape.empty()) + throw std::invalid_argument("Cannot concatenate scalar tensors"); + const size_t rank = first._shape.size(); + size_t last = 0; + for (const auto& tensor : tensors) { + tensor.requireValid(); + if (tensor._dType != inputDType) + throw std::invalid_argument("AMSTensor::concat dtype mismatch"); + if (tensor._location != first._location) + throw std::invalid_argument("AMSTensor::concat location mismatch"); + if (tensor._shape.size() != rank) + throw std::invalid_argument("AMSTensor::concat rank mismatch"); + for (size_t axis = 0; axis + 1 < rank; ++axis) + if (tensor._shape[axis] != first._shape[axis]) + throw std::invalid_argument("AMSTensor::concat shape mismatch"); + last = checkedAdd(last, static_cast(tensor._shape.back())); + } + if (last > static_cast(std::numeric_limits::max())) + throw std::overflow_error("AMSTensor::concat dimension overflow"); + SmallVector shape(first._shape.begin(), first._shape.end()); + shape.back() = static_cast(last); + auto strides = contiguousStrides(shape); + AMSTensor result = [&]() { + switch (inputDType) { + case AMS_SINGLE: + return create(shape, strides, first._location); + case AMS_DOUBLE: + return create(shape, strides, first._location); + case AMS_INT32: + return create(shape, strides, first._location); + case AMS_INT64: + return create(shape, strides, first._location); + default: + throw std::invalid_argument("Unsupported AMSTensor dtype"); + } + }(); + size_t rows = 1; + for (size_t axis = 0; axis + 1 < rank; ++axis) + rows = checkedMul(rows, static_cast(shape[axis])); + size_t dstLinear = 0; + for (size_t row = 0; row < rows; ++row) { + for (const auto& tensor : tensors) { + const size_t width = static_cast(tensor._shape.back()); + for (size_t col = 0; col < width; ++col) { + const size_t logical = checkedAdd(checkedMul(row, width), col); + const size_t src = + checkedMul(logicalOffset(logical, tensor._shape, tensor._strides), + static_cast(tensor._element_size)); + const size_t dst = + checkedMul(dstLinear++, static_cast(tensor._element_size)); + internal::_raw_copy(tensor._data + src, + tensor._location, + result._data + dst, + result._location, + static_cast(tensor._element_size)); + } + } + } + return result; } -template AMSTensor AMSTensor::create(ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::create(ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::create(ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::create(ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); - -template AMSTensor AMSTensor::view(float*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::view(double*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::view(int32_t*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::view(int64_t*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); - -template AMSTensor AMSTensor::view(const float*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::view(const double*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::view(const int32_t*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); -template AMSTensor AMSTensor::view(const int64_t*, - ams::ArrayRef, - ams::ArrayRef, - AMSResourceType); +#define AMS_INSTANTIATE(T) \ + template AMSTensor AMSTensor::create(ArrayRef, \ + ArrayRef, \ + AMSResourceType); \ + template AMSTensor AMSTensor::view(T*, \ + ArrayRef, \ + ArrayRef, \ + AMSResourceType); \ + template AMSTensor AMSTensor::view(const T*, \ + ArrayRef, \ + ArrayRef, \ + AMSResourceType); \ + template AMSTensor AMSTensor::view(T*, \ + ArrayRef, \ + ArrayRef, \ + AMSResourceType, \ + LifetimeToken); \ + template AMSTensor AMSTensor::view(const T*, \ + ArrayRef, \ + ArrayRef, \ + AMSResourceType, \ + LifetimeToken) + +AMS_INSTANTIATE(float); +AMS_INSTANTIATE(double); +AMS_INSTANTIATE(int32_t); +AMS_INSTANTIATE(int64_t); +#undef AMS_INSTANTIATE diff --git a/src/AMSlib/AMSTorchInterop.cpp b/src/AMSlib/AMSTorchInterop.cpp new file mode 100644 index 00000000..eb9c5515 --- /dev/null +++ b/src/AMSlib/AMSTorchInterop.cpp @@ -0,0 +1,175 @@ +#include "AMSTorchInterop.hpp" + +#include +#include + +#include "wf/resource_manager.hpp" + +namespace ams +{ +namespace +{ +AMSDType fromTorchDType(c10::ScalarType type) +{ + switch (type) { + case torch::kFloat32: + return AMS_SINGLE; + case torch::kFloat64: + return AMS_DOUBLE; + case torch::kInt32: + return AMS_INT32; + case torch::kInt64: + return AMS_INT64; + default: + throw std::invalid_argument("Unsupported Torch tensor dtype"); + } +} + +c10::ScalarType toTorchDType(AMSDType type) +{ + switch (type) { + case AMS_SINGLE: + return torch::kFloat32; + case AMS_DOUBLE: + return torch::kFloat64; + case AMS_INT32: + return torch::kInt32; + case AMS_INT64: + return torch::kInt64; + default: + throw std::invalid_argument("Unsupported AMS tensor dtype"); + } +} + +AMSResourceType fromTorchDevice(const torch::Tensor& tensor) +{ + if (tensor.device().is_cpu()) + return tensor.is_pinned() ? AMS_PINNED : AMS_HOST; + if (tensor.device().is_cuda()) return AMS_DEVICE; + throw std::invalid_argument("Unsupported Torch tensor device"); +} + +torch::Device toTorchDevice(AMSResourceType type) +{ + if (type == AMS_HOST || type == AMS_PINNED) return torch::Device(torch::kCPU); + if (type == AMS_DEVICE) return torch::Device(torch::kCUDA); + throw std::invalid_argument("Unsupported AMS tensor device"); +} + +SmallVector dims(c10::IntArrayRef values) +{ + SmallVector result; + result.reserve(values.size()); + for (int64_t value : values) + result.push_back(value); + return result; +} + +template +AMSTensor makeView(torch::Tensor tensor, + ArrayRef shape, + ArrayRef strides, + AMSResourceType location, + AMSTensor::LifetimeToken owner) +{ + return AMSTensor::view( + tensor.data_ptr(), shape, strides, location, std::move(owner)); +} +} // namespace + +AMSTensor fromTorchView(torch::Tensor tensor) +{ + if (!tensor.defined()) + throw std::invalid_argument("Torch tensor is undefined"); + const AMSDType dtype = fromTorchDType(tensor.scalar_type()); + const AMSResourceType location = fromTorchDevice(tensor); + auto shape = dims(tensor.sizes()); + auto strides = dims(tensor.strides()); + auto owner = + std::static_pointer_cast(std::make_shared(tensor)); + switch (dtype) { + case AMS_SINGLE: + return makeView( + tensor, shape, strides, location, std::move(owner)); + case AMS_DOUBLE: + return makeView( + tensor, shape, strides, location, std::move(owner)); + case AMS_INT32: + return makeView( + tensor, shape, strides, location, std::move(owner)); + case AMS_INT64: + return makeView( + tensor, shape, strides, location, std::move(owner)); + default: + throw std::invalid_argument("Unsupported Torch tensor dtype"); + } +} + +AMSTensor fromTorchCopy(const torch::Tensor& tensor) +{ + if (!tensor.defined()) + throw std::invalid_argument("Torch tensor is undefined"); + torch::Tensor source = tensor.detach().contiguous(); + const AMSDType dtype = fromTorchDType(source.scalar_type()); + const AMSResourceType location = fromTorchDevice(source); + auto shape = dims(source.sizes()); + auto strides = dims(source.strides()); + AMSTensor result = [&]() { + switch (dtype) { + case AMS_SINGLE: + return AMSTensor::create(shape, strides, location); + case AMS_DOUBLE: + return AMSTensor::create(shape, strides, location); + case AMS_INT32: + return AMSTensor::create(shape, strides, location); + case AMS_INT64: + return AMSTensor::create(shape, strides, location); + default: + throw std::invalid_argument("Unsupported Torch tensor dtype"); + } + }(); + if (result.nbytes()) + internal::_raw_copy(source.data_ptr(), + location, + result.data_ptr(), + location, + result.nbytes()); + return result; +} + +torch::Tensor toTorchView(AMSTensor& tensor) +{ + std::vector shape(tensor.shape().begin(), tensor.shape().end()); + std::vector strides(tensor.strides().begin(), + tensor.strides().end()); + auto options = torch::TensorOptions() + .dtype(toTorchDType(tensor.dtype())) + .device(toTorchDevice(tensor.location())); + auto owner = tensor.lifetimeToken(); + return torch::from_blob( + tensor.data_ptr(), + shape, + strides, + [owner = std::move(owner)](void*) mutable { owner.reset(); }, + options); +} + +torch::Tensor toTorchCopy(const AMSTensor& tensor) +{ + // Torch has no read-only tensor view, so keep this view private to the copy. + std::vector shape(tensor.shape().begin(), tensor.shape().end()); + std::vector strides(tensor.strides().begin(), + tensor.strides().end()); + auto options = torch::TensorOptions() + .dtype(toTorchDType(tensor.dtype())) + .device(toTorchDevice(tensor.location())); + auto owner = tensor.lifetimeToken(); + auto view = torch::from_blob( + const_cast(tensor.data_ptr()), + shape, + strides, + [owner = std::move(owner)](void*) mutable { owner.reset(); }, + options); + return view.clone(torch::MemoryFormat::Contiguous); +} +} // namespace ams diff --git a/src/AMSlib/CMakeLists.txt b/src/AMSlib/CMakeLists.txt index df4229ee..f56acd19 100644 --- a/src/AMSlib/CMakeLists.txt +++ b/src/AMSlib/CMakeLists.txt @@ -4,9 +4,13 @@ # ------------------------------------------------------------------------------ # handle sources and headers -set(AMS_LIB_SRC wf/debug.cpp wf/logger.cpp wf/utils.cpp wf/SmallVector.cpp ml/surrogate.cpp wf/basedb.cpp AMSTensor.cpp AMSGraph.cpp wf/interface.cpp wf/resource_manager.cpp ml/Model.cpp ml/AbstractModel.cpp AMS.cpp) +set(AMS_LIB_SRC wf/debug.cpp wf/logger.cpp wf/utils.cpp wf/SmallVector.cpp wf/basedb.cpp AMSTensor.cpp AMSGraph.cpp wf/interface.cpp wf/resource_manager.cpp AMS.cpp) - list(APPEND AMS_LIB_SRC wf/hdf5db.cpp) +if (ENABLE_TORCH) + list(APPEND AMS_LIB_SRC ml/surrogate.cpp ml/Model.cpp ml/AbstractModel.cpp AMSTorchInterop.cpp) +endif() + +list(APPEND AMS_LIB_SRC wf/hdf5db.cpp) if (ENABLE_RMQ) list(APPEND AMS_LIB_SRC wf/rmqdb.cpp) @@ -15,6 +19,16 @@ endif() blt_add_library(NAME AMS SOURCES ${AMS_LIB_SRC}) add_library(AMS::AMS ALIAS AMS) +target_compile_features(AMS PUBLIC cxx_std_17) + +if (ENABLE_CUDA OR CMAKE_CUDA_COMPILER_LOADED) + target_compile_features(AMS PUBLIC cuda_std_17) +endif() + +if (ENABLE_HIP OR CMAKE_HIP_COMPILER_LOADED) + target_compile_features(AMS PUBLIC hip_std_17) +endif() + # ------------------------------------------------------------------------------ # setup the lib first message(STATUS "ALL INCLUDES ARE ${AMS_APP_INCLUDES}") @@ -64,7 +78,18 @@ if (ENABLE_RMQ) target_link_libraries(AMS PUBLIC ${LIBEVENT_LIBRARY} ${LIBEVENT_THREAD}) endif() -target_link_libraries(AMS PRIVATE stdc++fs torch) +target_link_libraries(AMS PRIVATE stdc++fs) + +if (ENABLE_TORCH) + target_link_libraries(AMS PRIVATE torch) + add_library(AMSTorchInterop INTERFACE) + add_library(AMS::TorchInterop ALIAS AMSTorchInterop) + set_target_properties(AMSTorchInterop PROPERTIES EXPORT_NAME TorchInterop) + target_link_libraries(AMSTorchInterop INTERFACE AMS torch) + target_include_directories(AMSTorchInterop INTERFACE + $ + $) +endif() # torch adds -Wl,--as-needed to the link line which leads to issue with libstdc++ when building against AMS in static mode # TorchConfig.cmake wraps libtorch_cpu.so in -Wl,--no-as-needed and then re-enables --as-needed without restoring the prior state @@ -85,6 +110,9 @@ configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/include/SmallVector.hpp" "${PROJECT configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/include/ArrayRef.hpp" "${PROJECT_BINARY_DIR}/include/ArrayRef.hpp" COPYONLY) configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/include/AMSTensor.hpp" "${PROJECT_BINARY_DIR}/include/AMSTensor.hpp" COPYONLY) configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/include/AMSGraph.hpp" "${PROJECT_BINARY_DIR}/include/AMSGraph.hpp" COPYONLY) +if (ENABLE_TORCH) + configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/include/AMSTorchInterop.hpp" "${PROJECT_BINARY_DIR}/include/AMSTorchInterop.hpp" COPYONLY) +endif() # setup the exec #SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath -Wl,$ORIGIN") @@ -102,6 +130,9 @@ install(TARGETS AMS RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" # For executables (Windows-specific) INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" # Install headers directory ) +if (ENABLE_TORCH) + install(TARGETS AMSTorchInterop EXPORT AMSTargets) +endif() # Export the AMS targets for use by external projects export(EXPORT AMSTargets @@ -124,6 +155,10 @@ install(FILES ${PROJECT_BINARY_DIR}/include/SmallVector.hpp DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/" ) +if (ENABLE_TORCH) + install(FILES ${PROJECT_BINARY_DIR}/include/AMSTorchInterop.hpp + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/") +endif() # Generate a version file for the package diff --git a/src/AMSlib/include/AMSTensor.hpp b/src/AMSlib/include/AMSTensor.hpp index a8c483f7..2c15b1e3 100644 --- a/src/AMSlib/include/AMSTensor.hpp +++ b/src/AMSlib/include/AMSTensor.hpp @@ -1,8 +1,9 @@ #pragma once -#include #include #include +#include +#include #include #include "AMSTypes.hpp" @@ -11,149 +12,195 @@ namespace ams { - class AMSTensor { public: using IntDimType = long int; - IntDimType elements() const { return _elements; } - IntDimType element_size() const { return _element_size; } - AMSDType dType() const { return _dType; } - AMSResourceType location() const { return _location; } - ams::ArrayRef strides() const { return _strides; } - ams::ArrayRef shape() const { return _shape; } - bool contiguous() const { return _contiguous; } - + using LifetimeToken = std::shared_ptr; private: - uint8_t* _data; - IntDimType _elements; - IntDimType _element_size; - ams::SmallVector _shape; - ams::SmallVector _strides; - AMSDType _dType; // AMS_SINGLE/AMS_DOUBLE - AMSResourceType _location; // CPU/GPU/Pinned - bool _owned; - bool _contiguous; - bool _bytes; - - // Helper function to check if the tensor is contiguous in memory - bool isContiguous(IntDimType expected_stride) const; - - /** - * @brief Constructs a new AMSTensor with the specified shape, strides, data type, and location. - * This constructor is private and intended for internal use, such as creating views. - * @param[in] shapes The shape of the tensor. - * @param[in] strides The strides of the tensor. - * @param[in] dType The data type of the tensor elements. - * @param[in] location The memory location (e.g., CPU, GPU). - * @param[in] view Set to true if this tensor is a view of another tensor (non-owning). - */ - explicit AMSTensor(uint8_t* data, - ams::ArrayRef shapes, - ams::ArrayRef strides, - AMSDType dType, - AMSResourceType location, - bool view = false); + uint8_t* _data = nullptr; + IntDimType _elements = 0; + IntDimType _element_size = 0; + SmallVector _shape; + SmallVector _strides; + AMSDType _dType = AMS_UNKNOWN_TYPE; + AMSResourceType _location = AMS_UNKNOWN; + bool _contiguous = false; + bool _writable = false; + bool _valid = false; + size_t _bytes = 0; + size_t _storage_bytes = 0; + LifetimeToken _lifetime; + + AMSTensor(uint8_t* data, + ArrayRef shapes, + ArrayRef strides, + AMSDType dType, + AMSResourceType location, + bool writable, + LifetimeToken lifetime = {}); + void requireValid() const; + void requireDataType(AMSDType requested) const; + void resetMovedFrom() noexcept; + template + static constexpr AMSDType dtypeFor() + { + using U = std::remove_cv_t; + static_assert(std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v, + "Unsupported AMS scalar type"); + if constexpr (std::is_same_v) return AMS_SINGLE; + if constexpr (std::is_same_v) return AMS_DOUBLE; + if constexpr (std::is_same_v) return AMS_INT32; + return AMS_INT64; + } public: - /** - * @brief Creates a new AMSTensor and allocates the tensor memory. - * @param[in] shapes The shape of the tensor. - * @param[in] strides The strides of the tensor. - * @param[in] dType The data type of the tensor elements. - * @param[in] location The memory location (e.g., CPU, GPU). - * @return A new AMSTensor with allocated memory. - */ template - static AMSTensor create(ams::ArrayRef shapes, - ams::ArrayRef strides, + static AMSTensor create(ArrayRef shapes, + ArrayRef strides, AMSResourceType location); - /** - * @brief Creates a view on an existing memory buffer. - * @param[in] data Pointer to the existing data to be viewed. - * @param[in] shapes The shape of the view tensor. - * @param[in] strides The strides of the view tensor. - * @param[in] dType The data type of the tensor elements. - * @param[in] location The memory location (e.g., CPU, GPU). - * @return A new AMSTensor that acts as a view of the existing data. - */ + /** Borrow memory. The caller remains responsible for its lifetime/capacity. */ template static AMSTensor view(ScalarType* data, - ams::ArrayRef shapes, - ams::ArrayRef strides, + ArrayRef shapes, + ArrayRef strides, AMSResourceType location); + /** View foreign memory and retain the supplied foreign-storage owner. */ + template + static AMSTensor view(ScalarType* data, + ArrayRef shapes, + ArrayRef strides, + AMSResourceType location, + LifetimeToken lifetime); static AMSTensor view(AMSTensor& tensor); + static AMSTensor view(const AMSTensor& tensor); - /** - * @brief Destructor for AMSTensor, deallocates memory if this tensor owns it. - */ - ~AMSTensor(); - - - /** - * @brief Deleted copy assignment operator to prevent copying of tensors. - */ + ~AMSTensor() = default; AMSTensor(const AMSTensor&) = delete; - - /** - * @brief Move constructor for AMSTensor, transfers ownership of data. - * @param[in,out] other The tensor to move from. It will be left in a valid but unspecified state. - */ AMSTensor& operator=(const AMSTensor&) = delete; - - /** - * @brief Move assignment operator for AMSTensor, transfers ownership of data. - * @param[in,out] other The tensor to move from. It will be left in a valid but unspecified state. - * @return A reference to the updated tensor after move assignment. - */ AMSTensor(AMSTensor&& other) noexcept; - - // Define move assignment operator AMSTensor& operator=(AMSTensor&& other) noexcept; - /** - * @brief Retrieves a typed pointer to the underlying data. - * @tparam T The data type to retrieve. - * @return A typed pointer to the tensor's data. - */ + IntDimType elements() const + { + requireValid(); + return _elements; + } + IntDimType element_size() const + { + requireValid(); + return _element_size; + } + size_t nbytes() const + { + requireValid(); + return _bytes; + } + size_t storage_nbytes() const + { + requireValid(); + return _storage_bytes; + } + size_t dim() const + { + requireValid(); + return _shape.size(); + } + AMSDType dtype() const + { + requireValid(); + return _dType; + } + AMSResourceType location() const + { + requireValid(); + return _location; + } + ArrayRef strides() const + { + requireValid(); + return _strides; + } + ArrayRef shape() const + { + requireValid(); + return _shape; + } + ArrayRef sizes() const { return shape(); } + bool contiguous() const + { + requireValid(); + return _contiguous; + } + bool writable() const + { + requireValid(); + return _writable; + } + bool valid() const noexcept { return _valid; } + LifetimeToken lifetimeToken() const + { + requireValid(); + return _lifetime; + } + template - T* data() const + T* data() { + requireValid(); + requireDataType(dtypeFor()); + if constexpr (!std::is_const_v) { + if (!_writable) throw std::logic_error("AMSTensor is read-only"); + } return reinterpret_cast(_data); } - void* raw_data() const { return reinterpret_cast(_data); } + template + const std::remove_cv_t* data() const + { + requireValid(); + requireDataType(dtypeFor()); + return reinterpret_cast*>(_data); + } + + void* data_ptr() + { + requireValid(); + if (!_writable) throw std::logic_error("AMSTensor is read-only"); + return _data; + } + const void* data_ptr() const + { + requireValid(); + return _data; + } - /** - * @brief Creates a transposed view of the tensor by swapping two specified axes. - * @param[in] axis1 The first axis to swap in the transposition. - * @param[in] axis2 The second axis to swap in the transposition. - * @return A new AMSTensor that is a transposed view of the original tensor. - * @throw std::out_of_range if any axis is out of bounds. - */ + AMSTensor transpose(IntDimType axis1 = 0, IntDimType axis2 = 1); AMSTensor transpose(IntDimType axis1 = 0, IntDimType axis2 = 1) const; + AMSTensor clone() const; + static AMSTensor concat(ArrayRef tensors, AMSDType inputDType); }; -// Explicit instantiation declarations extern template AMSTensor AMSTensor::create( - ams::ArrayRef shapes, - ams::ArrayRef strides, - AMSResourceType location); + ArrayRef, + ArrayRef, + AMSResourceType); extern template AMSTensor AMSTensor::create( - ams::ArrayRef shapes, - ams::ArrayRef strides, - AMSResourceType location); + ArrayRef, + ArrayRef, + AMSResourceType); extern template AMSTensor AMSTensor::create( - ams::ArrayRef shapes, - ams::ArrayRef strides, - AMSResourceType location); + ArrayRef, + ArrayRef, + AMSResourceType); extern template AMSTensor AMSTensor::create( - ams::ArrayRef shapes, - ams::ArrayRef strides, - AMSResourceType location); + ArrayRef, + ArrayRef, + AMSResourceType); } // namespace ams diff --git a/src/AMSlib/include/AMSTorchInterop.hpp b/src/AMSlib/include/AMSTorchInterop.hpp new file mode 100644 index 00000000..3dcf6e00 --- /dev/null +++ b/src/AMSlib/include/AMSTorchInterop.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "AMSTensor.hpp" + +namespace ams +{ +/** Zero-copy view retaining the Torch tensor's storage owner. */ +AMSTensor fromTorchView(torch::Tensor tensor); + +/** Independent, contiguous AMS-managed copy. */ +AMSTensor fromTorchCopy(const torch::Tensor& tensor); + +/** Zero-copy mutable Torch view retaining AMS-managed storage when present. */ +torch::Tensor toTorchView(AMSTensor& tensor); + +/** Independent, contiguous Torch-managed copy. */ +torch::Tensor toTorchCopy(const AMSTensor& tensor); +} // namespace ams diff --git a/src/AMSlib/include/SmallVector.hpp b/src/AMSlib/include/SmallVector.hpp index 685b0f37..52803bf3 100644 --- a/src/AMSlib/include/SmallVector.hpp +++ b/src/AMSlib/include/SmallVector.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -59,7 +60,7 @@ template class SmallVectorBase { protected: - void *BeginX; + void* BeginX; Size_T Size = 0, Capacity; /// The maximum value of the Size_T used. @@ -69,7 +70,7 @@ class SmallVectorBase } SmallVectorBase() = delete; - SmallVectorBase(void *FirstEl, size_t TotalCapacity) + SmallVectorBase(void* FirstEl, size_t TotalCapacity) : BeginX(FirstEl), Capacity(static_cast(TotalCapacity)) { } @@ -77,15 +78,15 @@ class SmallVectorBase /// This is a helper for \a grow() that's out of line to reduce code /// duplication. This function will report a fatal error if it can't grow at /// least to \p MinSize. - void *mallocForGrow(void *FirstEl, + void* mallocForGrow(void* FirstEl, size_t MinSize, size_t TSize, - size_t &NewCapacity); + size_t& NewCapacity); /// This is an implementation of the grow() method which only works /// on POD-like data types and is out of line to reduce code duplication. /// This function will report a fatal error if it cannot increase capacity. - void grow_pod(void *FirstEl, size_t MinSize, size_t TSize); + void grow_pod(void* FirstEl, size_t MinSize, size_t TSize); /// If vector was first created with capacity 0, getFirstEl() points to the /// memory right after, an area unallocated. If a subsequent allocation, @@ -97,7 +98,7 @@ class SmallVectorBase /// space, and happens to allocate precisely at BeginX. /// This is unlikely to be called often, but resolves a memory leak when the /// situation does occur. - void *replaceAllocation(void *NewElts, + void* replaceAllocation(void* NewElts, size_t TSize, size_t NewCapacity, size_t VSize = 0); @@ -123,7 +124,7 @@ class SmallVectorBase /// /// This does not construct or destroy any elements in the vector. // This does not clean up any existing allocation. - void set_allocation_range(void *Begin, size_t N) + void set_allocation_range(void* Begin, size_t N) { assert(N <= SizeTypeMax()); BeginX = Begin; @@ -132,8 +133,8 @@ class SmallVectorBase }; template -using SmallVectorSizeType = std:: - conditional_t= 8, uint64_t, uint32_t>; +using SmallVectorSizeType = + std::conditional_t= 8, uint64_t, uint32_t>; /// Figure out the offset of the first element. template @@ -155,10 +156,10 @@ class SmallVectorTemplateCommon : public SmallVectorBase> /// Find the address of the first element. For this pointer math to be valid /// with small-size of 0 for T with lots of alignment, it's important that /// SmallVectorStorage is properly-aligned even for small-size of 0. - void *getFirstEl() const + void* getFirstEl() const { - return const_cast(reinterpret_cast( - reinterpret_cast(this) + + return const_cast(reinterpret_cast( + reinterpret_cast(this) + offsetof(SmallVectorAlignmentAndSize, FirstEl))); } // Space after 'FirstEl' is clobbered, do not add any instance vars after it. @@ -183,9 +184,9 @@ class SmallVectorTemplateCommon : public SmallVectorBase> } /// Return true if V is an internal reference to the given range. - bool isReferenceToRange(const void *V, - const void *First, - const void *Last) const + bool isReferenceToRange(const void* V, + const void* First, + const void* Last) const { // Use std::less to avoid UB. std::less<> LessThan; @@ -193,14 +194,14 @@ class SmallVectorTemplateCommon : public SmallVectorBase> } /// Return true if V is an internal reference to this vector. - bool isReferenceToStorage(const void *V) const + bool isReferenceToStorage(const void* V) const { return isReferenceToRange(V, this->begin(), this->end()); } /// Return true if First and Last form a valid (possibly empty) range in this /// vector's storage. - bool isRangeInStorage(const void *First, const void *Last) const + bool isRangeInStorage(const void* First, const void* Last) const { // Use std::less to avoid UB. std::less<> LessThan; @@ -210,7 +211,7 @@ class SmallVectorTemplateCommon : public SmallVectorBase> /// Return true unless Elt will be invalidated by resizing the vector to /// NewSize. - bool isSafeToReferenceAfterResize(const void *Elt, size_t NewSize) + bool isSafeToReferenceAfterResize(const void* Elt, size_t NewSize) { // Past the end. if (!isReferenceToStorage(Elt)) return true; @@ -223,7 +224,7 @@ class SmallVectorTemplateCommon : public SmallVectorBase> } /// Check whether Elt will be invalidated by resizing the vector to NewSize. - void assertSafeToReferenceAfterResize(const void *Elt, size_t NewSize) + void assertSafeToReferenceAfterResize(const void* Elt, size_t NewSize) { assert(isSafeToReferenceAfterResize(Elt, NewSize) && "Attempting to reference an element of the vector in an operation " @@ -232,13 +233,13 @@ class SmallVectorTemplateCommon : public SmallVectorBase> /// Check whether Elt will be invalidated by increasing the size of the /// vector by N. - void assertSafeToAdd(const void *Elt, size_t N = 1) + void assertSafeToAdd(const void* Elt, size_t N = 1) { this->assertSafeToReferenceAfterResize(Elt, this->size() + N); } /// Check whether any part of the range will be invalidated by clearing. - void assertSafeToReferenceAfterClear(const T *From, const T *To) + void assertSafeToReferenceAfterClear(const T* From, const T* To) { if (From == To) return; this->assertSafeToReferenceAfterResize(From, 0); @@ -246,14 +247,14 @@ class SmallVectorTemplateCommon : public SmallVectorBase> } template < class ItTy, - std::enable_if_t, T *>::value, + std::enable_if_t, T*>::value, bool> = false> void assertSafeToReferenceAfterClear(ItTy, ItTy) { } /// Check whether any part of the range will be invalidated by growing. - void assertSafeToAddRange(const T *From, const T *To) + void assertSafeToAddRange(const T* From, const T* To) { if (From == To) return; this->assertSafeToAdd(From, To - From); @@ -261,7 +262,7 @@ class SmallVectorTemplateCommon : public SmallVectorBase> } template < class ItTy, - std::enable_if_t, T *>::value, + std::enable_if_t, T*>::value, bool> = false> void assertSafeToAddRange(ItTy, ItTy) { @@ -270,8 +271,8 @@ class SmallVectorTemplateCommon : public SmallVectorBase> /// Reserve enough space to add one element, and return the updated element /// pointer in case it was a reference to the storage. template - static const T *reserveForParamAndGetAddressImpl(U *This, - const T &Elt, + static const T* reserveForParamAndGetAddressImpl(U* This, + const T& Elt, size_t N) { size_t NewSize = This->size() + N; @@ -293,16 +294,16 @@ class SmallVectorTemplateCommon : public SmallVectorBase> using size_type = size_t; using difference_type = ptrdiff_t; using value_type = T; - using iterator = T *; - using const_iterator = const T *; + using iterator = T*; + using const_iterator = const T*; using const_reverse_iterator = std::reverse_iterator; using reverse_iterator = std::reverse_iterator; - using reference = T &; - using const_reference = const T &; - using pointer = T *; - using const_pointer = const T *; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; using Base::capacity; using Base::empty; @@ -391,11 +392,11 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon protected: static constexpr bool TakesParamByValue = false; - using ValueParamT = const T &; + using ValueParamT = const T&; SmallVectorTemplateBase(size_t Size) : SmallVectorTemplateCommon(Size) {} - static void destroy_range(T *S, T *E) + static void destroy_range(T* S, T* E) { while (S != E) { --E; @@ -430,38 +431,37 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon /// Create a new allocation big enough for \p MinSize and pass back its size /// in \p NewCapacity. This is the first section of \a grow(). - T *mallocForGrow(size_t MinSize, size_t &NewCapacity); + T* mallocForGrow(size_t MinSize, size_t& NewCapacity); /// Move existing elements over to the new allocation \p NewElts, the middle /// section of \a grow(). - void moveElementsForGrow(T *NewElts); + void moveElementsForGrow(T* NewElts); /// Transfer ownership of the allocation, finishing up \a grow(). - void takeAllocationForGrow(T *NewElts, size_t NewCapacity); + void takeAllocationForGrow(T* NewElts, size_t NewCapacity); /// Reserve enough space to add one element, and return the updated element /// pointer in case it was a reference to the storage. - const T *reserveForParamAndGetAddress(const T &Elt, size_t N = 1) + const T* reserveForParamAndGetAddress(const T& Elt, size_t N = 1) { return this->reserveForParamAndGetAddressImpl(this, Elt, N); } /// Reserve enough space to add one element, and return the updated element /// pointer in case it was a reference to the storage. - T *reserveForParamAndGetAddress(T &Elt, size_t N = 1) + T* reserveForParamAndGetAddress(T& Elt, size_t N = 1) { - return const_cast( - this->reserveForParamAndGetAddressImpl(this, Elt, N)); + return const_cast(this->reserveForParamAndGetAddressImpl(this, Elt, N)); } - static T &&forward_value_param(T &&V) { return std::move(V); } - static const T &forward_value_param(const T &V) { return V; } + static T&& forward_value_param(T&& V) { return std::move(V); } + static const T& forward_value_param(const T& V) { return V; } - void growAndAssign(size_t NumElts, const T &Elt) + void growAndAssign(size_t NumElts, const T& Elt) { // Grow manually in case Elt is an internal reference. size_t NewCapacity; - T *NewElts = mallocForGrow(NumElts, NewCapacity); + T* NewElts = mallocForGrow(NumElts, NewCapacity); std::uninitialized_fill_n(NewElts, NumElts, Elt); this->destroy_range(this->begin(), this->end()); takeAllocationForGrow(NewElts, NewCapacity); @@ -469,12 +469,12 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon } template - T &growAndEmplaceBack(ArgTypes &&...Args) + T& growAndEmplaceBack(ArgTypes&&... Args) { // Grow manually in case one of Args is an internal reference. size_t NewCapacity; - T *NewElts = mallocForGrow(0, NewCapacity); - ::new ((void *)(NewElts + this->size())) T(std::forward(Args)...); + T* NewElts = mallocForGrow(0, NewCapacity); + ::new ((void*)(NewElts + this->size())) T(std::forward(Args)...); moveElementsForGrow(NewElts); takeAllocationForGrow(NewElts, NewCapacity); this->set_size(this->size() + 1); @@ -482,17 +482,17 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon } public: - void push_back(const T &Elt) + void push_back(const T& Elt) { - const T *EltPtr = reserveForParamAndGetAddress(Elt); - ::new ((void *)this->end()) T(*EltPtr); + const T* EltPtr = reserveForParamAndGetAddress(Elt); + ::new ((void*)this->end()) T(*EltPtr); this->set_size(this->size() + 1); } - void push_back(T &&Elt) + void push_back(T&& Elt) { - T *EltPtr = reserveForParamAndGetAddress(Elt); - ::new ((void *)this->end()) T(::std::move(*EltPtr)); + T* EltPtr = reserveForParamAndGetAddress(Elt); + ::new ((void*)this->end()) T(::std::move(*EltPtr)); this->set_size(this->size() + 1); } @@ -508,25 +508,24 @@ template void SmallVectorTemplateBase::grow(size_t MinSize) { size_t NewCapacity; - T *NewElts = mallocForGrow(MinSize, NewCapacity); + T* NewElts = mallocForGrow(MinSize, NewCapacity); moveElementsForGrow(NewElts); takeAllocationForGrow(NewElts, NewCapacity); } template -T *SmallVectorTemplateBase::mallocForGrow( +T* SmallVectorTemplateBase::mallocForGrow( size_t MinSize, - size_t &NewCapacity) + size_t& NewCapacity) { - return static_cast( - SmallVectorBase>::mallocForGrow( - this->getFirstEl(), MinSize, sizeof(T), NewCapacity)); + return static_cast(SmallVectorBase>::mallocForGrow( + this->getFirstEl(), MinSize, sizeof(T), NewCapacity)); } // Define this out-of-line to dissuade the C++ compiler from inlining it. template void SmallVectorTemplateBase::moveElementsForGrow( - T *NewElts) + T* NewElts) { // Move the elements over. this->uninitialized_move(this->begin(), this->end(), NewElts); @@ -538,7 +537,7 @@ void SmallVectorTemplateBase::moveElementsForGrow( // Define this out-of-line to dissuade the C++ compiler from inlining it. template void SmallVectorTemplateBase::takeAllocationForGrow( - T *NewElts, + T* NewElts, size_t NewCapacity) { // If this wasn't grown from the inline copy, deallocate the old space. @@ -559,16 +558,16 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon protected: /// True if it's cheap enough to take parameters by value. Doing so avoids /// overhead related to mitigations for reference invalidation. - static constexpr bool TakesParamByValue = sizeof(T) <= 2 * sizeof(void *); + static constexpr bool TakesParamByValue = sizeof(T) <= 2 * sizeof(void*); /// Either const T& or T, depending on whether it's cheap enough to take /// parameters by value. - using ValueParamT = std::conditional_t; + using ValueParamT = std::conditional_t; SmallVectorTemplateBase(size_t Size) : SmallVectorTemplateCommon(Size) {} // No need to do a destroy loop for POD's. - static void destroy_range(T *, T *) {} + static void destroy_range(T*, T*) {} /// Move the range [I, E) onto the uninitialized memory /// starting with "Dest", constructing elements into it as needed. @@ -592,17 +591,17 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon /// starting with "Dest", constructing elements into it as needed. template static void uninitialized_copy( - T1 *I, - T1 *E, - T2 *Dest, - std::enable_if_t, T2>::value> * = + T1* I, + T1* E, + T2* Dest, + std::enable_if_t, T2>::value>* = nullptr) { // Use memcpy for PODs iterated by pointers (which includes SmallVector // iterators): std::uninitialized_copy optimizes to memmove, but we can // use memcpy here. Note that I and E are iterators and thus might be // invalid for memcpy if they are equal. - if (I != E) memcpy(reinterpret_cast(Dest), I, (E - I) * sizeof(T)); + if (I != E) memcpy(reinterpret_cast(Dest), I, (E - I) * sizeof(T)); } /// Double the size of the allocated memory, guaranteeing space for at @@ -611,17 +610,16 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon /// Reserve enough space to add one element, and return the updated element /// pointer in case it was a reference to the storage. - const T *reserveForParamAndGetAddress(const T &Elt, size_t N = 1) + const T* reserveForParamAndGetAddress(const T& Elt, size_t N = 1) { return this->reserveForParamAndGetAddressImpl(this, Elt, N); } /// Reserve enough space to add one element, and return the updated element /// pointer in case it was a reference to the storage. - T *reserveForParamAndGetAddress(T &Elt, size_t N = 1) + T* reserveForParamAndGetAddress(T& Elt, size_t N = 1) { - return const_cast( - this->reserveForParamAndGetAddressImpl(this, Elt, N)); + return const_cast(this->reserveForParamAndGetAddressImpl(this, Elt, N)); } /// Copy \p V or return a reference, depending on \a ValueParamT. @@ -638,7 +636,7 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon } template - T &growAndEmplaceBack(ArgTypes &&...Args) + T& growAndEmplaceBack(ArgTypes&&... Args) { // Use push_back with a copy in case Args has an internal reference, // side-stepping reference invalidation problems without losing the realloc @@ -650,8 +648,8 @@ class SmallVectorTemplateBase : public SmallVectorTemplateCommon public: void push_back(ValueParamT Elt) { - const T *EltPtr = reserveForParamAndGetAddress(Elt); - memcpy(reinterpret_cast(this->end()), EltPtr, sizeof(T)); + const T* EltPtr = reserveForParamAndGetAddress(Elt); + memcpy(reinterpret_cast(this->end()), EltPtr, sizeof(T)); this->set_size(this->size() + 1); } @@ -678,7 +676,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase // Default ctor - Initialize to empty. explicit SmallVectorImpl(unsigned N) : SmallVectorTemplateBase(N) {} - void assignRemote(SmallVectorImpl &&RHS) + void assignRemote(SmallVectorImpl&& RHS) { this->destroy_range(this->begin(), this->end()); if (!this->isSmall()) free(this->begin()); @@ -696,7 +694,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase } public: - SmallVectorImpl(const SmallVectorImpl &) = delete; + SmallVectorImpl(const SmallVectorImpl&) = delete; void clear() { @@ -772,7 +770,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase return Result; } - void swap(SmallVectorImpl &RHS); + void swap(SmallVectorImpl& RHS); /// Add the specified range to the end of the SmallVector. template > @@ -788,14 +786,14 @@ class SmallVectorImpl : public SmallVectorTemplateBase /// Append \p NumInputs copies of \p Elt to the end. void append(size_type NumInputs, ValueParamT Elt) { - const T *EltPtr = this->reserveForParamAndGetAddress(Elt, NumInputs); + const T* EltPtr = this->reserveForParamAndGetAddress(Elt, NumInputs); std::uninitialized_fill_n(this->end(), NumInputs, *EltPtr); this->set_size(this->size() + NumInputs); } void append(std::initializer_list IL) { append(IL.begin(), IL.end()); } - void append(const SmallVectorImpl &RHS) { append(RHS.begin(), RHS.end()); } + void append(const SmallVectorImpl& RHS) { append(RHS.begin(), RHS.end()); } void assign(size_type NumElts, ValueParamT Elt) { @@ -831,7 +829,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase append(IL); } - void assign(const SmallVectorImpl &RHS) { assign(RHS.begin(), RHS.end()); } + void assign(const SmallVectorImpl& RHS) { assign(RHS.begin(), RHS.end()); } iterator erase(const_iterator CI) { @@ -868,7 +866,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase private: template - iterator insert_one_impl(iterator I, ArgType &&Elt) + iterator insert_one_impl(iterator I, ArgType&& Elt) { // Callers ensure that ArgType is derived from T. static_assert( @@ -886,11 +884,11 @@ class SmallVectorImpl : public SmallVectorTemplateBase // Grow if necessary. size_t Index = I - this->begin(); - std::remove_reference_t *EltPtr = + std::remove_reference_t* EltPtr = this->reserveForParamAndGetAddress(Elt); I = this->begin() + Index; - ::new ((void *)this->end()) T(::std::move(this->back())); + ::new ((void*)this->end()) T(::std::move(this->back())); // Push everything else over. std::move_backward(I, this->end() - 1, this->end()); this->set_size(this->size() + 1); @@ -907,12 +905,12 @@ class SmallVectorImpl : public SmallVectorTemplateBase } public: - iterator insert(iterator I, T &&Elt) + iterator insert(iterator I, T&& Elt) { return insert_one_impl(I, this->forward_value_param(std::move(Elt))); } - iterator insert(iterator I, const T &Elt) + iterator insert(iterator I, const T& Elt) { return insert_one_impl(I, this->forward_value_param(Elt)); } @@ -932,7 +930,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase // Ensure there is enough space, and get the (maybe updated) address of // Elt. - const T *EltPtr = this->reserveForParamAndGetAddress(Elt, NumToInsert); + const T* EltPtr = this->reserveForParamAndGetAddress(Elt, NumToInsert); // Uninvalidate the iterator. I = this->begin() + InsertElt; @@ -942,7 +940,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase // insertion. Since we already reserved space, we know that this won't // reallocate the vector. if (size_t(this->end() - I) >= NumToInsert) { - T *OldEnd = this->end(); + T* OldEnd = this->end(); append(std::move_iterator(this->end() - NumToInsert), std::move_iterator(this->end())); @@ -962,7 +960,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase // not inserting at the end. // Move over the elements that we're about to overwrite. - T *OldEnd = this->end(); + T* OldEnd = this->end(); this->set_size(this->size() + NumToInsert); size_t NumOverwritten = OldEnd - I; this->uninitialized_move(I, OldEnd, this->end() - NumOverwritten); @@ -1010,7 +1008,7 @@ class SmallVectorImpl : public SmallVectorTemplateBase // insertion. Since we already reserved space, we know that this won't // reallocate the vector. if (size_t(this->end() - I) >= NumToInsert) { - T *OldEnd = this->end(); + T* OldEnd = this->end(); append(std::move_iterator(this->end() - NumToInsert), std::move_iterator(this->end())); @@ -1025,13 +1023,13 @@ class SmallVectorImpl : public SmallVectorTemplateBase // not inserting at the end. // Move over the elements that we're about to overwrite. - T *OldEnd = this->end(); + T* OldEnd = this->end(); this->set_size(this->size() + NumToInsert); size_t NumOverwritten = OldEnd - I; this->uninitialized_move(I, OldEnd, this->end() - NumOverwritten); // Replace the overwritten part. - for (T *J = I; NumOverwritten > 0; --NumOverwritten) { + for (T* J = I; NumOverwritten > 0; --NumOverwritten) { *J = *From; ++J; ++From; @@ -1048,41 +1046,41 @@ class SmallVectorImpl : public SmallVectorTemplateBase } template - reference emplace_back(ArgTypes &&...Args) + reference emplace_back(ArgTypes&&... Args) { if (this->size() >= this->capacity()) return this->growAndEmplaceBack(std::forward(Args)...); - ::new ((void *)this->end()) T(std::forward(Args)...); + ::new ((void*)this->end()) T(std::forward(Args)...); this->set_size(this->size() + 1); return this->back(); } - SmallVectorImpl &operator=(const SmallVectorImpl &RHS); + SmallVectorImpl& operator=(const SmallVectorImpl& RHS); - SmallVectorImpl &operator=(SmallVectorImpl &&RHS); + SmallVectorImpl& operator=(SmallVectorImpl&& RHS); - bool operator==(const SmallVectorImpl &RHS) const + bool operator==(const SmallVectorImpl& RHS) const { if (this->size() != RHS.size()) return false; return std::equal(this->begin(), this->end(), RHS.begin()); } - bool operator!=(const SmallVectorImpl &RHS) const { return !(*this == RHS); } + bool operator!=(const SmallVectorImpl& RHS) const { return !(*this == RHS); } - bool operator<(const SmallVectorImpl &RHS) const + bool operator<(const SmallVectorImpl& RHS) const { return std::lexicographical_compare(this->begin(), this->end(), RHS.begin(), RHS.end()); } - bool operator>(const SmallVectorImpl &RHS) const { return RHS < *this; } - bool operator<=(const SmallVectorImpl &RHS) const { return !(*this > RHS); } - bool operator>=(const SmallVectorImpl &RHS) const { return !(*this < RHS); } + bool operator>(const SmallVectorImpl& RHS) const { return RHS < *this; } + bool operator<=(const SmallVectorImpl& RHS) const { return !(*this > RHS); } + bool operator>=(const SmallVectorImpl& RHS) const { return !(*this < RHS); } }; template -void SmallVectorImpl::swap(SmallVectorImpl &RHS) +void SmallVectorImpl::swap(SmallVectorImpl& RHS) { if (this == &RHS) return; @@ -1119,7 +1117,7 @@ void SmallVectorImpl::swap(SmallVectorImpl &RHS) } template -SmallVectorImpl &SmallVectorImpl::operator=(const SmallVectorImpl &RHS) +SmallVectorImpl& SmallVectorImpl::operator=(const SmallVectorImpl& RHS) { // Avoid self-assignment. if (this == &RHS) return *this; @@ -1168,7 +1166,7 @@ SmallVectorImpl &SmallVectorImpl::operator=(const SmallVectorImpl &RHS) } template -SmallVectorImpl &SmallVectorImpl::operator=(SmallVectorImpl &&RHS) +SmallVectorImpl& SmallVectorImpl::operator=(SmallVectorImpl&& RHS) { // Avoid self-assignment. if (this == &RHS) return *this; @@ -1333,7 +1331,7 @@ class SmallVector : public SmallVectorImpl, SmallVectorStorage this->resize(Size); } - SmallVector(size_t Size, const T &Value) : SmallVectorImpl(N) + SmallVector(size_t Size, const T& Value) : SmallVectorImpl(N) { this->assign(Size, Value); } @@ -1345,7 +1343,7 @@ class SmallVector : public SmallVectorImpl, SmallVectorStorage } template - explicit SmallVector(const iterator_range &R) : SmallVectorImpl(N) + explicit SmallVector(const iterator_range& R) : SmallVectorImpl(N) { this->append(R.begin(), R.end()); } @@ -1362,28 +1360,28 @@ class SmallVector : public SmallVectorImpl, SmallVectorStorage this->append(A.begin(), A.end()); } - SmallVector(const SmallVector &RHS) : SmallVectorImpl(N) + SmallVector(const SmallVector& RHS) : SmallVectorImpl(N) { if (!RHS.empty()) SmallVectorImpl::operator=(RHS); } - SmallVector &operator=(const SmallVector &RHS) + SmallVector& operator=(const SmallVector& RHS) { SmallVectorImpl::operator=(RHS); return *this; } - SmallVector(SmallVector &&RHS) : SmallVectorImpl(N) + SmallVector(SmallVector&& RHS) : SmallVectorImpl(N) { if (!RHS.empty()) SmallVectorImpl::operator=(::std::move(RHS)); } - SmallVector(SmallVectorImpl &&RHS) : SmallVectorImpl(N) + SmallVector(SmallVectorImpl&& RHS) : SmallVectorImpl(N) { if (!RHS.empty()) SmallVectorImpl::operator=(::std::move(RHS)); } - SmallVector &operator=(SmallVector &&RHS) + SmallVector& operator=(SmallVector&& RHS) { if (N) { SmallVectorImpl::operator=(::std::move(RHS)); @@ -1401,13 +1399,13 @@ class SmallVector : public SmallVectorImpl, SmallVectorStorage return *this; } - SmallVector &operator=(SmallVectorImpl &&RHS) + SmallVector& operator=(SmallVectorImpl&& RHS) { SmallVectorImpl::operator=(::std::move(RHS)); return *this; } - SmallVector &operator=(std::initializer_list IL) + SmallVector& operator=(std::initializer_list IL) { this->assign(IL); return *this; @@ -1415,13 +1413,13 @@ class SmallVector : public SmallVectorImpl, SmallVectorStorage }; template -inline size_t capacity_in_bytes(const SmallVector &X) +inline size_t capacity_in_bytes(const SmallVector& X) { return X.capacity_in_bytes(); } template -std::ostream &operator<<(std::ostream &out, const SmallVector &list) +std::ostream& operator<<(std::ostream& out, const SmallVector& list) { int i = 0; out << "["; @@ -1434,32 +1432,31 @@ std::ostream &operator<<(std::ostream &out, const SmallVector &list) } template -using ValueTypeFromRangeType = - std::remove_const_t()))>>; +using ValueTypeFromRangeType = std::remove_const_t< + std::remove_reference_t()))>>; /// Given a range of type R, iterate the entire range and return a /// SmallVector with elements of the vector. This is useful, for example, /// when you want to iterate a range and then sort the results. template -SmallVector, Size> to_vector(R &&Range) +SmallVector, Size> to_vector(R&& Range) { return {std::begin(Range), std::end(Range)}; } template -SmallVector> to_vector(R &&Range) +SmallVector> to_vector(R&& Range) { return {std::begin(Range), std::end(Range)}; } template -SmallVector to_vector_of(R &&Range) +SmallVector to_vector_of(R&& Range) { return {std::begin(Range), std::end(Range)}; } template -SmallVector to_vector_of(R &&Range) +SmallVector to_vector_of(R&& Range) { return {std::begin(Range), std::end(Range)}; } @@ -1477,14 +1474,14 @@ namespace std /// Implement std::swap in terms of SmallVector swap. template -inline void swap(ams::SmallVectorImpl &LHS, ams::SmallVectorImpl &RHS) +inline void swap(ams::SmallVectorImpl& LHS, ams::SmallVectorImpl& RHS) { LHS.swap(RHS); } /// Implement std::swap in terms of SmallVector swap. template -inline void swap(ams::SmallVector &LHS, ams::SmallVector &RHS) +inline void swap(ams::SmallVector& LHS, ams::SmallVector& RHS) { LHS.swap(RHS); } diff --git a/src/AMSlib/ml/surrogate.cpp b/src/AMSlib/ml/surrogate.cpp index 0ab4e7dc..faa7af0c 100644 --- a/src/AMSlib/ml/surrogate.cpp +++ b/src/AMSlib/ml/surrogate.cpp @@ -197,8 +197,11 @@ std::tuple SurrogateModel::_evaluate( std::tuple SurrogateModel::evaluate( ams::MutableArrayRef Inputs, - float threshold) + float threshold, + torch::Tensor* packedWorkspace, + bool* reusedWorkspace) { + if (reusedWorkspace) *reusedWorkspace = false; if (Inputs.size() == 0) { throw std::invalid_argument( "Input Vector should always contain at " @@ -208,6 +211,8 @@ std::tuple SurrogateModel::evaluate( torch::DeviceType InputDevice = Inputs[0].device().type(); torch::Dtype InputDType = torch::typeMetaToScalarType(Inputs[0].dtype()); auto CAxis = Inputs[0].sizes().size() - 1; + if (Inputs[0].dim() == 0) + throw std::invalid_argument("Surrogate inputs must have rank at least one"); // Verify input/device matching for (auto& In : Inputs) { @@ -223,17 +228,48 @@ std::tuple SurrogateModel::evaluate( "domain tensors have different data " "types\n"); } + if (In.dim() != Inputs[0].dim()) + throw std::invalid_argument("Surrogate input ranks differ"); + for (int64_t axis = 0; axis < In.dim() - 1; ++axis) + if (In.size(axis) != Inputs[0].size(axis)) + throw std::invalid_argument( + "Surrogate input shapes differ outside the feature axis"); } - c10::SmallVector ConvertedInputs(Inputs.begin(), Inputs.end()); - // If either the model's execution device or the data type differ - // in respect to the inputs we need to handle this separately. - if (InputDevice != torch_device || InputDType != torch_dtype) { - for (int i = 0; i < ConvertedInputs.size(); i++) { - ConvertedInputs[i] = ConvertedInputs[i].to(torch_device, torch_dtype); + + torch::Tensor ITensor; + const bool direct = Inputs.size() == 1 && InputDevice == torch_device && + InputDType == torch_dtype; + if (direct) { + ITensor = Inputs[0]; + } else { + std::vector packedShape(Inputs[0].sizes().begin(), + Inputs[0].sizes().end()); + int64_t features = 0; + for (const auto& input : Inputs) + features += input.size(CAxis); + packedShape[CAxis] = features; + torch::Tensor localWorkspace; + torch::Tensor& workspace = + packedWorkspace ? *packedWorkspace : localWorkspace; + auto options = + torch::TensorOptions().dtype(torch_dtype).device(torch_device); + if (!workspace.defined() || workspace.scalar_type() != torch_dtype || + workspace.device().type() != torch_device) { + workspace = torch::empty(packedShape, options); + } else { + void* oldPointer = workspace.data_ptr(); + workspace.resize_(packedShape); + if (reusedWorkspace) + *reusedWorkspace = workspace.data_ptr() == oldPointer; } + int64_t offset = 0; + for (const auto& input : Inputs) { + const int64_t width = input.size(CAxis); + workspace.narrow(CAxis, offset, width).copy_(input); + offset += width; + } + ITensor = workspace; } - - auto ITensor = torch::cat(ConvertedInputs, CAxis); AMS_DBG(Surrogate, "Input concatenated tensor is {}", shapeToString(ITensor)); auto [OTensor, Predicate] = _evaluate(ITensor, threshold); diff --git a/src/AMSlib/ml/surrogate.hpp b/src/AMSlib/ml/surrogate.hpp index 7bf378b1..b5b243a7 100644 --- a/src/AMSlib/ml/surrogate.hpp +++ b/src/AMSlib/ml/surrogate.hpp @@ -106,7 +106,9 @@ class SurrogateModel std::tuple evaluate( ams::MutableArrayRef Inputs, - const float threshold); + const float threshold, + torch::Tensor* packedWorkspace = nullptr, + bool* reusedWorkspace = nullptr); inline bool is_gpu() const diff --git a/src/AMSlib/wf/SmallVector.cpp b/src/AMSlib/wf/SmallVector.cpp index 6959b86a..9f347212 100644 --- a/src/AMSlib/wf/SmallVector.cpp +++ b/src/AMSlib/wf/SmallVector.cpp @@ -17,12 +17,12 @@ // added operator<< for std::ostream +#include "SmallVector.hpp" + #include #include #include #include - -#include "SmallVector.hpp" using namespace ams; // Check that no bytes are wasted and everything is well-aligned. diff --git a/src/AMSlib/wf/action.hpp b/src/AMSlib/wf/action.hpp index 6f875119..a4385121 100644 --- a/src/AMSlib/wf/action.hpp +++ b/src/AMSlib/wf/action.hpp @@ -1,5 +1,7 @@ #pragma once +#if defined(__AMS_ENABLE_TORCH__) + #include "AMSError.hpp" namespace ams @@ -24,3 +26,4 @@ class Action }; } // namespace ams +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/basedb.hpp b/src/AMSlib/wf/basedb.hpp index 33483eb3..05524ac7 100644 --- a/src/AMSlib/wf/basedb.hpp +++ b/src/AMSlib/wf/basedb.hpp @@ -8,9 +8,6 @@ #ifndef __AMS_BASE_DB__ #define __AMS_BASE_DB__ -#include -#include - #include #include #include @@ -24,6 +21,7 @@ #include #include "AMS.h" +#include "AMSTensor.hpp" #include "ArrayRef.hpp" #include "debug.h" #include "macro.h" @@ -121,8 +119,8 @@ class BaseDB * 'num_elements' values to be stored */ - virtual void store(ArrayRef Inputs, - ArrayRef Outputs) = 0; + virtual void store(ArrayRef Inputs, + ArrayRef Outputs) = 0; uint64_t getId() const { return id; } @@ -235,8 +233,8 @@ class hdf5DB final : public FileDB */ hid_t getDataSet(hid_t group, std::string dName, - ams::SmallVector& currentShape, - const at::IntArrayRef Shape, + SmallVector& currentShape, + ArrayRef Shape, hid_t dataType, const size_t Chunk = 1024L); @@ -248,8 +246,8 @@ class hdf5DB final : public FileDB * @param[in] numIn number of input 1-D vectors * @param[in] numOut number of output 1-D vectors */ - void createDataSets(const at::IntArrayRef InShapes, - const at::IntArrayRef OutShapes); + void createDataSets(ArrayRef InShapes, + ArrayRef OutShapes); /** * @brief Write all the data in the vectors in the respective datasets. @@ -262,10 +260,10 @@ class hdf5DB final : public FileDB void writeDataToDataset(ams::MutableArrayRef currentShape, hid_t& dset, - const at::Tensor& tensor_data); + const AMSTensor& tensor_data); PERFFASPECT() - void _store(const at::Tensor& inputs, const at::Tensor& outputs); + void _store(const AMSTensor& inputs, const AMSTensor& outputs); public: // Delete copy constructors. We do not want to copy the DB around @@ -296,6 +294,13 @@ class hdf5DB final : public FileDB */ AMSDBType dbType() override { return AMSDBType::AMS_HDF5; }; + /** + * @brief Concatenate tensors along the last dimension into a single contiguous tensor. + * @param[in] tensors List of tensors to concatenate. + * + * @note For now, we compute the total shape and do a manual copy. + */ + AMSTensor concatAndStore(ArrayRef tensors); /** * @brief Takes an input and an output tensor each holding data, @@ -303,8 +308,8 @@ class hdf5DB final : public FileDB * @param[in] inputs Tensor containing the inputs to bestored * @param[in] outputs Tensor containing the outputs to bestored */ - virtual void store(ArrayRef Inputs, - ArrayRef Outputs) override; + virtual void store(ArrayRef Inputs, + ArrayRef Outputs) override; }; #endif @@ -413,7 +418,7 @@ static inline size_t serialize_data(uint8_t* dest, T src) class AMSMessage { private: - static size_t computeSerializedSize(const torch::Tensor& tensor) + static size_t computeSerializedSize(const AMSTensor& tensor) { // First we need to store how many dimensions this tensor has. size_t totalBytes = sizeof(size_t); @@ -425,7 +430,7 @@ class AMSMessage return totalBytes + tensor.nbytes(); } - static void serializeTensorHeader(const torch::Tensor& tensor, uint8_t*& blob) + static void serializeTensorHeader(const AMSTensor& tensor, uint8_t*& blob) { blob += serialize_data(blob, static_cast(tensor.sizes().size())); blob += serialize_data(blob, static_cast(tensor.nbytes())); @@ -437,7 +442,7 @@ class AMSMessage } } - static void serializeTensor(const torch::Tensor& tensor, uint8_t*& blob) + static void serializeTensor(const AMSTensor& tensor, uint8_t*& blob) { serializeTensorHeader(tensor, blob); std::memcpy(blob, tensor.data_ptr(), tensor.nbytes()); @@ -472,18 +477,19 @@ class AMSMessage } /** - * @brief Constructor + * @brief Constructor. Warning: Callers must ensure tensors are CPU-resident and + * contiguous before constructing the message. * @param[in] id ID of the message * @param[in] rId MPI Rank of the messages (0 default) - * @param[in] num_elements Number of elements - * @param[in] inputs Inputs - * @param[in] outputs Outputs + * @param[in] domain_name Domain name + * @param[in] inputs Inputs (must be CPU, contiguous) + * @param[in] outputs Outputs (must be CPU, contiguous) */ AMSMessage(int id, uint64_t rId, std::string& domain_name, - ArrayRef Inputs, - ArrayRef Outputs) + ArrayRef Inputs, + ArrayRef Outputs) : _id(id), _rank(rId), _input_dim(Inputs.size()), @@ -491,25 +497,13 @@ class AMSMessage _data(nullptr), _total_size(0) { - SmallVector _inputs; - SmallVector _outputs; - auto tOptions = torch::TensorOptions() - .dtype(torch::kFloat32) - .device(c10::DeviceType::CPU); - - for (auto& tensor : Inputs) - _inputs.push_back(tensor.contiguous().to(tOptions)); - - for (auto& tensor : Outputs) - _outputs.push_back(tensor.contiguous().to(tOptions)); - AMSMsgHeader header(_rank, domain_name.size(), _input_dim, _output_dim); _total_size = AMSMsgHeader::size() + domain_name.size(); - for (auto& tensor : _inputs) + for (auto& tensor : Inputs) _total_size += computeSerializedSize(tensor); - for (auto& tensor : _outputs) + for (auto& tensor : Outputs) _total_size += computeSerializedSize(tensor); auto& rm = ams::ResourceManager::getInstance(); @@ -524,9 +518,9 @@ class AMSMessage uint8_t* blob = _data + current_offset; - for (auto& tensor : _inputs) + for (auto& tensor : Inputs) serializeTensor(tensor, blob); - for (auto& tensor : _outputs) + for (auto& tensor : Outputs) serializeTensor(tensor, blob); AMS_DBG(AMSMessage, "Allocated message {}: {} with size: {}", @@ -1510,8 +1504,8 @@ class RMQInterface * @param[in] outputs A vector containing arrays of outputs, each array has num_elements elements */ void publish(std::string& domain_name, - ArrayRef Inputs, - ArrayRef Outputs) + ArrayRef Inputs, + ArrayRef Outputs) { CALIPER(CALI_MARK_BEGIN("STORE_RMQ");) AMS_DBG(RMQInterface, @@ -1559,8 +1553,10 @@ class RMQInterface flush(100, 100); _publishingManager->stop(); auto size = MessagesBuffer::getInstance().size(); - if (size != 0) + if (size != 0) { AMS_DBG(RMQInterface, "Rank {} did not ack {} messages", _rId, size) + return; + } } ~RMQInterface() @@ -1615,8 +1611,8 @@ class RabbitMQDB final : public BaseDB * @param[in] predicate (NOT SUPPORTED YET) Series of predicate */ PERFFASPECT() - virtual void store(ArrayRef Inputs, - ArrayRef Outputs) + virtual void store(ArrayRef Inputs, + ArrayRef Outputs) override { interface.publish(appDomain, Inputs, Outputs); } diff --git a/src/AMSlib/wf/eval_context.hpp b/src/AMSlib/wf/eval_context.hpp index 00e1a443..226007a9 100644 --- a/src/AMSlib/wf/eval_context.hpp +++ b/src/AMSlib/wf/eval_context.hpp @@ -1,5 +1,6 @@ #pragma once +#if defined(__AMS_ENABLE_TORCH__) #include #include @@ -74,3 +75,4 @@ struct EvalContext { }; } // namespace ams +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/hdf5db.cpp b/src/AMSlib/wf/hdf5db.cpp index 1cb064c9..8d9e1cf5 100644 --- a/src/AMSlib/wf/hdf5db.cpp +++ b/src/AMSlib/wf/hdf5db.cpp @@ -5,16 +5,15 @@ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ -#include #include #include #include -#include -#include -#include +#include #include +#include +#include "AMSTensor.hpp" #include "ArrayRef.hpp" #include "wf/basedb.hpp" @@ -36,7 +35,7 @@ static std::string SmallVectorToString(ams::MutableArrayRef shape) return oss.str(); } -static std::string tensorSizeToString(const at::IntArrayRef shape) +static std::string tensorSizeToString(ArrayRef shape) { std::ostringstream oss; oss << "["; @@ -50,45 +49,42 @@ static std::string tensorSizeToString(const at::IntArrayRef shape) return oss.str(); } -// Helper function to convert torch::Dtype to a string -static std::string dtypeToString(torch::Dtype dtype) +static std::string amsDTypeToString(AMSDType dtype) { - static const std::unordered_map dtypeMap = { - {torch::kFloat32, "float32"}, - {torch::kFloat, "float32"}, // Alias for float32 - {torch::kFloat64, "float64"}, - {torch::kDouble, "float64"}, // Alias for float64 - {torch::kInt32, "int32"}, - {torch::kInt64, "int64"}, - {torch::kBool, "bool"}, - {torch::kUInt8, "uint8"}, - {torch::kInt8, "int8"}, - {torch::kHalf, "float16"}, - {torch::kBFloat16, "bfloat16"}}; - return dtypeMap.count(dtype) ? dtypeMap.at(dtype) : "unknown dtype"; + switch (dtype) { + case AMSDType::AMS_SINGLE: + return "float32"; + case AMSDType::AMS_DOUBLE: + return "float64"; + case AMSDType::AMS_INT32: + return "int32"; + case AMSDType::AMS_INT64: + return "int64"; + default: + return "unknown dtype"; + } } -// Helper function to convert torch::Dtype to a string -static hid_t torchDTypeToHDF5Type(torch::Dtype dtype) + +static hid_t amsDTypeToHDF5Type(AMSDType dtype) { - static const std::unordered_map dtypeMap = { - {torch::kFloat32, H5T_NATIVE_FLOAT}, - {torch::kFloat, H5T_NATIVE_FLOAT}, // Alias for float32 - {torch::kFloat64, H5T_NATIVE_DOUBLE}, - {torch::kDouble, H5T_NATIVE_DOUBLE}, // Alias for float64 - {torch::kInt32, H5T_NATIVE_INT}, - {torch::kInt64, H5T_NATIVE_LONG}, - {torch::kBool, H5T_NO_CLASS}, - {torch::kUInt8, H5T_NO_CLASS}, - {torch::kInt8, H5T_NO_CLASS}, - {torch::kHalf, H5T_NO_CLASS}, - {torch::kBFloat16, H5T_NO_CLASS}}; - return dtypeMap.count(dtype) ? dtypeMap.at(dtype) : H5T_NO_CLASS; + switch (dtype) { + case AMSDType::AMS_SINGLE: + return H5T_NATIVE_FLOAT; + case AMSDType::AMS_DOUBLE: + return H5T_NATIVE_DOUBLE; + case AMSDType::AMS_INT32: + return H5T_NATIVE_INT; + case AMSDType::AMS_INT64: + return H5T_NATIVE_LONG; + default: + return H5T_NO_CLASS; + } } hid_t hdf5DB::getDataSet(hid_t group, std::string dName, - ams::SmallVector& currentShape, - const at::IntArrayRef Shape, + SmallVector& currentShape, + ArrayRef Shape, hid_t dataType, const size_t Chunk) { @@ -121,7 +117,7 @@ hid_t hdf5DB::getDataSet(hid_t group, hsize_t max_dims[Shape.size()]; hsize_t initial_shape[Shape.size()]; for (int i = 0; i < Shape.size(); i++) { - max_dims[i] = Shape[i]; + max_dims[i] = static_cast(Shape[i]); initial_shape[i] = 0; } max_dims[0] = H5S_UNLIMITED; @@ -153,7 +149,8 @@ hid_t hdf5DB::getDataSet(hid_t group, } -void hdf5DB::createDataSets(at::IntArrayRef InShapes, at::IntArrayRef OutShapes) +void hdf5DB::createDataSets(ArrayRef InShapes, + ArrayRef OutShapes) { HDIset = getDataSet(HFile, "input_data", currentInputShape, InShapes, HDType); @@ -163,16 +160,17 @@ void hdf5DB::createDataSets(at::IntArrayRef InShapes, at::IntArrayRef OutShapes) void hdf5DB::writeDataToDataset(ams::MutableArrayRef currentShape, hid_t& dset, - const at::Tensor& tensor_data) + const AMSTensor& tensor_data) { herr_t status; - // Ensure tensor is contiguous - torch::Tensor tensor_contiguous = tensor_data.contiguous(); + // // Ensure tensor is contiguous + // torch::Tensor tensor_contiguous = tensor_data.contiguous(); + + // TODO: make sure it is contiguous + std::vector tensor_dims(tensor_data.sizes().begin(), + tensor_data.sizes().end()); - // Get tensor dimensions - std::vector tensor_dims(tensor_contiguous.sizes().begin(), - tensor_contiguous.sizes().end()); int rank = tensor_dims.size(); // Initialize currentShape if it's empty (e.g., first write or reopening an existing file) @@ -230,12 +228,8 @@ void hdf5DB::writeDataToDataset(ams::MutableArrayRef currentShape, } // Write the tensor data to the dataset - status = H5Dwrite(dset, - HDType, - memSpace, - fileSpace, - H5P_DEFAULT, - tensor_contiguous.data_ptr()); + status = H5Dwrite( + dset, HDType, memSpace, fileSpace, H5P_DEFAULT, tensor_data.data_ptr()); if (status < 0) { throw std::runtime_error("Failed to write data to dataset."); } @@ -249,7 +243,7 @@ void hdf5DB::writeDataToDataset(ams::MutableArrayRef currentShape, } -void hdf5DB::_store(const at::Tensor& inputs, const at::Tensor& outputs) +void hdf5DB::_store(const AMSTensor& inputs, const AMSTensor& outputs) { AMS_DBG(DB, "DB of type {} stores input/output tensors of shapes {}, " @@ -316,41 +310,50 @@ hdf5DB::~hdf5DB() HDF5_ERROR(err); } -void hdf5DB::store(ArrayRef Inputs, - ArrayRef Outputs) +void hdf5DB::store(ArrayRef Inputs, ArrayRef Outputs) { - auto tOptions = torch::TensorOptions() - .dtype(torch::kFloat32) - .device(c10::DeviceType::CPU); + // auto tOptions = torch::TensorOptions() + // .dtype(torch::kFloat32) + // .device(c10::DeviceType::CPU); - c10::SmallVector ConvertedInputs(Inputs.begin(), Inputs.end()); - c10::SmallVector ConvertedOutputs(Outputs.begin(), - Outputs.end()); + // c10::SmallVector ConvertedInputs(Inputs.begin(), Inputs.end()); + // c10::SmallVector ConvertedOutputs(Outputs.begin(), + // Outputs.end()); - auto inputs = - torch::cat(ConvertedInputs, Inputs[0].sizes().size() - 1).to(tOptions); - auto outputs = - torch::cat(ConvertedOutputs, Outputs[0].sizes().size() - 1).to(tOptions); + // auto inputs = + // torch::cat(ConvertedInputs, Inputs[0].sizes().size() - 1).to(tOptions); + // auto outputs = + // torch::cat(ConvertedOutputs, Outputs[0].sizes().size() - 1).to(tOptions); + // TODO: handle error in better fashion here + if (Inputs.size() == 0 || Outputs.size() == 0) { + throw std::invalid_argument( + "store() requires non-empty input and output tensors"); + } - if (inputs.dtype() != outputs.dtype()) { + // TODO: Check every tensors type constentcy + AMSDType inputDType = Inputs[0].dtype(); + AMSDType outputDType = Outputs[0].dtype(); + + if (inputDType != outputDType) { throw std::invalid_argument( "Storing into HDF5 database requires all tensors to have the same " - "datatype. Now they have:" + - dtypeToString(torch::typeMetaToScalarType(inputs.dtype())) + " and " + - dtypeToString(torch::typeMetaToScalarType(outputs.dtype()))); + "datatype. Now they have: " + + amsDTypeToString(inputDType) + " and " + amsDTypeToString(outputDType)); } if (HDType == -1) { - HDType = torchDTypeToHDF5Type(torch::typeMetaToScalarType(inputs.dtype())); + HDType = amsDTypeToHDF5Type(inputDType); } if (HDType == -1 || HDType == H5T_NO_CLASS) throw std::invalid_argument( "Data base can not deduce the data type of the tensors" + - dtypeToString(torch::typeMetaToScalarType(inputs.dtype())) + " and " + - dtypeToString(torch::typeMetaToScalarType(outputs.dtype()))); + amsDTypeToString(inputDType) + " and " + amsDTypeToString(outputDType)); + + auto inputs = AMSTensor::concat(Inputs, inputDType); + auto outputs = AMSTensor::concat(Outputs, outputDType); _store(inputs, outputs); } diff --git a/src/AMSlib/wf/interface.cpp b/src/AMSlib/wf/interface.cpp index 71c474ea..0618dd25 100644 --- a/src/AMSlib/wf/interface.cpp +++ b/src/AMSlib/wf/interface.cpp @@ -1,11 +1,5 @@ -#include -#include -#include -#include #include -#include -#include #include "AMS.h" #include "AMSTensor.hpp" @@ -13,7 +7,18 @@ using namespace ams; -// dtype/device helpers +#if defined(__AMS_ENABLE_TORCH__) +#include +#include +#include +#include + +#include "AMSTorchInterop.hpp" + +// ============================================================================ +// Torch device helper functions +// ============================================================================ + static AMSResourceType torchDeviceToAMSDevice(c10::DeviceType dType) { switch (dType) { @@ -75,123 +80,30 @@ static c10::ScalarType amsToTorchDType(const ams::AMSDType dType) return torch::kInt64; throw std::runtime_error("Unknown ams data type"); - return torch::kHalf; + return torch::kHalf; // fp16 by default } -// single tensor +// ============================================================================ +// Torch tensor <=> AMS Tensor conversion functions +// ============================================================================ + static ams::AMSTensor torchToAMSTensorView(torch::Tensor& tensor) { - auto dType = torchDTypeToAMSType(tensor.scalar_type()); - auto rType = torchDeviceToAMSDevice(tensor.device().type()); - - auto shapes = ams::ArrayRef(tensor.sizes().begin(), tensor.sizes().size()); - auto strides = - ams::ArrayRef(tensor.strides().begin(), tensor.strides().size()); - - switch (dType) { - case AMSDType::AMS_SINGLE: - return AMSTensor::view(tensor.data_ptr(), shapes, strides, rType); - - case AMSDType::AMS_DOUBLE: - return AMSTensor::view(tensor.data_ptr(), shapes, strides, rType); - - case AMSDType::AMS_INT32: - return AMSTensor::view(tensor.data_ptr(), - shapes, - strides, - rType); - - case AMSDType::AMS_INT64: - return AMSTensor::view(tensor.data_ptr(), - shapes, - strides, - rType); - - default: - throw std::runtime_error("torchToAMSTensorView: unsupported Torch dtype"); - } + return ams::fromTorchView(tensor); } static ams::AMSTensor torchToAMSTensorCopy(const torch::Tensor& tensor) { - torch::Tensor src = tensor.detach(); - if (!src.is_contiguous()) { - src = src.contiguous(); - } - - auto dType = torchDTypeToAMSType(src.scalar_type()); - auto rType = torchDeviceToAMSDevice(src.device().type()); - if (rType == AMSResourceType::AMS_UNKNOWN) { - throw std::runtime_error("torchToAMSTensorCopy: unsupported Torch device"); - } - - ams::SmallVector shapes; - ams::SmallVector strides; - for (const auto dim : src.sizes()) { - shapes.push_back(static_cast(dim)); - } - for (const auto stride : src.strides()) { - strides.push_back(static_cast(stride)); - } - - auto& rm = ams::ResourceManager::getInstance(); - switch (dType) { - case AMSDType::AMS_SINGLE: { - auto out = AMSTensor::create(shapes, strides, rType); - rm.copy( - src.data_ptr(), rType, out.data(), rType, src.numel()); - return out; - } - case AMSDType::AMS_DOUBLE: { - auto out = AMSTensor::create(shapes, strides, rType); - rm.copy(src.data_ptr(), - rType, - out.data(), - rType, - src.numel()); - return out; - } - case AMSDType::AMS_INT32: { - auto out = AMSTensor::create(shapes, strides, rType); - rm.copy(src.data_ptr(), - rType, - out.data(), - rType, - src.numel()); - return out; - } - case AMSDType::AMS_INT64: { - auto out = AMSTensor::create(shapes, strides, rType); - rm.copy(src.data_ptr(), - rType, - out.data(), - rType, - src.numel()); - return out; - } - default: - throw std::runtime_error("torchToAMSTensorCopy: unsupported Torch dtype"); - } + return ams::fromTorchCopy(tensor); } static torch::Tensor amsToTorchTensorView(const ams::AMSTensor& tensor) { - auto dType = amsToTorchDType(tensor.dType()); - auto deviceType = amsToTorchDevice(tensor.location()); - - c10::SmallVector shapes(tensor.shape().begin(), tensor.shape().end()); - c10::SmallVector strides(tensor.strides().begin(), - tensor.strides().end()); - - return torch::from_blob(tensor.raw_data(), - shapes, - strides, - torch::TensorOptions().dtype(dType).device( - deviceType)); + // Internal const-only inference view: models contractually do not mutate input. + return ams::toTorchView(const_cast(tensor)); } -// flat containers -static ams::SmallVector torchToAMSTensors( +ams::SmallVector torchToAMSTensors( ams::MutableArrayRef tensorVector) { ams::SmallVector ams_tensors; @@ -251,6 +163,10 @@ static torch::Tensor amsTensorToTorchModelInput(const ams::AMSTensor& tensor, return out; } +// ============================================================================ +// Graph surrogate AMS helper functions +// ============================================================================ + static void requireOutputFirstDim(const torch::Tensor& tensor, int64_t expected, const std::string& key, @@ -465,6 +381,10 @@ static c10::impl::GenericDict amsToTorchHeterogeneousGraph( return out; } +// ============================================================================ +// Tensor-based callApplication overloads +// ============================================================================ + void callApplication(ams::DomainLambda CallBack, ams::MutableArrayRef Ins, ams::MutableArrayRef InOuts, @@ -474,7 +394,6 @@ void callApplication(ams::DomainLambda CallBack, auto AMSInOuts = torchToAMSTensors(InOuts); auto AMSOuts = torchToAMSTensors(Outs); CallBack(AMSIns, AMSInOuts, AMSOuts); - return; } void callAMS(ams::AMSWorkflow* executor, @@ -483,11 +402,7 @@ void callAMS(ams::AMSWorkflow* executor, ams::SmallVector& inouts, ams::SmallVector& outs) { - ams::SmallVector tins = amsToTorchTensors(ins); - ams::SmallVector tinouts = amsToTorchTensors(inouts); - ams::SmallVector touts = amsToTorchTensors(outs); - - executor->evaluate(Physics, tins, tinouts, touts); + executor->evaluate(Physics, ins, inouts, outs); } // ============================================================================ @@ -498,7 +413,6 @@ void callApplication(ams::HomogeneousGraphDomainFn CallBack, const ams::AMSHomogeneousGraph& graph, ams::AMSHomogeneousGraphFields& outputs) { - // Directly invoke the user's physics callback with graph-native types CallBack(graph, outputs); } @@ -506,7 +420,6 @@ void callApplication(ams::HeterogeneousGraphDomainFn CallBack, const ams::AMSHeterogeneousGraph& graph, ams::AMSHeterogeneousGraphFields& outputs) { - // Directly invoke the user's physics callback with graph-native types CallBack(graph, outputs); } @@ -558,13 +471,13 @@ bool tryGraphSurrogate(AMSWorkflow* executor, torch::Tensor tensor = item.value().toTensor(); if (parts[0] == "node") { requireOutputFirstDim(tensor, num_nodes, key, "node"); - outputs.node_fields.insert(parts[1], torchToAMSTensorCopy(tensor)); + outputs.node_fields.insert(parts[1], torchToAMSTensorView(tensor)); } else if (parts[0] == "edge") { requireOutputFirstDim(tensor, num_edges, key, "edge"); - outputs.edge_fields.insert(parts[1], torchToAMSTensorCopy(tensor)); + outputs.edge_fields.insert(parts[1], torchToAMSTensorView(tensor)); } else if (parts[0] == "global") { requireGlobalOutputShape(tensor, key); - outputs.global_fields.insert(parts[1], torchToAMSTensorCopy(tensor)); + outputs.global_fields.insert(parts[1], torchToAMSTensorView(tensor)); } else { throw std::runtime_error("Malformed homogeneous graph output key '" + key + @@ -623,7 +536,7 @@ bool tryGraphSurrogate(AMSWorkflow* executor, const int64_t num_nodes = reference_tensor.shape()[0]; requireOutputFirstDim(tensor, num_nodes, key, "node"); outputs.getOrCreateNodeStore(parts[1]).insert(parts[2], - torchToAMSTensorCopy( + torchToAMSTensorView( tensor)); } else if (parts.size() == 3 && parts[0] == "edge" && !parts[1].empty() && !parts[2].empty()) { @@ -642,12 +555,12 @@ bool tryGraphSurrogate(AMSWorkflow* executor, } requireOutputFirstDim(tensor, edge_index->shape()[1], key, "edge"); outputs.getOrCreateEdgeStore(edge_type).insert(parts[2], - torchToAMSTensorCopy( + torchToAMSTensorView( tensor)); } else if (parts.size() == 2 && parts[0] == "global" && !parts[1].empty()) { requireGlobalOutputShape(tensor, key); - outputs.global_store.insert(parts[1], torchToAMSTensorCopy(tensor)); + outputs.global_store.insert(parts[1], torchToAMSTensorView(tensor)); } else { throw std::runtime_error("Malformed heterogeneous graph output key '" + key + @@ -692,14 +605,25 @@ void callAMS(ams::AMSWorkflow* executor, const ams::AMSHeterogeneousGraph& graph_input, ams::AMSHeterogeneousGraphFields& outputs) { - // Try graph surrogate execution first bool surrogate_used = tryGraphSurrogate(executor, graph_input, outputs); - // If surrogate succeeded, we're done if (surrogate_used) { return; } - // Otherwise, fallback to original physics computation callApplication(Physics, graph_input, outputs); } + +#else + +void callAMS(ams::AMSWorkflow* executor, + DomainLambda Physics, + const ams::SmallVector& ins, + ams::SmallVector& inouts, + ams::SmallVector& outs) +{ + // In training mode, we can directlty use AMSTensor, no conversion needed + executor->evaluate(Physics, ins, inouts, outs); +} + +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/interface.hpp b/src/AMSlib/wf/interface.hpp index 463c121e..717c1df7 100644 --- a/src/AMSlib/wf/interface.hpp +++ b/src/AMSlib/wf/interface.hpp @@ -1,33 +1,38 @@ #pragma once -#include -#include #include "AMS.h" +// #include "AMSTensor.hpp" namespace ams { class AMSWorkflow; } +void callAMS(ams::AMSWorkflow* executor, + ams::DomainLambda Physics, + const ams::SmallVector& ins, + ams::SmallVector& inouts, + ams::SmallVector& outs); + +#if defined(__AMS_ENABLE_TORCH__) + +#include +#include + void callApplication(ams::DomainLambda CallBack, ams::MutableArrayRef Ins, ams::MutableArrayRef InOuts, ams::MutableArrayRef Outs); -void callApplication(ams::HomogeneousGraphDomainFn CallBack, - const ams::AMSHomogeneousGraph& graph, - ams::AMSHomogeneousGraphFields& outputs); - -void callApplication(ams::HeterogeneousGraphDomainFn CallBack, - const ams::AMSHeterogeneousGraph& graph, - ams::AMSHeterogeneousGraphFields& outputs); - +/** @brief Helper to create AMSTensor views from a vector of torch::Tensors. +* @note The torch::Tensors MUST outlive the returned views. +*/ +ams::SmallVector torchToAMSTensors( + ams::MutableArrayRef tensorVector); -void callAMS(ams::AMSWorkflow* executor, - ams::DomainLambda Physics, - const ams::SmallVector& ins, - ams::SmallVector& inouts, - ams::SmallVector& outs); +// ============================================================================ +// Graph-based callApplication overloads +// ============================================================================ void callAMS(ams::AMSWorkflow* executor, ams::HomogeneousGraphDomainFn Physics, @@ -38,3 +43,13 @@ void callAMS(ams::AMSWorkflow* executor, ams::HeterogeneousGraphDomainFn Physics, const ams::AMSHeterogeneousGraph& graph_input, ams::AMSHeterogeneousGraphFields& outputs); + +void callApplication(ams::HomogeneousGraphDomainFn CallBack, + const ams::AMSHomogeneousGraph& graph, + ams::AMSHomogeneousGraphFields& outputs); + +void callApplication(ams::HeterogeneousGraphDomainFn CallBack, + const ams::AMSHeterogeneousGraph& graph, + ams::AMSHeterogeneousGraphFields& outputs); + +#endif \ No newline at end of file diff --git a/src/AMSlib/wf/layout_transform.hpp b/src/AMSlib/wf/layout_transform.hpp index b7806fee..34a44de6 100644 --- a/src/AMSlib/wf/layout_transform.hpp +++ b/src/AMSlib/wf/layout_transform.hpp @@ -1,5 +1,7 @@ #pragma once +#if defined(__AMS_ENABLE_TORCH__) + #include #include // for torch::jit::IValue @@ -39,3 +41,4 @@ class LayoutTransform }; } // namespace ams +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/pipeline.hpp b/src/AMSlib/wf/pipeline.hpp index e412f0f0..f03fba27 100644 --- a/src/AMSlib/wf/pipeline.hpp +++ b/src/AMSlib/wf/pipeline.hpp @@ -1,5 +1,7 @@ #pragma once +#if defined(__AMS_ENABLE_TORCH__) + #include #include @@ -53,3 +55,4 @@ class Pipeline }; } // namespace ams +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/pointwise_layout_transform.hpp b/src/AMSlib/wf/pointwise_layout_transform.hpp index e3b5a2e0..9754c0be 100644 --- a/src/AMSlib/wf/pointwise_layout_transform.hpp +++ b/src/AMSlib/wf/pointwise_layout_transform.hpp @@ -1,5 +1,7 @@ #pragma once +#if defined(__AMS_ENABLE_TORCH__) + #include #include @@ -165,3 +167,4 @@ class PointwiseConcatTransform : public LayoutTransform }; } // namespace ams +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/policy.hpp b/src/AMSlib/wf/policy.hpp index 7d020404..b39ba07b 100644 --- a/src/AMSlib/wf/policy.hpp +++ b/src/AMSlib/wf/policy.hpp @@ -1,5 +1,6 @@ #pragma once +#if defined(__AMS_ENABLE_TORCH__) #include "wf/pipeline.hpp" namespace ams @@ -31,3 +32,4 @@ class Policy }; } // namespace ams +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/resource_manager.hpp b/src/AMSlib/wf/resource_manager.hpp index 39e646dd..7c9f660f 100644 --- a/src/AMSlib/wf/resource_manager.hpp +++ b/src/AMSlib/wf/resource_manager.hpp @@ -9,6 +9,9 @@ #define __AMS_ALLOCATOR__ #include +#include +#include +#include #include #include @@ -57,16 +60,12 @@ class ResourceManager private: /** @brief Used internally to map resource types (Device, host, pinned memory) to * umpire allocator ids. */ - std::vector RMAllocators; + mutable std::vector> RMAllocators; + mutable std::mutex Mutex; ResourceManager() : RMAllocators({nullptr, nullptr, nullptr}) {}; public: - ~ResourceManager() - { - for (auto allocator : RMAllocators) { - if (allocator) delete allocator; - } - }; + ~ResourceManager() = default; ResourceManager(const ResourceManager&) = delete; ResourceManager(ResourceManager&&) = delete; ResourceManager& operator=(const ResourceManager&) = delete; @@ -81,7 +80,29 @@ class ResourceManager /** @brief return the name of an allocator */ const std::string getAllocatorName(AMSResourceType resource) const { - return RMAllocators[resource]->getName(); + return getAllocator(resource)->getName(); + } + + std::shared_ptr getAllocator(AMSResourceType resource) const + { + if (resource < AMS_HOST || resource > AMS_PINNED) + throw std::invalid_argument("Invalid AMS memory resource"); + std::lock_guard lock(Mutex); + auto allocator = RMAllocators[resource]; + if (!allocator) { + if (resource != AMS_HOST) { +#if !defined(__AMS_ENABLE_CUDA__) && !defined(__AMS_ENABLE_HIP__) + throw std::invalid_argument( + "Requested AMS device/pinned memory resource is unavailable"); +#endif + } + std::string name = resource == AMS_HOST ? "HOST" + : resource == AMS_DEVICE ? "DEVICE" + : "PINNED"; + allocator.reset(ams::internal::_get_allocator(name, resource)); + RMAllocators[resource] = allocator; + } + return allocator; } /** @brief Allocates nvalues on the specified device. @@ -96,8 +117,9 @@ class ResourceManager AMSResourceType dev, size_t alignment = sizeof(TypeInValue)) { + auto allocator = getAllocator(dev); return static_cast( - RMAllocators[dev]->allocate(nvalues * sizeof(TypeInValue), alignment)); + allocator->allocate(nvalues * sizeof(TypeInValue), alignment)); } /** @brief deallocates pointer from the specified device. @@ -110,7 +132,7 @@ class ResourceManager PERFFASPECT() void deallocate(TypeInValue* data, AMSResourceType dev) { - RMAllocators[dev]->deallocate(data); + getAllocator(dev)->deallocate(data); } /** @brief copy values from src to destination regardless of their memory location. @@ -144,7 +166,7 @@ class ResourceManager void deallocate(std::vector& dPtr, AMSResourceType resource) { for (auto* I : dPtr) - RMAllocators[resource]->deallocate(I); + getAllocator(resource)->deallocate(I); } void init() @@ -166,12 +188,11 @@ class ResourceManager void setAllocator(std::string& alloc_name, AMSResourceType resource) { - if (RMAllocators[resource]) { - delete RMAllocators[resource]; - } - - RMAllocators[resource] = - ams::internal::_get_allocator(alloc_name, resource); + if (resource < AMS_HOST || resource > AMS_PINNED) + throw std::invalid_argument("Invalid AMS memory resource"); + std::lock_guard lock(Mutex); + RMAllocators[resource].reset( + ams::internal::_get_allocator(alloc_name, resource)); AMS_DBG(ResourceManager, "Set Allocator [{}] to pool with name : {}", resource, @@ -180,6 +201,8 @@ class ResourceManager bool isActive(AMSResourceType resource) { + if (resource < AMS_HOST || resource > AMS_PINNED) return false; + std::lock_guard lock(Mutex); return RMAllocators[resource] != nullptr; } diff --git a/src/AMSlib/wf/tensor_bundle.hpp b/src/AMSlib/wf/tensor_bundle.hpp index d771d061..1c611ee0 100644 --- a/src/AMSlib/wf/tensor_bundle.hpp +++ b/src/AMSlib/wf/tensor_bundle.hpp @@ -1,5 +1,7 @@ #pragma once +#if defined(__AMS_ENABLE_TORCH__) + #include #include @@ -111,3 +113,4 @@ struct TensorBundle { }; } // namespace ams +#endif // __AMS_ENABLE_TORCH__ \ No newline at end of file diff --git a/src/AMSlib/wf/utils.hpp b/src/AMSlib/wf/utils.hpp index 59cc352a..ed04fb4c 100644 --- a/src/AMSlib/wf/utils.hpp +++ b/src/AMSlib/wf/utils.hpp @@ -8,16 +8,21 @@ #ifndef __AMS_UTILS_HPP__ #define __AMS_UTILS_HPP__ +#if defined(__AMS_ENABLE_TORCH__) #include +#endif + #include #include #include #include #include +#include #include #include "AMS.h" +#include "AMSTensor.hpp" #include "SmallVector.hpp" // ----------------------------------------------------------------------------- @@ -76,6 +81,20 @@ static inline size_t dtype_to_size(ams::AMSDType dType) } } +static inline std::string shapeToString(const ams::AMSTensor& tensor) +{ + std::ostringstream oss; + oss << "["; + auto shape = tensor.sizes(); + for (size_t i = 0; i < shape.size(); ++i) { + oss << shape[i]; + if (i < shape.size() - 1) oss << ", "; + } + oss << "]"; + return oss.str(); +} + +#if defined(__AMS_ENABLE_TORCH__) static inline std::string shapeToString(const at::Tensor& tensor) { std::ostringstream oss; @@ -91,6 +110,8 @@ SmallVector maskTensor(at::Tensor& Src, at::Tensor& Mask); } // namespace tensor } // namespace ams +#endif // __AMS_ENABLE_TORCH__ + template <> struct fmt::formatter : fmt::formatter { diff --git a/src/AMSlib/wf/workflow.hpp b/src/AMSlib/wf/workflow.hpp index 6ea872ef..2963c924 100644 --- a/src/AMSlib/wf/workflow.hpp +++ b/src/AMSlib/wf/workflow.hpp @@ -8,10 +8,8 @@ #ifndef __AMS_WORKFLOW_HPP__ #define __AMS_WORKFLOW_HPP__ -#include -#include - #include +#include #include #include "AMS.h" @@ -19,12 +17,19 @@ #include "SmallVector.hpp" #include "interface.hpp" #include "macro.h" -#include "ml/surrogate.hpp" #include "resource_manager.hpp" #include "utils.hpp" #include "wf/basedb.hpp" #include "wf/debug.h" +#if defined(__AMS_ENABLE_TORCH__) +#include +#include + +#include "AMSTorchInterop.hpp" +#include "ml/surrogate.hpp" +#endif + //! ---------------------------------------------------------------------------- //! AMS Workflow class //! the purpose of this class is to expose an "evaluate" function @@ -46,8 +51,16 @@ class AMSWorkflow /** @brief A string identifier describing the domain-model being solved. */ std::string domainName; +#if defined(__AMS_ENABLE_TORCH__) /** @brief The module that performs uncertainty quantification (UQ) */ std::shared_ptr MLModel; + /** Protects per-executor Torch conversion/model workspace. */ + mutable std::mutex InferenceMutex; + torch::Tensor PackedModelInput; + uint64_t TorchViewConversions = 0; + uint64_t PackedWorkspaceAllocations = 0; + uint64_t PackedWorkspaceReuses = 0; +#endif /** @brief The database to store data for which we cannot apply the current * model */ @@ -70,7 +83,7 @@ class AMSWorkflow /** @brief whether we should store data **/ bool storeData; -#ifdef __AMS_ENABLE_MPI__ +#if defined(__AMS_ENABLE_MPI__) /** @brief MPI Communicator for all ranks that call collectively the evaluate function **/ MPI_Comm comm; #endif @@ -78,19 +91,23 @@ class AMSWorkflow /** @brief Is the evaluate a distributed execution **/ bool isDistributed; - void storeComputedData(ArrayRef Ins, - ArrayRef InOutsBefore, - ArrayRef Outs, - ArrayRef InOutsAfter) + void storeComputedData(ArrayRef Ins, + ArrayRef InOutsBefore, + ArrayRef Outs, + ArrayRef InOutsAfter) { CALIPER(CALI_MARK_BEGIN("DBSTORE");) - SmallVector StoreInputTensors(Ins.begin(), Ins.end()); - SmallVector StoreOutputTensors(Outs.begin(), Outs.end()); - for (auto Tensor : InOutsBefore) - StoreInputTensors.push_back(Tensor); - for (auto Tensor : InOutsAfter) { - StoreOutputTensors.push_back(Tensor); - } + + SmallVector StoreInputTensors; + SmallVector StoreOutputTensors; + for (auto& Tensor : Ins) + StoreInputTensors.push_back(AMSTensor::view(Tensor)); + for (auto& Tensor : InOutsBefore) + StoreInputTensors.push_back(AMSTensor::view(Tensor)); + for (auto& Tensor : Outs) + StoreOutputTensors.push_back(AMSTensor::view(Tensor)); + for (auto& Tensor : InOutsAfter) + StoreOutputTensors.push_back(AMSTensor::view(Tensor)); AMS_DBG(Workflow, "Storing data (#elements = {}) to database", @@ -99,6 +116,29 @@ class AMSWorkflow CALIPER(CALI_MARK_END("DBSTORE");) } + // #if defined(__AMS_ENABLE_TORCH__) + // void storeComputedData(ArrayRef Ins, + // ArrayRef InOutsBefore, + // ArrayRef Outs, + // ArrayRef InOutsAfter) + // { + // CALIPER(CALI_MARK_BEGIN("DBSTORE");) + // SmallVector StoreInputTensors(Ins.begin(), Ins.end()); + // SmallVector StoreOutputTensors(Outs.begin(), Outs.end()); + // for (auto Tensor : InOutsBefore) + // StoreInputTensors.push_back(Tensor); + // for (auto Tensor : InOutsAfter) { + // StoreOutputTensors.push_back(Tensor); + // } + + // AMS_DBG(Workflow, + // "Storing data (#elements = {}) to database", + // StoreInputTensors[0].sizes()[0]); + // DB->store(StoreInputTensors, StoreOutputTensors); + // CALIPER(CALI_MARK_END("DBSTORE");) + // } + // #endif // __AMS_ENABLE_TORCH__ + void storeGraphData(const ams::AMSHomogeneousGraph& graph, const ams::AMSHomogeneousGraphFields& outputs) { @@ -138,6 +178,22 @@ class AMSWorkflow } public: +#if defined(__AMS_ENABLE_TORCH__) + struct InferenceStats { + uint64_t torchViewConversions; + uint64_t packedWorkspaceAllocations; + uint64_t packedWorkspaceReuses; + }; + + InferenceStats inferenceStats() const + { + std::lock_guard lock(InferenceMutex); + return {TorchViewConversions, + PackedWorkspaceAllocations, + PackedWorkspaceReuses}; + } +#endif + AMSWorkflow(std::string& surrogate_path, std::string& domain_name, float threshold, @@ -148,7 +204,7 @@ class AMSWorkflow rId(_pId), wSize(_wSize), storeData(store_data), -#ifdef __AMS_ENABLE_MPI__ +#if defined(__AMS_ENABLE_MPI__) comm(MPI_COMM_NULL), #endif threshold(threshold), @@ -158,9 +214,20 @@ class AMSWorkflow auto& dbm = ams::db::DBManager::getInstance(); if (storeData) DB = dbm.getDB(domainName, rId); +#if defined(__AMS_ENABLE_TORCH__) MLModel = nullptr; if (!surrogate_path.empty()) MLModel = SurrogateModel::getInstance(surrogate_path); +#endif + } + + ~AMSWorkflow() + { + AMS_DBG(Workflow, "Destroying Workflow Handler, DB: {}", DB.use_count()); + if (DB.use_count() == 2) { + auto& dbm = ams::db::DBManager::getInstance(); + dbm.dropDB(domainName, rId); + } } std::string getDBFilename() const @@ -170,7 +237,7 @@ class AMSWorkflow } -#ifdef __AMS_ENABLE_MPI__ +#if defined(__AMS_ENABLE_MPI__) void set_communicator(MPI_Comm communicator) { comm = communicator; } #endif @@ -178,13 +245,61 @@ class AMSWorkflow bool should_load_balance() const { -#ifdef __AMS_ENABLE_MPI__ +#if defined(__AMS_ENABLE_MPI__) return (comm != MPI_COMM_NULL && ePolicy == AMSExecPolicy::AMS_BALANCED); #else return false; #endif } + std::string getDBName() + { + if (!DB) return ""; + return DB->getFilename(); + } + +#if defined(__AMS_ENABLE_TORCH__) + + void evaluate(DomainLambda CallBack, + ams::ArrayRef Ins, + ams::MutableArrayRef InOuts, + ams::MutableArrayRef Outs) + { + // The common physics-only path stays entirely in AMSTensor space. + if (!MLModel) { + SmallVector inputs; + SmallVector inouts; + SmallVector outputs; + for (const auto& tensor : Ins) + inputs.push_back(AMSTensor::view(tensor)); + for (auto& tensor : InOuts) + inouts.push_back(AMSTensor::view(tensor)); + for (auto& tensor : Outs) + outputs.push_back(AMSTensor::view(tensor)); + + SmallVector inoutsBefore; + if (DB) + for (const auto& tensor : InOuts) + inoutsBefore.push_back(tensor.clone()); + CallBack(inputs, inouts, outputs); + if (DB) storeComputedData(inputs, inoutsBefore, outputs, inouts); + return; + } + + // Cross into Torch only at the model boundary. The existing Torch path also + // performs packing/scattering for the physics fallback subset. + SmallVector inputs; + SmallVector inouts; + SmallVector outputs; + for (const auto& tensor : Ins) + inputs.push_back(toTorchView(const_cast(tensor))); + for (auto& tensor : InOuts) + inouts.push_back(toTorchView(tensor)); + for (auto& tensor : Outs) + outputs.push_back(toTorchView(tensor)); + TorchViewConversions += inputs.size() + inouts.size() + outputs.size(); + evaluate(CallBack, inputs, inouts, outputs); + } static SmallVector subSelectTensors( ArrayRef Tensors, @@ -231,16 +346,6 @@ class AMSWorkflow return offset; } - - ~AMSWorkflow() - { - AMS_DBG(Workflow, "Destroying Workflow Handler, DB: {}", DB.use_count()); - if (DB.use_count() == 2) { - auto& dbm = ams::db::DBManager::getInstance(); - dbm.dropDB(domainName, rId); - } - } - /** @brief This is the main entry point of AMSLib and replaces the original * execution path of the application. * @param[in] probDescr an opaque type that will be forwarded to the @@ -292,6 +397,7 @@ class AMSWorkflow ams::MutableArrayRef InOuts, ams::MutableArrayRef Outs) { + std::lock_guard inferenceLock(InferenceMutex); CALIPER(CALI_MARK_BEGIN("AMSEvaluate");) AMS_DBG(Workflow, "Entering Workflow with TorchIn:{}, TorchInOut:{}, TorchOut:{}", @@ -346,7 +452,14 @@ class AMSWorkflow CALIPER(CALI_MARK_BEGIN("PHYSICS MODULE");) callApplication(CallBack, Ins, InOuts, Outs); CALIPER(CALI_MARK_END("PHYSICS MODULE");) - if (DB) storeComputedData(Ins, PhysicInOutsBefore, Outs, InOuts); + if (DB) { + // Convert torch tensors to AMSTensor views for storage + auto amsIns = torchToAMSTensors(Ins); + auto amsInOutsBefore = torchToAMSTensors(PhysicInOutsBefore); + auto amsOuts = torchToAMSTensors(Outs); + auto amsInOuts = torchToAMSTensors(InOuts); + storeComputedData(amsIns, amsInOutsBefore, amsOuts, amsInOuts); + } CALIPER(CALI_MARK_END("AMSEvaluate");) return; } @@ -363,7 +476,19 @@ class AMSWorkflow // ------------------------------------------------------------- CALIPER(CALI_MARK_BEGIN("SURROGATE");) // The predicate with which we will split the data on a lateMLInputsr step - auto [MLOutputs, Predicate] = MLModel->evaluate(InputTensors, threshold); + const void* packedPointer = + PackedModelInput.defined() ? PackedModelInput.data_ptr() : nullptr; + bool reusedWorkspace = false; + auto [MLOutputs, Predicate] = MLModel->evaluate(InputTensors, + threshold, + &PackedModelInput, + &reusedWorkspace); + if (PackedModelInput.defined()) { + if (reusedWorkspace && packedPointer == PackedModelInput.data_ptr()) + ++PackedWorkspaceReuses; + else if (!packedPointer || packedPointer != PackedModelInput.data_ptr()) + ++PackedWorkspaceAllocations; + } CALIPER(CALI_MARK_END("SURROGATE");) @@ -417,10 +542,15 @@ class AMSWorkflow AMS_DBG(Workflow, "Finished physics evaluation") if (DB) { - storeComputedData(PhysicIns, - PhysicInOutsBefore, - PhysicOuts, - PhysicInOuts); + // Convert torch tensors to AMSTensor views for storage + auto amsPhysicIns = torchToAMSTensors(PhysicIns); + auto amsPhysicInOutsBefore = torchToAMSTensors(PhysicInOutsBefore); + auto amsPhysicOuts = torchToAMSTensors(PhysicOuts); + auto amsPhysicInOuts = torchToAMSTensors(PhysicInOuts); + storeComputedData(amsPhysicIns, + amsPhysicInOutsBefore, + amsPhysicOuts, + amsPhysicInOuts); } AMS_DBG(Workflow, "Finished AMSExecution") @@ -444,11 +574,71 @@ class AMSWorkflow CALIPER(CALI_MARK_END("AMSEvaluate");) } - std::string getDBName() +#else // !__AMS_ENABLE_TORCH__ + // ----------------------------------------------------------------------- + // Non-training evaluate path (AMSTensor) + // ----------------------------------------------------------------------- + + void evaluate(DomainLambda CallBack, + ams::ArrayRef Ins, + ams::MutableArrayRef InOuts, + ams::MutableArrayRef Outs) { - if (!DB) return ""; - return DB->getFilename(); + CALIPER(CALI_MARK_BEGIN("AMSEvaluate");) + REPORT_MEM_USAGE(Workflow, "Start") + AMS_DBG(Workflow, + "Entering Workflow (no-torch) with In:{}, InOut:{}, Out:{}", + Ins.size(), + InOuts.size(), + Outs.size()); + + // Clone InOuts before physics overwrites them (for DB storage) + SmallVector InOutsBefore; + for (auto& S : InOuts) + InOutsBefore.push_back(S.clone()); + + CALIPER(CALI_MARK_BEGIN("PACK");) + + SmallVector insVec; + for (auto& t : Ins) + insVec.push_back(AMSTensor::view(t)); + + SmallVector inoutsVec; + for (auto& t : InOuts) + inoutsVec.push_back(AMSTensor::view(t)); + + SmallVector outsVec; + for (auto& t : Outs) + outsVec.push_back(AMSTensor::view(t)); + + CALIPER(CALI_MARK_END("PACK");) + + // We call the application here + CALIPER(CALI_MARK_BEGIN("PHYSICS MODULE");) + CallBack(insVec, inoutsVec, outsVec); + CALIPER(CALI_MARK_END("PHYSICS MODULE");) + + if (DB) { + // TODO: remove useless copies + SmallVector storeIns; + for (auto& t : Ins) + storeIns.push_back(AMSTensor::view(t)); + + SmallVector storeOuts; + for (auto& t : Outs) + storeOuts.push_back(AMSTensor::view(t)); + + SmallVector storeInOuts; + for (auto& t : InOuts) + storeInOuts.push_back(AMSTensor::view(t)); + storeComputedData(storeIns, InOutsBefore, storeOuts, storeInOuts); + } + + REPORT_MEM_USAGE(Workflow, "End") + CALIPER(CALI_MARK_END("AMSEvaluate");) } + +#endif // __AMS_ENABLE_TORCH__ }; diff --git a/tests/AMSlib/CMakeLists.txt b/tests/AMSlib/CMakeLists.txt index ddc0c3d4..9b8bc993 100644 --- a/tests/AMSlib/CMakeLists.txt +++ b/tests/AMSlib/CMakeLists.txt @@ -38,9 +38,11 @@ if (NOT _ams_catch2_ctest_adapter) message(STATUS "Catch2 CTest adapter not found; using explicit add_test registrations") endif() -set(AMS_TEST_ROOT "${CMAKE_CURRENT_BINARY_DIR}") -add_subdirectory(models) -add_subdirectory(torch) +if (ENABLE_TORCH) + add_subdirectory(models) + add_subdirectory(torch) +endif() +add_subdirectory(core) add_subdirectory(db) add_subdirectory(wf) add_subdirectory(ams_interface) diff --git a/tests/AMSlib/ams_interface/CMakeLists.txt b/tests/AMSlib/ams_interface/CMakeLists.txt index 10b5a25e..ae0b5eed 100644 --- a/tests/AMSlib/ams_interface/CMakeLists.txt +++ b/tests/AMSlib/ams_interface/CMakeLists.txt @@ -27,40 +27,45 @@ function(BUILD_UNIT_TEST exe source) if (${ARGC} GREATER 3) target_sources(${exe} PRIVATE ${ARGV3}) endif() - target_link_libraries(${exe} PRIVATE stdc++fs AMS torch ${catch2_target}) + target_link_libraries(${exe} PRIVATE stdc++fs AMS ${catch2_target}) + + if (ENABLE_TORCH) + target_link_libraries(${exe} PRIVATE torch) + endif() target_link_libraries(${exe} PRIVATE ${AMS_HDF5_LINK_TARGETS}) target_compile_definitions(${exe} PRIVATE ${AMS_APP_DEFINES} CATCH_CONFIG_PREFIX_ALL) endfunction() -BUILD_UNIT_TEST(ams_explicit_end_to_end ams_ete.cpp Catch2::Catch2) -ADD_AMS_UNIT_TEST(AMS_EXPLICIT ams_explicit_end_to_end) +# ams_ete.cpp uses torch::Tensor +if (ENABLE_TORCH) + BUILD_UNIT_TEST(ams_explicit_end_to_end ams_ete.cpp Catch2::Catch2) + ADD_AMS_UNIT_TEST(AMS_EXPLICIT ams_explicit_end_to_end) -BUILD_UNIT_TEST(int_interface int_interface.cpp Catch2::Catch2 ../ams_catch_main.cpp) -ADD_AMS_UNIT_TEST(AMS_INT_INTERFACE int_interface) + BUILD_UNIT_TEST(ams_graph_fallback test_graph_fallback.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_AMS_UNIT_TEST(AMS_GRAPH_FALLBACK ams_graph_fallback) -BUILD_UNIT_TEST(ams_graph_fallback test_graph_fallback.cpp Catch2::Catch2 ../ams_catch_main.cpp) -ADD_AMS_UNIT_TEST(AMS_GRAPH_FALLBACK ams_graph_fallback) + BUILD_UNIT_TEST(ams_graph_surrogate test_graph_surrogate.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_AMS_UNIT_TEST(AMS_GRAPH_SURROGATE ams_graph_surrogate) -BUILD_UNIT_TEST(ams_graph_surrogate test_graph_surrogate.cpp Catch2::Catch2 ../ams_catch_main.cpp) -ADD_AMS_UNIT_TEST(AMS_GRAPH_SURROGATE ams_graph_surrogate) + BUILD_UNIT_TEST(ams_graph_mgn_surrogate test_graph_mgn_surrogate.cpp Catch2::Catch2 ../ams_catch_main.cpp) + target_link_libraries(ams_graph_mgn_surrogate PRIVATE nlohmann_json::nlohmann_json) + target_compile_definitions(ams_graph_mgn_surrogate + PRIVATE + AMS_MGN_DIFFUSION_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/../models/mgn_graph_diffusion" + ) -BUILD_UNIT_TEST(ams_graph_mgn_surrogate test_graph_mgn_surrogate.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(ams_graph_mgn_surrogate PRIVATE nlohmann_json::nlohmann_json) -target_compile_definitions(ams_graph_mgn_surrogate - PRIVATE - AMS_MGN_DIFFUSION_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/../models/mgn_graph_diffusion" -) + # Normal parity testing consumes the checked-in TorchScript model and + # self-contained JSON fixtures; training/export is an independent opt-in path. + add_test( + NAME MGN_DIFFUSION_AMS_PARITY + COMMAND $ -s --reporter console + ) + set_tests_properties(MGN_DIFFUSION_AMS_PARITY PROPERTIES + LABELS "AMS_INTERFACE;MGN_DIFFUSION" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + ) -# Normal parity testing consumes the checked-in TorchScript model and -# self-contained JSON fixtures; training/export is an independent opt-in path. -add_test( - NAME MGN_DIFFUSION_AMS_PARITY - COMMAND $ -s --reporter console -) -set_tests_properties(MGN_DIFFUSION_AMS_PARITY PROPERTIES - LABELS "AMS_INTERFACE;MGN_DIFFUSION" - WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" -) +endif() if(NOT Torch_FOUND) set_tests_properties(MGN_DIFFUSION_AMS_PARITY PROPERTIES DISABLED TRUE) diff --git a/tests/AMSlib/ams_interface/ams_ete_env.cpp b/tests/AMSlib/ams_interface/ams_ete_env.cpp index 9bddca90..2fce178b 100644 --- a/tests/AMSlib/ams_interface/ams_ete_env.cpp +++ b/tests/AMSlib/ams_interface/ams_ete_env.cpp @@ -28,7 +28,7 @@ struct Problem { int multiplier; Problem(int ni, int no) : num_inputs(ni), num_outputs(no), multiplier(100) {} - void run(long num_elements, DType** inputs, DType** outputs) + void run(long num_elements, const DType* const* inputs, DType** outputs) { for (int i = 0; i < num_elements; i++) { DType sum = 0; @@ -85,7 +85,7 @@ struct Problem { [&](const ams::SmallVector& ams_ins, ams::SmallVector& ams_inouts, ams::SmallVector& ams_outs) { - DType* ins[num_inputs]; + const DType* ins[num_inputs]; DType* outs[num_outputs]; if (num_inputs != ams_ins.size()) throw std::runtime_error( diff --git a/tests/AMSlib/ams_interface/int_interface.cpp b/tests/AMSlib/ams_interface/int_interface.cpp index d2bf7954..9fad6f23 100644 --- a/tests/AMSlib/ams_interface/int_interface.cpp +++ b/tests/AMSlib/ams_interface/int_interface.cpp @@ -24,7 +24,7 @@ struct AMSGlobalFixture { static AMSGlobalFixture amsGlobalFixture; // Simple computation function for int32_t -void compute_int32(int32_t* input, int32_t* output, int num_elements) +void compute_int32(const int32_t* input, int32_t* output, int num_elements) { for (int i = 0; i < num_elements; ++i) { // Simple computation: output = input * 2 + 1 @@ -33,7 +33,7 @@ void compute_int32(int32_t* input, int32_t* output, int num_elements) } // Simple computation function for int64_t -void compute_int64(int64_t* input, int64_t* output, int num_elements) +void compute_int64(const int64_t* input, int64_t* output, int num_elements) { for (int i = 0; i < num_elements; ++i) { // Simple computation: output = input * 3 + 10 @@ -78,10 +78,10 @@ CATCH_TEST_CASE("AMS API: int32_t tensor execution without model", SmallVector& outs) { CATCH_REQUIRE(ins.size() == 1); CATCH_REQUIRE(outs.size() == 1); - CATCH_REQUIRE(ins[0].dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(outs[0].dType() == AMSDType::AMS_INT32); + CATCH_REQUIRE(ins[0].dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(outs[0].dtype() == AMSDType::AMS_INT32); - int32_t* in_ptr = ins[0].data(); + const int32_t* in_ptr = ins[0].data(); int32_t* out_ptr = outs[0].data(); int count = ins[0].elements(); @@ -147,10 +147,10 @@ CATCH_TEST_CASE("AMS API: int64_t tensor execution without model", SmallVector& outs) { CATCH_REQUIRE(ins.size() == 1); CATCH_REQUIRE(outs.size() == 1); - CATCH_REQUIRE(ins[0].dType() == AMSDType::AMS_INT64); - CATCH_REQUIRE(outs[0].dType() == AMSDType::AMS_INT64); + CATCH_REQUIRE(ins[0].dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(outs[0].dtype() == AMSDType::AMS_INT64); - int64_t* in_ptr = ins[0].data(); + const int64_t* in_ptr = ins[0].data(); int64_t* out_ptr = outs[0].data(); int count = ins[0].elements(); @@ -220,7 +220,7 @@ CATCH_TEST_CASE("AMS API: 2D int32_t tensor execution", "[ams][api][int32][2d]") CATCH_REQUIRE(ins[0].shape()[0] == rows); CATCH_REQUIRE(ins[0].shape()[1] == cols); - int32_t* in_ptr = ins[0].data(); + const int32_t* in_ptr = ins[0].data(); int32_t* out_ptr = outs[0].data(); for (int i = 0; i < num_elements; ++i) { @@ -290,12 +290,12 @@ CATCH_TEST_CASE("AMS API: Mixed type tensors", "[ams][api][mixed]") SmallVector& io, SmallVector& outs) { CATCH_REQUIRE(ins.size() == 2); - CATCH_REQUIRE(ins[0].dType() == AMSDType::AMS_SINGLE); - CATCH_REQUIRE(ins[1].dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(outs[0].dType() == AMSDType::AMS_INT32); + CATCH_REQUIRE(ins[0].dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(ins[1].dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(outs[0].dtype() == AMSDType::AMS_INT32); - float* float_ptr = ins[0].data(); - int32_t* int_ptr = ins[1].data(); + const float* float_ptr = ins[0].data(); + const int32_t* int_ptr = ins[1].data(); int32_t* out_ptr = outs[0].data(); for (int i = 0; i < num_elements; ++i) { diff --git a/tests/AMSlib/ams_interface/problems.hpp b/tests/AMSlib/ams_interface/problems.hpp index 2d981b4d..f50a5e3d 100644 --- a/tests/AMSlib/ams_interface/problems.hpp +++ b/tests/AMSlib/ams_interface/problems.hpp @@ -15,8 +15,8 @@ struct Problem2D { } void run(long num_elements, - DType* input1, - DType* input2, + const DType* input1, + const DType* input2, DType* inout, DType* out1, DType* out2, @@ -99,7 +99,7 @@ struct Problem2D { ams_inouts, ams::SmallVector& ams_outs) { - DType* ins[num_inputs]; + const DType* ins[num_inputs]; DType* outs[num_outputs]; DType* inout; @@ -158,7 +158,7 @@ struct Problem { int multiplier; Problem(int ni, int no) : num_inputs(ni), num_outputs(no), multiplier(100) {} - void run(long num_elements, DType** inputs, DType** outputs) + void run(long num_elements, const DType* const* inputs, DType** outputs) { for (int i = 0; i < num_elements; i++) { DType sum = 0; @@ -216,7 +216,7 @@ struct Problem { [&](const ams::SmallVector& ams_ins, ams::SmallVector& ams_inouts, ams::SmallVector& ams_outs) { - DType* ins[num_inputs]; + const DType* ins[num_inputs]; DType* outs[num_outputs]; if (num_inputs != ams_ins.size()) throw std::runtime_error( @@ -268,7 +268,10 @@ struct ProblemBroadcast { { } - void run(long num_elements, DType** inputs, DType** outputs, DType constant) + void run(long num_elements, + const DType* const* inputs, + DType** outputs, + DType constant) { for (int i = 0; i < num_elements; i++) { DType sum = constant; @@ -333,7 +336,7 @@ struct ProblemBroadcast { [&](const ams::SmallVector& ams_ins, ams::SmallVector& ams_inouts, ams::SmallVector& ams_outs) { - DType* ins[num_inputs - 1]; + const DType* ins[num_inputs - 1]; DType* outs[num_outputs]; if (num_inputs != ams_ins.size()) throw std::runtime_error( diff --git a/tests/AMSlib/ams_test_device.hpp b/tests/AMSlib/ams_test_device.hpp new file mode 100644 index 00000000..f78132b9 --- /dev/null +++ b/tests/AMSlib/ams_test_device.hpp @@ -0,0 +1,26 @@ +#ifndef AMS_TEST_DEVICE_HPP +#define AMS_TEST_DEVICE_HPP + +#if defined(__AMS_ENABLE_CUDA__) +#include +#elif defined(__AMS_ENABLE_HIP__) +#include +#endif + +namespace ams::test +{ +inline bool hasRuntimeDevice() +{ +#if defined(__AMS_ENABLE_CUDA__) + int count = 0; + return cudaGetDeviceCount(&count) == cudaSuccess && count > 0; +#elif defined(__AMS_ENABLE_HIP__) + int count = 0; + return hipGetDeviceCount(&count) == hipSuccess && count > 0; +#else + return false; +#endif +} +} // namespace ams::test + +#endif // AMS_TEST_DEVICE_HPP diff --git a/tests/AMSlib/core/CMakeLists.txt b/tests/AMSlib/core/CMakeLists.txt new file mode 100644 index 00000000..fbf412d5 --- /dev/null +++ b/tests/AMSlib/core/CMakeLists.txt @@ -0,0 +1,137 @@ +function(ADD_CORE_UNIT_TEST name exec) + string(JOIN " " args ${ARGN}) + add_test(NAME ${name} COMMAND ${exec} -s --reporter console) + set_tests_properties(${name} PROPERTIES LABELS CORE_UNIT_TEST) +endfunction() + +function(BUILD_UNIT_TEST exe source) + add_executable(${exe} ${source}) + + target_compile_features(${exe} PRIVATE cxx_std_17) + target_include_directories(${exe} PRIVATE ${CMAKE_SOURCE_DIR}/src/AMSlib/) + target_include_directories(${exe} PRIVATE ${CMAKE_BINARY_DIR}/include/) + target_include_directories(${exe} PRIVATE ${AMS_TEST_ROOT}) + target_include_directories(${exe} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + + set(catch2_target Catch2::Catch2WithMain) + if (${ARGC} GREATER 2) + set(catch2_target ${ARGV2}) + endif() + if (${ARGC} GREATER 3) + target_sources(${exe} PRIVATE ${ARGV3}) + endif() + + target_link_libraries(${exe} PRIVATE stdc++fs AMS ${catch2_target}) + target_link_libraries(${exe} PRIVATE fmt::fmt) + + if (ENABLE_TORCH) + target_link_libraries(${exe} PRIVATE torch) + endif() + target_compile_definitions(${exe} PRIVATE ${AMS_APP_DEFINES} CATCH_CONFIG_PREFIX_ALL) + + target_link_libraries(${exe} PRIVATE ${AMS_HDF5_TARGET}) + + target_link_libraries(${exe} PRIVATE Threads::Threads) + + if (ENABLE_CUDA) + target_link_libraries(${exe} PRIVATE CUDA::cudart) + elseif (ENABLE_HIP) + target_link_libraries(${exe} PRIVATE hip::host) + endif() + + if (ENABLE_CALIPER) + target_link_libraries(${exe} PRIVATE caliper) + endif() + + if (ENABLE_RMQ) + target_link_libraries(${exe} PRIVATE amqpcpp) + if (OPENSSL_FOUND) + target_link_libraries(${exe} PRIVATE OpenSSL::SSL OpenSSL::Crypto) + endif() + # NOTE: We set here the event/event pthreads as public. As there is no easy way + # to do a find package(libevent) and RMQ is not exposing that properly. + target_link_libraries(${exe} PRIVATE ${LIBEVENT_LIBRARY} ${LIBEVENT_THREAD}) + endif() + + if (ENABLE_MPI) + target_link_libraries(${exe} PRIVATE MPI::MPI_CXX) + endif() +endfunction() + +# Tests that do NOT require torch +BUILD_UNIT_TEST(amstensor_int amstensor_int.cpp Catch2::Catch2 ../ams_catch_main.cpp) +ADD_CORE_UNIT_TEST(CORE::TENSOR_INT amstensor_int) +BUILD_UNIT_TEST(amstensor_float amstensor_float.cpp Catch2::Catch2 ../ams_catch_main.cpp) +ADD_CORE_UNIT_TEST(CORE::TENSOR_FLOAT amstensor_float) +BUILD_UNIT_TEST(amstensor_mixed amstensor_mixed.cpp Catch2::Catch2 ../ams_catch_main.cpp) +ADD_CORE_UNIT_TEST(CORE::TENSOR_MIXED amstensor_mixed) + +# Tests that require torch +# TODO: rewrite some of these tests with AMSTensor +if (ENABLE_TORCH) + BUILD_UNIT_TEST(tensor_bundle tensor_bundle.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_CORE_UNIT_TEST(CORE::TENSOR_BUNDLE tensor_bundle) + BUILD_UNIT_TEST(amstorch_interop amstorch_interop.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_CORE_UNIT_TEST(CORE::TORCH_INTEROP amstorch_interop) + + if (ENABLE_BENCHMARKS) + set(_ams_tensor_benchmark_commit "unknown") + set(_ams_tensor_benchmark_source_state "unknown") + if (GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE _ams_tensor_benchmark_git_result + OUTPUT_VARIABLE _ams_tensor_benchmark_git_output + OUTPUT_STRIP_TRAILING_WHITESPACE) + if (_ams_tensor_benchmark_git_result EQUAL 0) + set(_ams_tensor_benchmark_commit + "${_ams_tensor_benchmark_git_output}") + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain --untracked-files=no + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE _ams_tensor_benchmark_status_result + OUTPUT_VARIABLE _ams_tensor_benchmark_git_status + OUTPUT_STRIP_TRAILING_WHITESPACE) + if (_ams_tensor_benchmark_status_result EQUAL 0) + if (_ams_tensor_benchmark_git_status) + set(_ams_tensor_benchmark_source_state "dirty") + else() + set(_ams_tensor_benchmark_source_state "clean") + endif() + endif() + endif() + endif() + + set(_ams_tensor_benchmark_csv + "${CMAKE_BINARY_DIR}/benchmark-results/amstensor_torch.csv") + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/benchmark-results") + + add_executable(amstensor_torch_benchmark amstensor_torch_benchmark.cpp) + target_compile_features(amstensor_torch_benchmark PRIVATE cxx_std_17) + target_compile_definitions( + amstensor_torch_benchmark + PRIVATE CATCH_CONFIG_PREFIX_ALL + AMS_BENCHMARK_GIT_COMMIT="${_ams_tensor_benchmark_commit}" + AMS_BENCHMARK_SOURCE_STATE="${_ams_tensor_benchmark_source_state}" + AMS_TENSOR_BENCHMARK_CSV_DEFAULT="${_ams_tensor_benchmark_csv}") + target_include_directories(amstensor_torch_benchmark + PRIVATE ${CMAKE_BINARY_DIR}/include/ + ${CMAKE_CURRENT_SOURCE_DIR}/..) + target_link_libraries(amstensor_torch_benchmark + PRIVATE AMS torch Catch2::Catch2) + + add_test( + NAME PERFORMANCE::AMSTENSOR_TORCH + COMMAND amstensor_torch_benchmark + --reporter console + --benchmark-samples 50 + --benchmark-resamples 1000 + --benchmark-warmup-time 100) + set_tests_properties( + PERFORMANCE::AMSTENSOR_TORCH + PROPERTIES LABELS PERFORMANCE_REGRESSION + ENVIRONMENT + "OMP_NUM_THREADS=1;MKL_NUM_THREADS=1;OPENBLAS_NUM_THREADS=1") + endif() +endif() diff --git a/tests/AMSlib/core/amstensor_float.cpp b/tests/AMSlib/core/amstensor_float.cpp new file mode 100644 index 00000000..97f626b7 --- /dev/null +++ b/tests/AMSlib/core/amstensor_float.cpp @@ -0,0 +1,1005 @@ +/* + * Copyright 2021-2026 Lawrence Livermore National Security, LLC and other + * AMSLib Project Developers + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "AMS.h" +#include "AMSTensor.hpp" +#include "ams_test_device.hpp" +#include "wf/resource_manager.hpp" +#include "wf/utils.hpp" + +using namespace ams; + +CATCH_TEST_CASE("float: validates metadata and access contracts", + "[ams][tensor][float][validation]") +{ + using D = AMSTensor::IntDimType; + float values[16]{}; + CATCH_REQUIRE_THROWS_AS(AMSTensor::view(values, + std::vector{2}, + std::vector{1, 1}, + AMS_HOST), + std::invalid_argument); + CATCH_REQUIRE_THROWS_AS( + AMSTensor::view(values, std::vector{-1}, std::vector{1}, AMS_HOST), + std::invalid_argument); + CATCH_REQUIRE_THROWS_AS( + AMSTensor::view(values, std::vector{2}, std::vector{0}, AMS_HOST), + std::invalid_argument); + CATCH_REQUIRE_THROWS_AS(AMSTensor::view(values, + std::vector{2, 2}, + std::vector{1, 1}, + AMS_HOST), + std::invalid_argument); + CATCH_REQUIRE_THROWS_AS(AMSTensor::view(static_cast(nullptr), + std::vector{1}, + std::vector{1}, + AMS_HOST), + std::invalid_argument); + CATCH_REQUIRE_THROWS_AS(AMSTensor::view(reinterpret_cast( + reinterpret_cast(values) + + 1), + std::vector{1}, + std::vector{1}, + AMS_HOST), + std::invalid_argument); + CATCH_REQUIRE_THROWS_AS( + AMSTensor::view(values, + std::vector{std::numeric_limits::max(), 2}, + std::vector{2, 1}, + AMS_HOST), + std::overflow_error); + + auto tensor = + AMSTensor::view(values, std::vector{4}, std::vector{1}, AMS_HOST); + CATCH_REQUIRE_THROWS_AS(tensor.data(), std::invalid_argument); + CATCH_REQUIRE_THROWS_AS(tensor.transpose(-1, 0), std::out_of_range); +} + +CATCH_TEST_CASE("float: views retain owners and const views are read-only", + "[ams][tensor][float][view]") +{ + using D = AMSTensor::IntDimType; + auto derived = [] { + auto owner = AMSTensor::create(std::vector{2, 3}, + std::vector{3, 1}, + AMS_HOST); + owner.data()[5] = 42.0f; + return owner.transpose(0, 1); + }(); + CATCH_REQUIRE(derived.data()[5] == 42.0f); + + int destroyed = 0; + auto foreign = std::shared_ptr(new float[2], [&](void* pointer) { + delete[] static_cast(pointer); + ++destroyed; + }); + auto retained = AMSTensor::view(static_cast(foreign.get()), + std::vector{2}, + std::vector{1}, + AMS_HOST, + foreign); + foreign.reset(); + CATCH_REQUIRE(destroyed == 0); + retained = retained.clone(); + CATCH_REQUIRE(destroyed == 1); + + const auto& constTensor = retained; + auto readOnly = AMSTensor::view(constTensor); + CATCH_REQUIRE_FALSE(readOnly.writable()); + CATCH_REQUIRE_THROWS_AS(readOnly.data(), std::logic_error); + CATCH_REQUIRE(static_cast(readOnly).data() != + nullptr); +} + +CATCH_TEST_CASE("float: concat handles strided inputs and rejects mismatch", + "[ams][tensor][float][concat]") +{ + using D = AMSTensor::IntDimType; + std::vector left = {1, 2, -1, 3, 4}; + std::vector right = {5, -1, 6, -1}; + auto leftView = AMSTensor::view(left.data(), + std::vector{2, 2}, + std::vector{3, 1}, + AMS_HOST); + auto rightView = AMSTensor::view(right.data(), + std::vector{2, 1}, + std::vector{2, 1}, + AMS_HOST); + SmallVector values; + values.push_back(std::move(leftView)); + values.push_back(std::move(rightView)); + auto joined = AMSTensor::concat(values, AMS_SINGLE); + const std::vector expected = {1, 2, 5, 3, 4, 6}; + for (size_t i = 0; i < expected.size(); ++i) + CATCH_REQUIRE(joined.data()[i] == expected[i]); + CATCH_REQUIRE_THROWS_AS(AMSTensor::concat({}, AMS_SINGLE), + std::invalid_argument); + CATCH_REQUIRE_THROWS_AS(AMSTensor::concat(values, AMS_DOUBLE), + std::invalid_argument); +} + +// ========================================================================= +// float — create +// ========================================================================= + +CATCH_TEST_CASE("float: create 1D tensor", "[ams][tensor][float][create]") +{ + AMSInit(); + const auto device = + GENERATE(AMSResourceType::AMS_HOST, AMSResourceType::AMS_DEVICE); + if (device == AMSResourceType::AMS_DEVICE) { + if (!ams::test::hasRuntimeDevice()) CATCH_SKIP("GPU device not available"); + } + + std::vector shape = {8}; + std::vector strides = {1}; + + auto tensor = AMSTensor::create(shape, strides, device); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(tensor.elements() == 8); + CATCH_REQUIRE(tensor.element_size() == sizeof(float)); + CATCH_REQUIRE(tensor.dim() == 1); + CATCH_REQUIRE(tensor.nbytes() == 8 * sizeof(float)); + CATCH_REQUIRE(tensor.location() == device); + CATCH_REQUIRE(tensor.shape()[0] == 8); +} + + +CATCH_TEST_CASE("float: create 2D tensor", "[ams][tensor][float][create]") +{ + AMSInit(); + std::vector shape = {4, 6}; + std::vector strides = {6, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(tensor.elements() == 24); + CATCH_REQUIRE(tensor.dim() == 2); + CATCH_REQUIRE(tensor.shape()[0] == 4); + CATCH_REQUIRE(tensor.shape()[1] == 6); + CATCH_REQUIRE(tensor.nbytes() == 24 * sizeof(float)); +} + + +CATCH_TEST_CASE("float: create 3D tensor", "[ams][tensor][float][create]") +{ + AMSInit(); + std::vector shape = {2, 3, 5}; + std::vector strides = {15, 5, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(tensor.elements() == 30); + CATCH_REQUIRE(tensor.dim() == 3); +} + + +// ========================================================================= +// float — view +// ========================================================================= + +CATCH_TEST_CASE("float: view 1D from existing buffer", + "[ams][tensor][float][view]") +{ + AMSInit(); + std::vector data = {1.1f, 2.2f, 3.3f, 4.4f, 5.5f}; + std::vector shape = {5}; + std::vector strides = {1}; + + auto v = AMSTensor::view(data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(v.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(v.elements() == 5); + CATCH_REQUIRE(v.data() == data.data()); + CATCH_REQUIRE(v.data()[0] == 1.1f); + CATCH_REQUIRE(v.data()[4] == 5.5f); +} + + +CATCH_TEST_CASE("float: view 2D from existing buffer", + "[ams][tensor][float][view]") +{ + AMSInit(); + std::vector data(12, 3.14f); + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto v = AMSTensor::view(data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(v.elements() == 12); + CATCH_REQUIRE(v.shape()[0] == 3); + CATCH_REQUIRE(v.shape()[1] == 4); + CATCH_REQUIRE(v.data()[11] == 3.14f); +} + + +CATCH_TEST_CASE("float: view from AMSTensor alias", + "[ams][tensor][float][view]") +{ + AMSInit(); + std::vector shape = {4}; + std::vector strides = {1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* ptr = tensor.data(); + for (int i = 0; i < 4; ++i) + ptr[i] = static_cast(i + 1); + + auto alias = AMSTensor::view(tensor); + + CATCH_REQUIRE(alias.data() == ptr); + CATCH_REQUIRE(alias.elements() == 4); + CATCH_REQUIRE(alias.data()[3] == 4.0f); +} + + +// ========================================================================= +// float — data access +// ========================================================================= + +CATCH_TEST_CASE("float: write and read 1D data", "[ams][tensor][float][data]") +{ + AMSInit(); + std::vector shape = {4}; + std::vector strides = {1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* data = tensor.data(); + + data[0] = 0.1f; + data[1] = 0.2f; + data[2] = 0.3f; + data[3] = 0.4f; + + CATCH_REQUIRE(data[0] == 0.1f); + CATCH_REQUIRE(data[3] == 0.4f); +} + + +CATCH_TEST_CASE("float: write and read 2D data", "[ams][tensor][float][data]") +{ + AMSInit(); + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* data = tensor.data(); + + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 4; ++j) + data[i * 4 + j] = static_cast(i) + static_cast(j) * 0.1f; + + CATCH_REQUIRE(data[0] == 0.0f); // [0,0] + CATCH_REQUIRE(data[3] == 0.3f); // [0,3] + CATCH_REQUIRE(std::fabs(data[5] - 1.1f) < 1e-6f); // [1,1] +} + + +// ========================================================================= +// float — transpose +// ========================================================================= + +CATCH_TEST_CASE("float: transpose 2D tensor", "[ams][tensor][float][transpose]") +{ + AMSInit(); + std::vector shape = {3, 5}; + std::vector strides = {5, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto transposed = tensor.transpose(0, 1); + + CATCH_REQUIRE(transposed.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(transposed.shape()[0] == 5); + CATCH_REQUIRE(transposed.shape()[1] == 3); + CATCH_REQUIRE(transposed.elements() == 15); + CATCH_REQUIRE(!transposed.contiguous()); +} + + +// ========================================================================= +// float — move +// ========================================================================= + +CATCH_TEST_CASE("float: move constructor", "[ams][tensor][float][move]") +{ + AMSInit(); + std::vector shape = {12}; + std::vector strides = {1}; + + auto t1 = AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* ptr = t1.data(); + + auto t2 = std::move(t1); + + CATCH_REQUIRE(t2.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(t2.elements() == 12); + CATCH_REQUIRE(t2.dim() == 1); + CATCH_REQUIRE(t2.nbytes() == 12 * sizeof(float)); + CATCH_REQUIRE(t2.data() == ptr); +} + + +CATCH_TEST_CASE("float: move assignment", "[ams][tensor][float][move]") +{ + AMSInit(); + std::vector shape1 = {10}; + std::vector strides = {1}; + std::vector shape2 = {3}; + + auto t1 = + AMSTensor::create(shape1, strides, AMSResourceType::AMS_HOST); + auto t2 = + AMSTensor::create(shape2, strides, AMSResourceType::AMS_HOST); + auto* ptr1 = t1.data(); + + t2 = std::move(t1); + + CATCH_REQUIRE(t2.elements() == 10); + CATCH_REQUIRE(t2.data() == ptr1); +} + + +// ========================================================================= +// float — clone +// ========================================================================= + +CATCH_TEST_CASE("float: clone contiguous 1D tensor", + "[ams][tensor][float][clone]") +{ + AMSInit(); + std::vector shape = {5}; + std::vector strides = {1}; + + std::vector src = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + CATCH_REQUIRE(cloned.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(cloned.elements() == 5); + CATCH_REQUIRE(cloned.dim() == 1); + CATCH_REQUIRE(cloned.strides()[0] == 1); + CATCH_REQUIRE(cloned.contiguous()); + CATCH_REQUIRE(cloned.nbytes() == 5 * sizeof(float)); + + auto* p = cloned.data(); + CATCH_REQUIRE(p != src.data()); + for (int i = 0; i < 5; ++i) + CATCH_REQUIRE(p[i] == src[i]); +} + + +CATCH_TEST_CASE("float: clone contiguous 2D tensor", + "[ams][tensor][float][clone]") +{ + AMSInit(); + std::vector src(12); + for (int i = 0; i < 12; ++i) + src[i] = static_cast(i) * 0.5f; + + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + CATCH_REQUIRE(cloned.dim() == 2); + CATCH_REQUIRE(cloned.shape()[0] == 3); + CATCH_REQUIRE(cloned.shape()[1] == 4); + CATCH_REQUIRE(cloned.contiguous()); + + auto* p = cloned.data(); + CATCH_REQUIRE(p != src.data()); + for (int i = 0; i < 12; ++i) + CATCH_REQUIRE(p[i] == src[i]); +} + + +CATCH_TEST_CASE("float: clone non-contiguous (transposed) tensor", + "[ams][tensor][float][clone][transpose]") +{ + AMSInit(); + // 3x4 row-major → transpose to 4x3 + std::vector src(12); + for (int i = 0; i < 12; ++i) + src[i] = static_cast(i); + + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto original = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto transposed = original.transpose(0, 1); + CATCH_REQUIRE(!transposed.contiguous()); + + auto cloned = transposed.clone(); + + CATCH_REQUIRE(cloned.contiguous()); + CATCH_REQUIRE(cloned.shape()[0] == 4); + CATCH_REQUIRE(cloned.shape()[1] == 3); + CATCH_REQUIRE(cloned.strides()[0] == 3); + CATCH_REQUIRE(cloned.strides()[1] == 1); + + // Logical [i,j] of transposed = src[j*4 + i] + auto* p = cloned.data(); + for (int i = 0; i < 4; ++i) + for (int j = 0; j < 3; ++j) { + float expected = static_cast(j * 4 + i); + CATCH_INFO("clone[" << i << "," << j << "]"); + CATCH_REQUIRE(p[i * 3 + j] == expected); + } +} + + +CATCH_TEST_CASE("float: clone is independent of source", + "[ams][tensor][float][clone]") +{ + AMSInit(); + std::vector src = {1.0f, 2.0f, 3.0f}; + + std::vector shape = {3}; + std::vector strides = {1}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + src[0] = 999.0f; + CATCH_REQUIRE(cloned.data()[0] == 1.0f); +} + + +// ========================================================================= +// float — concat +// ========================================================================= + +CATCH_TEST_CASE("float: concat single tensor", "[ams][tensor][float][concat]") +{ + AMSInit(); + std::vector a = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; + + std::vector shape = {2, 3}; + std::vector strides = {3, 1}; + + auto tA = AMSTensor::view(a.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_SINGLE); + + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 3); + auto* p = result.data(); + for (int i = 0; i < 6; ++i) + CATCH_REQUIRE(p[i] == a[i]); +} + + +CATCH_TEST_CASE("float: concat two 2D tensors", "[ams][tensor][float][concat]") +{ + AMSInit(); + // A:[3,2] B:[3,3] + // [1, 2] [7, 8, 9] + // [3, 4] [10, 11, 12] + // [5, 6] [13, 14, 15] + std::vector a = {1, 2, 3, 4, 5, 6}; + std::vector b = {7, 8, 9, 10, 11, 12, 13, 14, 15}; + + std::vector shapeA = {3, 2}; + std::vector stridesA = {2, 1}; + std::vector shapeB = {3, 3}; + std::vector stridesB = {3, 1}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_SINGLE); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(result.shape()[0] == 3); + CATCH_REQUIRE(result.shape()[1] == 5); + CATCH_REQUIRE(result.contiguous()); + + auto* p = result.data(); + std::vector expected = { + 1, 2, 7, 8, 9, 3, 4, 10, 11, 12, 5, 6, 13, 14, 15}; + for (int i = 0; i < 15; ++i) { + CATCH_INFO("index " << i); + CATCH_REQUIRE(p[i] == expected[i]); + } +} + + +CATCH_TEST_CASE("float: concat three tensors", "[ams][tensor][float][concat]") +{ + AMSInit(); + // A:[2,2] B:[2,3] C:[2,1] → [2,6] + std::vector a = {1, 2, 3, 4}; + std::vector b = {10, 20, 30, 40, 50, 60}; + std::vector c = {100, 200}; + + std::vector shapeA = {2, 2}; + std::vector stridesA = {2, 1}; + + std::vector shapeB = {2, 3}; + std::vector stridesB = {3, 1}; + + std::vector shapeC = {2, 1}; + std::vector stridesC = {1, 1}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + auto tC = AMSTensor::view(c.data(), + shapeC, + stridesC, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + tensors.push_back(AMSTensor::view(tC)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_SINGLE); + + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 6); + + auto* p = result.data(); + // Row 0: [1, 2, 10, 20, 30, 100] + CATCH_REQUIRE(p[0] == 1.0f); + CATCH_REQUIRE(p[1] == 2.0f); + CATCH_REQUIRE(p[2] == 10.0f); + CATCH_REQUIRE(p[3] == 20.0f); + CATCH_REQUIRE(p[4] == 30.0f); + CATCH_REQUIRE(p[5] == 100.0f); + // Row 1: [3, 4, 40, 50, 60, 200] + CATCH_REQUIRE(p[6] == 3.0f); + CATCH_REQUIRE(p[7] == 4.0f); + CATCH_REQUIRE(p[8] == 40.0f); + CATCH_REQUIRE(p[9] == 50.0f); + CATCH_REQUIRE(p[10] == 60.0f); + CATCH_REQUIRE(p[11] == 200.0f); +} + + +CATCH_TEST_CASE("float: concat 1D tensors", "[ams][tensor][float][concat]") +{ + AMSInit(); + std::vector a = {1.5f, 2.5f, 3.5f}; + std::vector b = {4.5f, 5.5f}; + + std::vector shapeA = {3}; + std::vector strides = {1}; + std::vector shapeB = {2}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + strides, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_SINGLE); + + CATCH_REQUIRE(result.dim() == 1); + CATCH_REQUIRE(result.shape()[0] == 5); + + auto* p = result.data(); + CATCH_REQUIRE(p[0] == 1.5f); + CATCH_REQUIRE(p[4] == 5.5f); +} + + +CATCH_TEST_CASE("float: concat result independent of source", + "[ams][tensor][float][concat]") +{ + AMSInit(); + std::vector a = {1.0f, 2.0f}; + std::vector b = {3.0f, 4.0f}; + + std::vector shape = {2}; + std::vector strides = {1}; + + auto tA = AMSTensor::view(a.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_SINGLE); + auto* p = result.data(); + + a[0] = 999.0f; + b[0] = 888.0f; + CATCH_REQUIRE(p[0] == 1.0f); + CATCH_REQUIRE(p[2] == 3.0f); +} + + +// ========================================================================= +// double — create +// ========================================================================= + +CATCH_TEST_CASE("double: create 1D tensor", "[ams][tensor][double][create]") +{ + AMSInit(); + const auto device = + GENERATE(AMSResourceType::AMS_HOST, AMSResourceType::AMS_DEVICE); + if (device == AMSResourceType::AMS_DEVICE) { + if (!ams::test::hasRuntimeDevice()) CATCH_SKIP("GPU device not available"); + } + + std::vector shape = {7}; + std::vector strides = {1}; + + auto tensor = AMSTensor::create(shape, strides, device); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(tensor.elements() == 7); + CATCH_REQUIRE(tensor.element_size() == sizeof(double)); + CATCH_REQUIRE(tensor.dim() == 1); + CATCH_REQUIRE(tensor.nbytes() == 7 * sizeof(double)); + CATCH_REQUIRE(tensor.location() == device); +} + + +CATCH_TEST_CASE("double: create 2D tensor", "[ams][tensor][double][create]") +{ + AMSInit(); + std::vector shape = {5, 3}; + std::vector strides = {3, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(tensor.elements() == 15); + CATCH_REQUIRE(tensor.dim() == 2); + CATCH_REQUIRE(tensor.nbytes() == 15 * sizeof(double)); +} + + +// ========================================================================= +// double — view +// ========================================================================= + +CATCH_TEST_CASE("double: view from existing buffer", + "[ams][tensor][double][view]") +{ + AMSInit(); + std::vector data = {1.11, 2.22, 3.33, 4.44}; + + std::vector shape = {4}; + std::vector strides = {1}; + + auto v = AMSTensor::view(data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(v.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(v.elements() == 4); + CATCH_REQUIRE(v.data() == data.data()); + CATCH_REQUIRE(v.data()[0] == 1.11); + CATCH_REQUIRE(v.data()[3] == 4.44); +} + + +// ========================================================================= +// double — data access +// ========================================================================= + +CATCH_TEST_CASE("double: write and read data", "[ams][tensor][double][data]") +{ + AMSInit(); + + std::vector shape = {3}; + std::vector strides = {1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* data = tensor.data(); + + data[0] = 1.0e-15; + data[1] = 3.141592653589793; + data[2] = 1.0e+15; + + CATCH_REQUIRE(data[0] == 1.0e-15); + CATCH_REQUIRE(data[1] == 3.141592653589793); + CATCH_REQUIRE(data[2] == 1.0e+15); +} + + +// ========================================================================= +// double — transpose +// ========================================================================= + +CATCH_TEST_CASE("double: transpose 2D tensor", + "[ams][tensor][double][transpose]") +{ + AMSInit(); + + std::vector shape = {4, 7}; + std::vector strides = {7, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto transposed = tensor.transpose(0, 1); + + CATCH_REQUIRE(transposed.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(transposed.shape()[0] == 7); + CATCH_REQUIRE(transposed.shape()[1] == 4); + CATCH_REQUIRE(transposed.elements() == 28); + CATCH_REQUIRE(!transposed.contiguous()); +} + + +// ========================================================================= +// double — move +// ========================================================================= + +CATCH_TEST_CASE("double: move constructor", "[ams][tensor][double][move]") +{ + AMSInit(); + std::vector shape = {6}; + std::vector strides = {1}; + + auto t1 = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* ptr = t1.data(); + + auto t2 = std::move(t1); + + CATCH_REQUIRE(t2.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(t2.elements() == 6); + CATCH_REQUIRE(t2.nbytes() == 6 * sizeof(double)); + CATCH_REQUIRE(t2.data() == ptr); +} + + +// ========================================================================= +// double — clone +// ========================================================================= + +CATCH_TEST_CASE("double: clone contiguous 2D tensor", + "[ams][tensor][double][clone]") +{ + AMSInit(); + std::vector src = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6}; + std::vector shape = {2, 3}; + std::vector strides = {3, 1}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + CATCH_REQUIRE(cloned.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(cloned.elements() == 6); + CATCH_REQUIRE(cloned.nbytes() == 6 * sizeof(double)); + CATCH_REQUIRE(cloned.contiguous()); + + auto* p = cloned.data(); + CATCH_REQUIRE(p != src.data()); + for (int i = 0; i < 6; ++i) + CATCH_REQUIRE(p[i] == src[i]); +} + + +CATCH_TEST_CASE("double: clone non-contiguous (transposed) tensor", + "[ams][tensor][double][clone][transpose]") +{ + AMSInit(); + // 2x4 row-major → transpose to 4x2 + std::vector src = {10, 20, 30, 40, 50, 60, 70, 80}; + std::vector shape = {2, 4}; + std::vector strides = {4, 1}; + + auto original = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto transposed = original.transpose(0, 1); + CATCH_REQUIRE(!transposed.contiguous()); + + auto cloned = transposed.clone(); + + CATCH_REQUIRE(cloned.contiguous()); + CATCH_REQUIRE(cloned.shape()[0] == 4); + CATCH_REQUIRE(cloned.shape()[1] == 2); + CATCH_REQUIRE(cloned.strides()[0] == 2); + CATCH_REQUIRE(cloned.strides()[1] == 1); + + // Logical [i,j] of transposed = src[j*4 + i] + auto* p = cloned.data(); + CATCH_REQUIRE(p[0] == 10.0); // [0,0] = src[0*4+0] + CATCH_REQUIRE(p[1] == 50.0); // [0,1] = src[1*4+0] + CATCH_REQUIRE(p[2] == 20.0); // [1,0] = src[0*4+1] + CATCH_REQUIRE(p[3] == 60.0); // [1,1] = src[1*4+1] + CATCH_REQUIRE(p[4] == 30.0); // [2,0] = src[0*4+2] + CATCH_REQUIRE(p[5] == 70.0); // [2,1] = src[1*4+2] + CATCH_REQUIRE(p[6] == 40.0); // [3,0] = src[0*4+3] + CATCH_REQUIRE(p[7] == 80.0); // [3,1] = src[1*4+3] +} + + +CATCH_TEST_CASE("double: clone is independent of source", + "[ams][tensor][double][clone]") +{ + AMSInit(); + std::vector src = {1.0, 2.0, 3.0}; + std::vector shape = {3}; + std::vector strides = {1}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + src[0] = 999.0; + CATCH_REQUIRE(cloned.data()[0] == 1.0); +} + + +// ========================================================================= +// double — concat +// ========================================================================= + +CATCH_TEST_CASE("double: concat two 2D tensors", + "[ams][tensor][double][concat]") +{ + AMSInit(); + std::vector a = {1.1, 2.2, 3.3, 4.4}; + std::vector b = {5.5, 6.6, 7.7, 8.8}; + + std::vector shape = {2, 2}; + std::vector strides = {2, 1}; + + auto tA = AMSTensor::view(a.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_DOUBLE); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 4); + + auto* p = result.data(); + // Row 0: [1.1, 2.2, 5.5, 6.6] + CATCH_REQUIRE(p[0] == 1.1); + CATCH_REQUIRE(p[1] == 2.2); + CATCH_REQUIRE(p[2] == 5.5); + CATCH_REQUIRE(p[3] == 6.6); + // Row 1: [3.3, 4.4, 7.7, 8.8] + CATCH_REQUIRE(p[4] == 3.3); + CATCH_REQUIRE(p[5] == 4.4); + CATCH_REQUIRE(p[6] == 7.7); + CATCH_REQUIRE(p[7] == 8.8); +} + + +CATCH_TEST_CASE("double: concat 1D tensors", "[ams][tensor][double][concat]") +{ + AMSInit(); + std::vector a = {1.0, 2.0}; + std::vector b = {3.0, 4.0, 5.0}; + std::vector shapeA = {2}; + std::vector strides = {1}; + std::vector shapeB = {3}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + strides, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_DOUBLE); + + CATCH_REQUIRE(result.dim() == 1); + CATCH_REQUIRE(result.shape()[0] == 5); + + auto* p = result.data(); + CATCH_REQUIRE(p[0] == 1.0); + CATCH_REQUIRE(p[4] == 5.0); +} + +// ========================================================================= +// dtype_to_size utility +// ========================================================================= + +CATCH_TEST_CASE("dtype_to_size: float types", "[ams][utils][float]") +{ + CATCH_REQUIRE(dtype_to_size(AMSDType::AMS_SINGLE) == sizeof(float)); + CATCH_REQUIRE(dtype_to_size(AMSDType::AMS_DOUBLE) == sizeof(double)); + CATCH_REQUIRE(dtype_to_size(AMSDType::AMS_DOUBLE) == + 2 * dtype_to_size(AMSDType::AMS_SINGLE)); +} diff --git a/tests/AMSlib/core/amstensor_int.cpp b/tests/AMSlib/core/amstensor_int.cpp new file mode 100644 index 00000000..68d35b26 --- /dev/null +++ b/tests/AMSlib/core/amstensor_int.cpp @@ -0,0 +1,873 @@ +/* + * Copyright 2021-2026 Lawrence Livermore National Security, LLC and other + * AMSLib Project Developers + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include +#include +#include +#include +#include + +#include "AMS.h" +#include "AMSTensor.hpp" +#include "ams_test_device.hpp" +#include "wf/resource_manager.hpp" +#include "wf/utils.hpp" + +using namespace ams; + +CATCH_TEST_CASE("int32: move resets metadata and padded clone preserves values", + "[ams][tensor][int32][move][clone]") +{ + using D = AMSTensor::IntDimType; + std::vector storage(8, -1); + storage[0] = 1; + storage[1] = 2; + storage[2] = 3; + storage[5] = 4; + storage[6] = 5; + storage[7] = 6; + auto padded = AMSTensor::view(storage.data(), + std::vector{2, 3}, + std::vector{5, 1}, + AMS_HOST); + auto clone = padded.clone(); + for (int i = 0; i < 6; ++i) + CATCH_REQUIRE(clone.data()[i] == i + 1); + CATCH_REQUIRE(clone.contiguous()); + + auto moved = std::move(clone); + CATCH_REQUIRE_FALSE(clone.valid()); + CATCH_REQUIRE_THROWS_AS(clone.nbytes(), std::logic_error); + CATCH_REQUIRE(moved.elements() == 6); +} + +CATCH_TEST_CASE("int64: randomized positive-stride clones match reference", + "[ams][tensor][int64][clone]") +{ + using D = AMSTensor::IntDimType; + std::mt19937 random(12345); + for (int trial = 0; trial < 100; ++trial) { + const D rows = 1 + random() % 7; + const D cols = 1 + random() % 7; + const D padding = random() % 5; + const D rowStride = cols + padding; + std::vector source( + static_cast((rows - 1) * rowStride + cols)); + for (D row = 0; row < rows; ++row) + for (D col = 0; col < cols; ++col) + source[static_cast(row * rowStride + col)] = row * 100 + col; + auto view = AMSTensor::view(source.data(), + std::vector{rows, cols}, + std::vector{rowStride, 1}, + AMS_HOST); + auto clone = view.clone(); + for (D row = 0; row < rows; ++row) + for (D col = 0; col < cols; ++col) + CATCH_REQUIRE(clone.data()[row * cols + col] == + row * 100 + col); + } +} + +// ========================================================================= +// int32_t — create +// ========================================================================= + +CATCH_TEST_CASE("int32: create 1D tensor", "[ams][tensor][int32][create]") +{ + AMSInit(); + const auto device = + GENERATE(AMSResourceType::AMS_HOST, AMSResourceType::AMS_DEVICE); + if (device == AMSResourceType::AMS_DEVICE) { + if (!ams::test::hasRuntimeDevice()) CATCH_SKIP("GPU device not available"); + } + + std::vector shape = {10}; + std::vector strides = {1}; + auto tensor = AMSTensor::create(shape, strides, device); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(tensor.elements() == 10); + CATCH_REQUIRE(tensor.element_size() == sizeof(int32_t)); + CATCH_REQUIRE(tensor.dim() == 1); + CATCH_REQUIRE(tensor.nbytes() == 10 * sizeof(int32_t)); + CATCH_REQUIRE(tensor.location() == device); + CATCH_REQUIRE(tensor.shape()[0] == 10); +} + + +CATCH_TEST_CASE("int32: create 2D tensor", "[ams][tensor][int32][create]") +{ + AMSInit(); + std::vector shape = {5, 8}; + std::vector strides = {8, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(tensor.elements() == 40); + CATCH_REQUIRE(tensor.dim() == 2); + CATCH_REQUIRE(tensor.shape()[0] == 5); + CATCH_REQUIRE(tensor.shape()[1] == 8); + CATCH_REQUIRE(tensor.nbytes() == 40 * sizeof(int32_t)); +} + + +CATCH_TEST_CASE("int32: create 3D tensor", "[ams][tensor][int32][create]") +{ + AMSInit(); + std::vector shape = {4, 3, 2}; + std::vector strides = {6, 2, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(tensor.elements() == 24); + CATCH_REQUIRE(tensor.dim() == 3); +} + + +// ========================================================================= +// int32_t — view +// ========================================================================= + +CATCH_TEST_CASE("int32: view 1D from existing buffer", + "[ams][tensor][int32][view]") +{ + AMSInit(); + std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + std::vector shape = {10}; + std::vector strides = {1}; + + auto v = AMSTensor::view(data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(v.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(v.elements() == 10); + CATCH_REQUIRE(v.data() == data.data()); // shares memory + CATCH_REQUIRE(v.data()[0] == 1); + CATCH_REQUIRE(v.data()[9] == 10); +} + + +CATCH_TEST_CASE("int32: view 2D from existing buffer", + "[ams][tensor][int32][view]") +{ + AMSInit(); + std::vector data(20, 42); + + std::vector shape = {4, 5}; + std::vector strides = {5, 1}; + + auto v = AMSTensor::view(data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(v.elements() == 20); + CATCH_REQUIRE(v.shape()[0] == 4); + CATCH_REQUIRE(v.shape()[1] == 5); + CATCH_REQUIRE(v.data()[0] == 42); + CATCH_REQUIRE(v.data()[19] == 42); +} + + +CATCH_TEST_CASE("int32: view from AMSTensor alias", + "[ams][tensor][int32][view]") +{ + AMSInit(); + std::vector shape = {6}; + std::vector strides = {1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* ptr = tensor.data(); + for (int i = 0; i < 6; ++i) + ptr[i] = i + 1; + + auto alias = AMSTensor::view(tensor); + + CATCH_REQUIRE(alias.data() == ptr); + CATCH_REQUIRE(alias.elements() == 6); + CATCH_REQUIRE(alias.data()[5] == 6); +} + + +// ========================================================================= +// int32_t — data access +// ========================================================================= + +CATCH_TEST_CASE("int32: write and read 1D data", "[ams][tensor][int32][data]") +{ + AMSInit(); + std::vector shape = {5}; + std::vector strides = {1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* data = tensor.data(); + + for (int i = 0; i < 5; ++i) + data[i] = i * 10; + + CATCH_REQUIRE(data[0] == 0); + CATCH_REQUIRE(data[1] == 10); + CATCH_REQUIRE(data[4] == 40); +} + + +CATCH_TEST_CASE("int32: write and read 2D data", "[ams][tensor][int32][data]") +{ + AMSInit(); + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* data = tensor.data(); + + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 4; ++j) + data[i * 4 + j] = i * 10 + j; + + CATCH_REQUIRE(data[0] == 0); // [0,0] + CATCH_REQUIRE(data[3] == 3); // [0,3] + CATCH_REQUIRE(data[4] == 10); // [1,0] + CATCH_REQUIRE(data[11] == 23); // [2,3] +} + + +// ========================================================================= +// int32_t — transpose +// ========================================================================= + +CATCH_TEST_CASE("int32: transpose 2D tensor", "[ams][tensor][int32][transpose]") +{ + AMSInit(); + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto transposed = tensor.transpose(0, 1); + + CATCH_REQUIRE(transposed.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(transposed.shape()[0] == 4); + CATCH_REQUIRE(transposed.shape()[1] == 3); + CATCH_REQUIRE(transposed.elements() == 12); + CATCH_REQUIRE(!transposed.contiguous()); +} + + +// ========================================================================= +// int32_t — move +// ========================================================================= + +CATCH_TEST_CASE("int32: move constructor", "[ams][tensor][int32][move]") +{ + AMSInit(); + std::vector shape = {10}; + std::vector strides = {1}; + + auto t1 = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* ptr = t1.data(); + + auto t2 = std::move(t1); + + CATCH_REQUIRE(t2.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(t2.elements() == 10); + CATCH_REQUIRE(t2.dim() == 1); + CATCH_REQUIRE(t2.nbytes() == 10 * sizeof(int32_t)); + CATCH_REQUIRE(t2.data() == ptr); +} + + +CATCH_TEST_CASE("int32: move assignment", "[ams][tensor][int32][move]") +{ + AMSInit(); + std::vector shape1 = {10}; + std::vector shape2 = {5}; + std::vector strides = {1}; + + auto t1 = + AMSTensor::create(shape1, strides, AMSResourceType::AMS_HOST); + auto t2 = + AMSTensor::create(shape2, strides, AMSResourceType::AMS_HOST); + auto* ptr1 = t1.data(); + + t2 = std::move(t1); + + CATCH_REQUIRE(t2.elements() == 10); + CATCH_REQUIRE(t2.data() == ptr1); +} + + +// ========================================================================= +// int32_t — clone +// ========================================================================= + +CATCH_TEST_CASE("int32: clone contiguous 2D tensor", + "[ams][tensor][int32][clone]") +{ + AMSInit(); + std::vector src(12); + for (int i = 0; i < 12; ++i) + src[i] = i * 7; + + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + CATCH_REQUIRE(cloned.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(cloned.dim() == 2); + CATCH_REQUIRE(cloned.shape()[0] == 3); + CATCH_REQUIRE(cloned.shape()[1] == 4); + CATCH_REQUIRE(cloned.elements() == 12); + CATCH_REQUIRE(cloned.contiguous()); + CATCH_REQUIRE(cloned.strides()[0] == 4); + CATCH_REQUIRE(cloned.strides()[1] == 1); + + auto* clonedPtr = cloned.data(); + CATCH_REQUIRE(clonedPtr != src.data()); + for (int i = 0; i < 12; ++i) { + CATCH_REQUIRE(clonedPtr[i] == src[i]); + } +} + + +CATCH_TEST_CASE("int32: clone non-contiguous (transposed) tensor", + "[ams][tensor][int32][clone][transpose]") +{ + AMSInit(); + // 3x4 row-major, transposed to 4x3 with strides [1,4] + std::vector src(12); + for (int i = 0; i < 12; ++i) + src[i] = i; + + std::vector shape = {3, 4}; + std::vector strides = {4, 1}; + + auto original = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto transposed = original.transpose(0, 1); + CATCH_REQUIRE(!transposed.contiguous()); + + auto cloned = transposed.clone(); + + CATCH_REQUIRE(cloned.contiguous()); + CATCH_REQUIRE(cloned.shape()[0] == 4); + CATCH_REQUIRE(cloned.shape()[1] == 3); + CATCH_REQUIRE(cloned.strides()[0] == 3); + CATCH_REQUIRE(cloned.strides()[1] == 1); + + // Logical element [i,j] of transposed is src[j*4 + i] + auto* p = cloned.data(); + for (int i = 0; i < 4; ++i) + for (int j = 0; j < 3; ++j) + CATCH_REQUIRE(p[i * 3 + j] == static_cast(j * 4 + i)); +} + + +CATCH_TEST_CASE("int32: clone is independent of source", + "[ams][tensor][int32][clone]") +{ + AMSInit(); + std::vector src = {10, 20, 30, 40}; + + std::vector shape = {4}; + std::vector strides = {1}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + src[0] = 999; + CATCH_REQUIRE(cloned.data()[0] == 10); +} + + +// ========================================================================= +// int32_t — concat +// ========================================================================= + +CATCH_TEST_CASE("int32: concat single tensor", "[ams][tensor][int32][concat]") +{ + AMSInit(); + std::vector a = {1, 2, 3, 4, 5, 6}; + std::vector shape = {2, 3}; + std::vector strides = {3, 1}; + + auto tA = AMSTensor::view(a.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT32); + + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 3); + auto* p = result.data(); + for (int i = 0; i < 6; ++i) + CATCH_REQUIRE(p[i] == a[i]); +} + + +CATCH_TEST_CASE("int32: concat two 2D tensors", "[ams][tensor][int32][concat]") +{ + AMSInit(); + // A:[3,2] B:[3,3] + // [1, 2] [10, 20, 30] + // [3, 4] [40, 50, 60] + // [5, 6] [70, 80, 90] + // + // Result: [3,5] + // [1, 2, 10, 20, 30] + // [3, 4, 40, 50, 60] + // [5, 6, 70, 80, 90] + std::vector a = {1, 2, 3, 4, 5, 6}; + std::vector b = {10, 20, 30, 40, 50, 60, 70, 80, 90}; + std::vector shapeA = {3, 2}; + std::vector stridesA = {2, 1}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + + std::vector shapeB = {3, 3}; + std::vector stridesB = {3, 1}; + + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT32); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(result.shape()[0] == 3); + CATCH_REQUIRE(result.shape()[1] == 5); + CATCH_REQUIRE(result.contiguous()); + + auto* p = result.data(); + std::vector expected = { + 1, 2, 10, 20, 30, 3, 4, 40, 50, 60, 5, 6, 70, 80, 90}; + for (int i = 0; i < 15; ++i) { + CATCH_INFO("index " << i); + CATCH_REQUIRE(p[i] == expected[i]); + } +} + + +CATCH_TEST_CASE("int32: concat three tensors", "[ams][tensor][int32][concat]") +{ + AMSInit(); + // A:[2,2] B:[2,1] C:[2,3] → [2,6] + std::vector a = {1, 2, 3, 4}; + std::vector b = {10, 20}; + std::vector c = {100, 200, 300, 400, 500, 600}; + + std::vector shapeA = {2, 2}; + std::vector stridesA = {2, 1}; + + std::vector shapeB = {2, 1}; + std::vector stridesB = {1, 1}; + + std::vector shapeC = {2, 3}; + std::vector stridesC = {3, 1}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + auto tC = AMSTensor::view(c.data(), + shapeC, + stridesC, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + tensors.push_back(AMSTensor::view(tC)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT32); + + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 6); + + auto* p = result.data(); + // Row 0: [1, 2, 10, 100, 200, 300] + CATCH_REQUIRE(p[0] == 1); + CATCH_REQUIRE(p[1] == 2); + CATCH_REQUIRE(p[2] == 10); + CATCH_REQUIRE(p[3] == 100); + CATCH_REQUIRE(p[4] == 200); + CATCH_REQUIRE(p[5] == 300); + // Row 1: [3, 4, 20, 400, 500, 600] + CATCH_REQUIRE(p[6] == 3); + CATCH_REQUIRE(p[7] == 4); + CATCH_REQUIRE(p[8] == 20); + CATCH_REQUIRE(p[9] == 400); + CATCH_REQUIRE(p[10] == 500); + CATCH_REQUIRE(p[11] == 600); +} + + +CATCH_TEST_CASE("int32: concat 1D tensors", "[ams][tensor][int32][concat]") +{ + AMSInit(); + std::vector a = {1, 2, 3}; + std::vector b = {4, 5}; + + std::vector shapeA = {3}; + std::vector shapeB = {2}; + std::vector strides = {1}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + strides, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT32); + + CATCH_REQUIRE(result.dim() == 1); + CATCH_REQUIRE(result.shape()[0] == 5); + + auto* p = result.data(); + CATCH_REQUIRE(p[0] == 1); + CATCH_REQUIRE(p[1] == 2); + CATCH_REQUIRE(p[2] == 3); + CATCH_REQUIRE(p[3] == 4); + CATCH_REQUIRE(p[4] == 5); +} + + +CATCH_TEST_CASE("int32: concat result independent of source", + "[ams][tensor][int32][concat]") +{ + AMSInit(); + std::vector a = {1, 2}; + std::vector b = {3, 4}; + + std::vector shape = {2}; + std::vector strides = {1}; + + auto tA = AMSTensor::view(a.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT32); + auto* p = result.data(); + + a[0] = 999; + b[0] = 888; + CATCH_REQUIRE(p[0] == 1); + CATCH_REQUIRE(p[2] == 3); +} + + +// ========================================================================= +// int64_t — create +// ========================================================================= + +CATCH_TEST_CASE("int64: create 1D tensor", "[ams][tensor][int64][create]") +{ + AMSInit(); + const auto device = + GENERATE(AMSResourceType::AMS_HOST, AMSResourceType::AMS_DEVICE); + if (device == AMSResourceType::AMS_DEVICE) { + if (!ams::test::hasRuntimeDevice()) CATCH_SKIP("GPU device not available"); + } + + std::vector shape = {15}; + std::vector strides = {1}; + + auto tensor = AMSTensor::create(shape, strides, device); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(tensor.elements() == 15); + CATCH_REQUIRE(tensor.element_size() == sizeof(int64_t)); + CATCH_REQUIRE(tensor.dim() == 1); + CATCH_REQUIRE(tensor.nbytes() == 15 * sizeof(int64_t)); + CATCH_REQUIRE(tensor.location() == device); +} + + +CATCH_TEST_CASE("int64: create 2D tensor", "[ams][tensor][int64][create]") +{ + AMSInit(); + std::vector shape = {6, 7}; + std::vector strides = {7, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(tensor.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(tensor.elements() == 42); + CATCH_REQUIRE(tensor.dim() == 2); + CATCH_REQUIRE(tensor.nbytes() == 42 * sizeof(int64_t)); +} + + +// ========================================================================= +// int64_t — view +// ========================================================================= + +CATCH_TEST_CASE("int64: view from existing buffer", + "[ams][tensor][int64][view]") +{ + AMSInit(); + std::vector data = {100, 200, 300, 400, 500}; + + std::vector shape = {5}; + std::vector strides = {1}; + + auto v = AMSTensor::view(data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + CATCH_REQUIRE(v.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(v.elements() == 5); + CATCH_REQUIRE(v.data()[0] == 100); + CATCH_REQUIRE(v.data()[4] == 500); +} + + +// ========================================================================= +// int64_t — data access +// ========================================================================= + +CATCH_TEST_CASE("int64: write and read large values", + "[ams][tensor][int64][data]") +{ + AMSInit(); + std::vector shape = {3}; + std::vector strides = {1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* data = tensor.data(); + + data[0] = 1000000000LL; + data[1] = 2000000000LL; + data[2] = 3000000000LL; + + CATCH_REQUIRE(data[0] == 1000000000LL); + CATCH_REQUIRE(data[1] == 2000000000LL); + CATCH_REQUIRE(data[2] == 3000000000LL); +} + + +// ========================================================================= +// int64_t — transpose +// ========================================================================= + +CATCH_TEST_CASE("int64: transpose 2D tensor", "[ams][tensor][int64][transpose]") +{ + AMSInit(); + std::vector shape = {5, 6}; + std::vector strides = {6, 1}; + + auto tensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto transposed = tensor.transpose(0, 1); + + CATCH_REQUIRE(transposed.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(transposed.shape()[0] == 6); + CATCH_REQUIRE(transposed.shape()[1] == 5); + CATCH_REQUIRE(transposed.elements() == 30); +} + + +// ========================================================================= +// int64_t — move +// ========================================================================= + +CATCH_TEST_CASE("int64: move constructor", "[ams][tensor][int64][move]") +{ + AMSInit(); + std::vector shape = {20}; + std::vector strides = {1}; + + auto t1 = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto* ptr = t1.data(); + + auto t2 = std::move(t1); + + CATCH_REQUIRE(t2.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(t2.elements() == 20); + CATCH_REQUIRE(t2.nbytes() == 20 * sizeof(int64_t)); + CATCH_REQUIRE(t2.data() == ptr); +} + + +// ========================================================================= +// int64_t — clone +// ========================================================================= + +CATCH_TEST_CASE("int64: clone contiguous tensor", "[ams][tensor][int64][clone]") +{ + AMSInit(); + std::vector src = {100, 200, 300, 400, 500, 600}; + std::vector shape = {3, 2}; + std::vector strides = {2, 1}; + + auto view = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto cloned = view.clone(); + + CATCH_REQUIRE(cloned.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(cloned.elements() == 6); + CATCH_REQUIRE(cloned.nbytes() == 6 * sizeof(int64_t)); + CATCH_REQUIRE(cloned.contiguous()); + + auto* p = cloned.data(); + CATCH_REQUIRE(p != src.data()); + for (int i = 0; i < 6; ++i) + CATCH_REQUIRE(p[i] == src[i]); +} + + +CATCH_TEST_CASE("int64: clone non-contiguous tensor", + "[ams][tensor][int64][clone][transpose]") +{ + AMSInit(); + // 2x3 row-major, transposed to 3x2 + std::vector src = {10, 20, 30, 40, 50, 60}; + std::vector shape = {2, 3}; + std::vector strides = {3, 1}; + + auto original = AMSTensor::view(src.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto transposed = original.transpose(0, 1); + auto cloned = transposed.clone(); + + CATCH_REQUIRE(cloned.contiguous()); + CATCH_REQUIRE(cloned.shape()[0] == 3); + CATCH_REQUIRE(cloned.shape()[1] == 2); + CATCH_REQUIRE(cloned.strides()[0] == 2); + CATCH_REQUIRE(cloned.strides()[1] == 1); + + // Logical [i,j] of transposed = src[j*3 + i] + auto* p = cloned.data(); + CATCH_REQUIRE(p[0] == 10); // [0,0] = src[0*3+0] + CATCH_REQUIRE(p[1] == 40); // [0,1] = src[1*3+0] + CATCH_REQUIRE(p[2] == 20); // [1,0] = src[0*3+1] + CATCH_REQUIRE(p[3] == 50); // [1,1] = src[1*3+1] + CATCH_REQUIRE(p[4] == 30); // [2,0] = src[0*3+2] + CATCH_REQUIRE(p[5] == 60); // [2,1] = src[1*3+2] +} + + +// ========================================================================= +// int64_t — concat +// ========================================================================= + +CATCH_TEST_CASE("int64: concat two 2D tensors", "[ams][tensor][int64][concat]") +{ + AMSInit(); + std::vector a = {1, 2, 3, 4}; + std::vector b = {10, 20, 30, 40, 50, 60}; + std::vector shapeA = {2, 2}; + std::vector stridesA = {2, 1}; + std::vector shapeB = {2, 3}; + std::vector stridesB = {3, 1}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT64); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 5); + + auto* p = result.data(); + // Row 0: [1, 2, 10, 20, 30] + CATCH_REQUIRE(p[0] == 1); + CATCH_REQUIRE(p[1] == 2); + CATCH_REQUIRE(p[2] == 10); + CATCH_REQUIRE(p[3] == 20); + CATCH_REQUIRE(p[4] == 30); + // Row 1: [3, 4, 40, 50, 60] + CATCH_REQUIRE(p[5] == 3); + CATCH_REQUIRE(p[6] == 4); + CATCH_REQUIRE(p[7] == 40); + CATCH_REQUIRE(p[8] == 50); + CATCH_REQUIRE(p[9] == 60); +} + + +// ========================================================================= +// dtype_to_size utility +// ========================================================================= + +CATCH_TEST_CASE("dtype_to_size: integer types", "[ams][utils][int]") +{ + CATCH_REQUIRE(dtype_to_size(AMSDType::AMS_INT32) == sizeof(int32_t)); + CATCH_REQUIRE(dtype_to_size(AMSDType::AMS_INT64) == sizeof(int64_t)); + CATCH_REQUIRE(dtype_to_size(AMSDType::AMS_INT64) == + 2 * dtype_to_size(AMSDType::AMS_INT32)); +} diff --git a/tests/AMSlib/core/amstensor_mixed.cpp b/tests/AMSlib/core/amstensor_mixed.cpp new file mode 100644 index 00000000..9b5bcf35 --- /dev/null +++ b/tests/AMSlib/core/amstensor_mixed.cpp @@ -0,0 +1,592 @@ +/* + * Copyright 2021-2026 Lawrence Livermore National Security, LLC and other + * AMSLib Project Developers + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include +#include +#include + +#include "AMS.h" +#include "AMSTensor.hpp" +#include "wf/resource_manager.hpp" +#include "wf/utils.hpp" + +using namespace ams; + +CATCH_TEST_CASE("mixed: scalar empty and padded storage", + "[ams][tensor][mixed][layout]") +{ + using D = AMSTensor::IntDimType; + auto scalar = AMSTensor::create({}, {}, AMS_HOST); + CATCH_REQUIRE(scalar.elements() == 1); + CATCH_REQUIRE(scalar.nbytes() == sizeof(int64_t)); + CATCH_REQUIRE(scalar.storage_nbytes() == sizeof(int64_t)); + + auto empty = AMSTensor::create(std::vector{3, 0, 2}, + std::vector{2, 2, 1}, + AMS_HOST); + CATCH_REQUIRE(empty.elements() == 0); + CATCH_REQUIRE(empty.nbytes() == 0); + + auto padded = AMSTensor::create(std::vector{2, 3}, + std::vector{5, 1}, + AMS_HOST); + CATCH_REQUIRE(padded.nbytes() == 6 * sizeof(int32_t)); + CATCH_REQUIRE(padded.storage_nbytes() == 8 * sizeof(int32_t)); + CATCH_REQUIRE_FALSE(padded.contiguous()); +} + +// ========================================================================= +// SmallVector holding mixed-dtype tensors +// ========================================================================= + +CATCH_TEST_CASE("mixed: SmallVector of all four dtypes", + "[ams][tensor][mixed][smallvector]") +{ + AMSInit(); + ams::SmallVector tensors; + std::vector shape = {8}; + std::vector strides = {1}; + + tensors.push_back( + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST)); + tensors.push_back( + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST)); + tensors.push_back( + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST)); + tensors.push_back( + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST)); + + CATCH_REQUIRE(tensors.size() == 4); + CATCH_REQUIRE(tensors[0].dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(tensors[1].dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(tensors[2].dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(tensors[3].dtype() == AMSDType::AMS_INT64); + + // All have the same number of elements + for (auto& t : tensors) + CATCH_REQUIRE(t.elements() == 8); + + // But different byte sizes + CATCH_REQUIRE(tensors[0].nbytes() == 8 * sizeof(float)); + CATCH_REQUIRE(tensors[1].nbytes() == 8 * sizeof(double)); + CATCH_REQUIRE(tensors[2].nbytes() == 8 * sizeof(int32_t)); + CATCH_REQUIRE(tensors[3].nbytes() == 8 * sizeof(int64_t)); + + // element_size matches each type + CATCH_REQUIRE(tensors[0].element_size() == sizeof(float)); + CATCH_REQUIRE(tensors[1].element_size() == sizeof(double)); + CATCH_REQUIRE(tensors[2].element_size() == sizeof(int32_t)); + CATCH_REQUIRE(tensors[3].element_size() == sizeof(int64_t)); +} + + +CATCH_TEST_CASE("mixed: SmallVector with different shapes per dtype", + "[ams][tensor][mixed][smallvector]") +{ + AMSInit(); + ams::SmallVector tensors; + + std::vector shapeA = {3, 4}; + std::vector stridesA = {4, 1}; + + std::vector shapeB = {5}; + std::vector stridesB = {1}; + + std::vector shapeC = {2, 2, 2}; + std::vector stridesC = {4, 2, 1}; + + tensors.push_back( + AMSTensor::create(shapeA, stridesA, AMSResourceType::AMS_HOST)); + tensors.push_back( + AMSTensor::create(shapeB, stridesB, AMSResourceType::AMS_HOST)); + tensors.push_back( + AMSTensor::create(shapeC, stridesC, AMSResourceType::AMS_HOST)); + + CATCH_REQUIRE(tensors[0].dim() == 2); + CATCH_REQUIRE(tensors[0].elements() == 12); + CATCH_REQUIRE(tensors[1].dim() == 1); + CATCH_REQUIRE(tensors[1].elements() == 5); + CATCH_REQUIRE(tensors[2].dim() == 3); + CATCH_REQUIRE(tensors[2].elements() == 8); +} + + +// ========================================================================= +// Clone preserves dtype across all types +// ========================================================================= + +CATCH_TEST_CASE("mixed: clone preserves dtype for all four types", + "[ams][tensor][mixed][clone]") +{ + AMSInit(); + + // float + std::vector fData = {1.0f, 2.0f, 3.0f}; + std::vector shape = {3}; + std::vector strides = {1}; + + auto fView = AMSTensor::view(fData.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto fClone = fView.clone(); + CATCH_REQUIRE(fClone.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(fClone.data()[2] == 3.0f); + + // double + std::vector dData = {10.0, 20.0, 30.0}; + auto dView = AMSTensor::view(dData.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto dClone = dView.clone(); + CATCH_REQUIRE(dClone.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(dClone.data()[2] == 30.0); + + // int32 + std::vector i32Data = {100, 200, 300}; + auto i32View = AMSTensor::view(i32Data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto i32Clone = i32View.clone(); + CATCH_REQUIRE(i32Clone.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(i32Clone.data()[2] == 300); + + // int64 + std::vector i64Data = {1000, 2000, 3000}; + auto i64View = AMSTensor::view(i64Data.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto i64Clone = i64View.clone(); + CATCH_REQUIRE(i64Clone.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(i64Clone.data()[2] == 3000); +} + +CATCH_TEST_CASE("mixed: clone all types in a SmallVector", + "[ams][tensor][mixed][clone]") +{ + AMSInit(); + std::vector fData = {1.0f, 2.0f}; + std::vector dData = {3.0, 4.0}; + std::vector i32Data = {5, 6}; + std::vector i64Data = {7, 8}; + + std::vector shape = {2}; + std::vector strides = {1}; + + ams::SmallVector originals; + originals.push_back(AMSTensor::view( + fData.data(), shape, strides, AMSResourceType::AMS_HOST)); + originals.push_back(AMSTensor::view( + dData.data(), shape, strides, AMSResourceType::AMS_HOST)); + originals.push_back(AMSTensor::view( + i32Data.data(), shape, strides, AMSResourceType::AMS_HOST)); + originals.push_back(AMSTensor::view( + i64Data.data(), shape, strides, AMSResourceType::AMS_HOST)); + + ams::SmallVector clones; + for (auto& t : originals) + clones.push_back(t.clone()); + + // Mutate all source buffers + fData[0] = 999.0f; + dData[0] = 999.0; + i32Data[0] = 999; + i64Data[0] = 999; + + // Clones must be unaffected + CATCH_REQUIRE(clones[0].data()[0] == 1.0f); + CATCH_REQUIRE(clones[1].data()[0] == 3.0); + CATCH_REQUIRE(clones[2].data()[0] == 5); + CATCH_REQUIRE(clones[3].data()[0] == 7); + + // Dtype preserved + CATCH_REQUIRE(clones[0].dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(clones[1].dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(clones[2].dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(clones[3].dtype() == AMSDType::AMS_INT64); +} + + +// ========================================================================= +// Concat: same operation across all four types +// ========================================================================= + +CATCH_TEST_CASE("mixed: concat two 2D tensors for each dtype", + "[ams][tensor][mixed][concat]") +{ + AMSInit(); + + // Pattern: A:[2,2] + B:[2,3] → [2,5] + + std::vector shapeA = {2, 2}; + std::vector stridesA = {2, 1}; + + std::vector shapeB = {2, 3}; + std::vector stridesB = {3, 1}; + + CATCH_SECTION("float") + { + std::vector a = {1, 2, 3, 4}; + std::vector b = {10, 20, 30, 40, 50, 60}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_SINGLE); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 5); + auto* p = result.data(); + CATCH_REQUIRE(p[0] == 1.0f); + CATCH_REQUIRE(p[2] == 10.0f); + CATCH_REQUIRE(p[5] == 3.0f); + CATCH_REQUIRE(p[7] == 40.0f); + } + + CATCH_SECTION("double") + { + std::vector a = {1, 2, 3, 4}; + std::vector b = {10, 20, 30, 40, 50, 60}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_DOUBLE); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 5); + auto* p = result.data(); + CATCH_REQUIRE(p[0] == 1.0); + CATCH_REQUIRE(p[2] == 10.0); + CATCH_REQUIRE(p[5] == 3.0); + CATCH_REQUIRE(p[7] == 40.0); + } + + CATCH_SECTION("int32") + { + std::vector a = {1, 2, 3, 4}; + std::vector b = {10, 20, 30, 40, 50, 60}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT32); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 5); + auto* p = result.data(); + CATCH_REQUIRE(p[0] == 1); + CATCH_REQUIRE(p[2] == 10); + CATCH_REQUIRE(p[5] == 3); + CATCH_REQUIRE(p[7] == 40); + } + + CATCH_SECTION("int64") + { + std::vector a = {1, 2, 3, 4}; + std::vector b = {10, 20, 30, 40, 50, 60}; + + auto tA = AMSTensor::view(a.data(), + shapeA, + stridesA, + AMSResourceType::AMS_HOST); + auto tB = AMSTensor::view(b.data(), + shapeB, + stridesB, + AMSResourceType::AMS_HOST); + + ams::SmallVector tensors; + tensors.push_back(AMSTensor::view(tA)); + tensors.push_back(AMSTensor::view(tB)); + + auto result = AMSTensor::concat(tensors, AMSDType::AMS_INT64); + + CATCH_REQUIRE(result.dtype() == AMSDType::AMS_INT64); + CATCH_REQUIRE(result.shape()[0] == 2); + CATCH_REQUIRE(result.shape()[1] == 5); + auto* p = result.data(); + CATCH_REQUIRE(p[0] == 1); + CATCH_REQUIRE(p[2] == 10); + CATCH_REQUIRE(p[5] == 3); + CATCH_REQUIRE(p[7] == 40); + } +} + + +// ========================================================================= +// Interleaved operations across types +// ========================================================================= + +CATCH_TEST_CASE("mixed: create, write, clone, verify across types", + "[ams][tensor][mixed][interleaved]") +{ + AMSInit(); + + std::vector shape = {2, 3}; + std::vector strides = {3, 1}; + + // Create a float tensor and an int32 tensor with the same shape + auto fTensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto iTensor = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + // Write the same logical values + auto* fPtr = fTensor.data(); + auto* iPtr = iTensor.data(); + for (int i = 0; i < 6; ++i) { + fPtr[i] = static_cast(i + 1); + iPtr[i] = i + 1; + } + + // Clone both + auto fClone = fTensor.clone(); + auto iClone = iTensor.clone(); + + // Verify dtypes didn't get mixed up + CATCH_REQUIRE(fClone.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(iClone.dtype() == AMSDType::AMS_INT32); + + // Verify data + for (int i = 0; i < 6; ++i) { + CATCH_REQUIRE(fClone.data()[i] == static_cast(i + 1)); + CATCH_REQUIRE(iClone.data()[i] == i + 1); + } + + // Clones are independent + fPtr[0] = 999.0f; + iPtr[0] = 999; + CATCH_REQUIRE(fClone.data()[0] == 1.0f); + CATCH_REQUIRE(iClone.data()[0] == 1); +} + + +CATCH_TEST_CASE("mixed: concat float then concat int32 independently", + "[ams][tensor][mixed][interleaved][concat]") +{ + AMSInit(); + // Two float tensors + std::vector fA = {1.0f, 2.0f}; + std::vector fB = {3.0f, 4.0f}; + + // Two int32 tensors with same values + std::vector iA = {1, 2}; + std::vector iB = {3, 4}; + + std::vector shape = {2}; + std::vector strides = {1}; + + auto ftA = AMSTensor::view(fA.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto ftB = AMSTensor::view(fB.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto itA = AMSTensor::view(iA.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto itB = AMSTensor::view(iB.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + + ams::SmallVector fTensors; + fTensors.push_back(AMSTensor::view(ftA)); + fTensors.push_back(AMSTensor::view(ftB)); + + ams::SmallVector iTensors; + iTensors.push_back(AMSTensor::view(itA)); + iTensors.push_back(AMSTensor::view(itB)); + + auto fResult = AMSTensor::concat(fTensors, AMSDType::AMS_SINGLE); + auto iResult = AMSTensor::concat(iTensors, AMSDType::AMS_INT32); + + // Both produce [4] shaped results with values [1, 2, 3, 4] + CATCH_REQUIRE(fResult.shape()[0] == 4); + CATCH_REQUIRE(iResult.shape()[0] == 4); + CATCH_REQUIRE(fResult.dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(iResult.dtype() == AMSDType::AMS_INT32); + + for (int i = 0; i < 4; ++i) { + CATCH_REQUIRE(fResult.data()[i] == static_cast(i + 1)); + CATCH_REQUIRE(iResult.data()[i] == i + 1); + } +} + + +// ========================================================================= +// Move across types in a SmallVector +// ========================================================================= + +CATCH_TEST_CASE("mixed: move tensors into SmallVector preserves types", + "[ams][tensor][mixed][move]") +{ + AMSInit(); + std::vector shape = {4}; + std::vector strides = {1}; + + auto f = AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto d = AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto i32 = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + auto i64 = + AMSTensor::create(shape, strides, AMSResourceType::AMS_HOST); + + auto* fPtr = f.data(); + auto* dPtr = d.data(); + auto* i32Ptr = i32.data(); + auto* i64Ptr = i64.data(); + + ams::SmallVector vec; + vec.push_back(std::move(f)); + vec.push_back(std::move(d)); + vec.push_back(std::move(i32)); + vec.push_back(std::move(i64)); + + // Types preserved after move + CATCH_REQUIRE(vec[0].dtype() == AMSDType::AMS_SINGLE); + CATCH_REQUIRE(vec[1].dtype() == AMSDType::AMS_DOUBLE); + CATCH_REQUIRE(vec[2].dtype() == AMSDType::AMS_INT32); + CATCH_REQUIRE(vec[3].dtype() == AMSDType::AMS_INT64); + + // Pointers transferred (not copied) + CATCH_REQUIRE(vec[0].data() == fPtr); + CATCH_REQUIRE(vec[1].data() == dPtr); + CATCH_REQUIRE(vec[2].data() == i32Ptr); + CATCH_REQUIRE(vec[3].data() == i64Ptr); + + // Sizes preserved + for (auto& t : vec) { + CATCH_REQUIRE(t.elements() == 4); + CATCH_REQUIRE(t.dim() == 1); + } + + // Byte sizes differ + CATCH_REQUIRE(vec[0].nbytes() == 4 * sizeof(float)); + CATCH_REQUIRE(vec[1].nbytes() == 4 * sizeof(double)); + CATCH_REQUIRE(vec[2].nbytes() == 4 * sizeof(int32_t)); + CATCH_REQUIRE(vec[3].nbytes() == 4 * sizeof(int64_t)); +} + + +// ========================================================================= +// Transpose + clone across types +// ========================================================================= + +CATCH_TEST_CASE("mixed: transpose then clone preserves dtype", + "[ams][tensor][mixed][transpose][clone]") +{ + AMSInit(); + std::vector shape = {2, 3}; + std::vector strides = {3, 1}; + + // float: 2x3 → transpose → 3x2 → clone + std::vector fSrc = {1, 2, 3, 4, 5, 6}; + auto fOrig = AMSTensor::view(fSrc.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto fTransposed = fOrig.transpose(0, 1); + auto fClone = fTransposed.clone(); + + // int32: same layout + std::vector iSrc = {1, 2, 3, 4, 5, 6}; + auto iOrig = AMSTensor::view(iSrc.data(), + shape, + strides, + AMSResourceType::AMS_HOST); + auto iTransposed = iOrig.transpose(0, 1); + auto iClone = iTransposed.clone(); + + // Both clones: shape [3,2], contiguous + CATCH_REQUIRE(fClone.shape()[0] == 3); + CATCH_REQUIRE(fClone.shape()[1] == 2); + CATCH_REQUIRE(fClone.contiguous()); + CATCH_REQUIRE(fClone.dtype() == AMSDType::AMS_SINGLE); + + CATCH_REQUIRE(iClone.shape()[0] == 3); + CATCH_REQUIRE(iClone.shape()[1] == 2); + CATCH_REQUIRE(iClone.contiguous()); + CATCH_REQUIRE(iClone.dtype() == AMSDType::AMS_INT32); + + // Logical element [i,j] of transposed = src[j*3 + i] + // Both should have identical values (just different types) + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 2; ++j) { + float fExpected = static_cast(j * 3 + i + 1); + int32_t iExpected = j * 3 + i + 1; + CATCH_INFO("[" << i << "," << j << "]"); + CATCH_REQUIRE(fClone.data()[i * 2 + j] == fExpected); + CATCH_REQUIRE(iClone.data()[i * 2 + j] == iExpected); + } + } +} + + +// ========================================================================= +// dtype_to_size: cross-type relationships +// ========================================================================= + +CATCH_TEST_CASE("dtype_to_size: cross-type size relationships", + "[ams][utils][mixed]") +{ + size_t sFloat = dtype_to_size(AMSDType::AMS_SINGLE); + size_t sDouble = dtype_to_size(AMSDType::AMS_DOUBLE); + size_t sInt32 = dtype_to_size(AMSDType::AMS_INT32); + size_t sInt64 = dtype_to_size(AMSDType::AMS_INT64); + + // float == int32 == 4 bytes + CATCH_REQUIRE(sFloat == sInt32); + CATCH_REQUIRE(sFloat == 4); + + // double == int64 == 8 bytes + CATCH_REQUIRE(sDouble == sInt64); + CATCH_REQUIRE(sDouble == 8); + + // 8-byte types are twice the 4-byte types + CATCH_REQUIRE(sDouble == 2 * sFloat); + CATCH_REQUIRE(sInt64 == 2 * sInt32); +} diff --git a/tests/AMSlib/core/amstensor_torch_benchmark.cpp b/tests/AMSlib/core/amstensor_torch_benchmark.cpp new file mode 100644 index 00000000..7c82fc27 --- /dev/null +++ b/tests/AMSlib/core/amstensor_torch_benchmark.cpp @@ -0,0 +1,582 @@ +/* + * Copyright 2021-2026 Lawrence Livermore National Security, LLC and other + * AMSLib Project Developers + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AMS.h" +#include "AMSTensor.hpp" +#include "AMSTorchInterop.hpp" +#include "ams_catch_main.hpp" + +namespace +{ +using ams::AMSTensor; +using Dim = AMSTensor::IntDimType; + +constexpr int64_t benchmarkSizes[] = {16, 128, 1024}; + +std::string utcTimestamp() +{ + const std::time_t now = std::time(nullptr); + std::tm utc{}; + if (!::gmtime_r(&now, &utc)) + throw std::runtime_error("Unable to construct benchmark UTC timestamp"); + std::ostringstream result; + result << std::put_time(&utc, "%Y-%m-%dT%H:%M:%SZ"); + return result.str(); +} + +std::string csvField(const std::string& value) +{ + if (value.find_first_of(",\"\r\n") == std::string::npos) return value; + std::string result = "\""; + for (const char character : value) { + result += character; + if (character == '\"') result += '\"'; + } + result += '\"'; + return result; +} + +bool hasContent(const std::string& path) +{ + std::ifstream input(path, std::ios::binary | std::ios::ate); + return input && input.tellg() > 0; +} + +class CsvBenchmarkListener : public Catch::EventListenerBase +{ +public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(const Catch::TestRunInfo&) override + { + const char* configuredPath = std::getenv("AMS_TENSOR_BENCHMARK_CSV"); + _path = configuredPath && *configuredPath + ? configuredPath + : AMS_TENSOR_BENCHMARK_CSV_DEFAULT; + const bool writeHeader = !hasContent(_path); + _output.open(_path, std::ios::out | std::ios::app); + if (!_output) + throw std::runtime_error("Unable to open benchmark CSV: " + _path); + if (writeHeader) { + _output << "timestamp_utc,ams_commit,source_state,benchmark,samples," + "iterations_per_sample,mean_ns,mean_lower_ns,mean_upper_ns," + "stddev_ns,stddev_lower_ns,stddev_upper_ns,outliers," + "confidence_interval\n"; + } + _timestamp = utcTimestamp(); + } + + void benchmarkEnded(const Catch::BenchmarkStats<>& stats) override + { + _output << csvField(_timestamp) << ',' << csvField(AMS_BENCHMARK_GIT_COMMIT) + << ',' << csvField(AMS_BENCHMARK_SOURCE_STATE) << ',' + << csvField(stats.info.name) << ',' << stats.info.samples << ',' + << stats.info.iterations << ',' << std::setprecision(17) + << stats.mean.point.count() << ',' << stats.mean.lower_bound.count() + << ',' << stats.mean.upper_bound.count() << ',' + << stats.standardDeviation.point.count() << ',' + << stats.standardDeviation.lower_bound.count() << ',' + << stats.standardDeviation.upper_bound.count() << ',' + << stats.outliers.total() << ',' << stats.mean.confidence_interval + << '\n'; + _output.flush(); + if (!_output) + throw std::runtime_error("Unable to write benchmark CSV: " + _path); + } + +private: + std::ofstream _output; + std::string _path; + std::string _timestamp; +}; + +CATCH_REGISTER_LISTENER(CsvBenchmarkListener) + +template +struct TypeInfo; + +template <> +struct TypeInfo { + static constexpr const char* name = "float32"; + static constexpr ams::AMSDType amsDtype = ams::AMS_SINGLE; + static constexpr c10::ScalarType torchDtype = torch::kFloat32; +}; + +template <> +struct TypeInfo { + static constexpr const char* name = "float64"; + static constexpr ams::AMSDType amsDtype = ams::AMS_DOUBLE; + static constexpr c10::ScalarType torchDtype = torch::kFloat64; +}; + +template <> +struct TypeInfo { + static constexpr const char* name = "int32"; + static constexpr ams::AMSDType amsDtype = ams::AMS_INT32; + static constexpr c10::ScalarType torchDtype = torch::kInt32; +}; + +template <> +struct TypeInfo { + static constexpr const char* name = "int64"; + static constexpr ams::AMSDType amsDtype = ams::AMS_INT64; + static constexpr c10::ScalarType torchDtype = torch::kInt64; +}; + +std::string benchmarkName(const char* implementation, + const char* operation, + const char* dtype, + const char* layout, + int64_t rows, + int64_t columns) +{ + return std::string(implementation) + "/" + operation + "/" + dtype + "/" + + layout + "/" + std::to_string(rows) + "x" + std::to_string(columns); +} + +std::vector shape(int64_t rows, int64_t columns) +{ + return {static_cast(rows), static_cast(columns)}; +} + +std::vector strides(int64_t columns) +{ + return {static_cast(columns), 1}; +} + +template +AMSTensor makeAmsTensor(int64_t rows, int64_t columns) +{ + auto result = AMSTensor::create(shape(rows, columns), + strides(columns), + ams::AMS_HOST); + std::fill_n(result.template data(), result.elements(), T{1}); + return result; +} + +template +torch::Tensor makeTorchTensor(int64_t rows, int64_t columns) +{ + auto options = + torch::TensorOptions().dtype(TypeInfo::torchDtype).device(torch::kCPU); + auto result = torch::empty({rows, columns}, options); + result.fill_(1); + return result; +} + +template +void benchmarkAllocation(int64_t side) +{ + const auto amsName = benchmarkName( + "AMS", "allocation", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(amsName)) + { + size_t bytes = 0; + { + auto result = + AMSTensor::create(shape(side, side), strides(side), ams::AMS_HOST); + bytes = result.nbytes(); + } + return bytes; + }; + + const auto torchName = benchmarkName( + "Torch", "allocation", TypeInfo::name, "contiguous", side, side); + const auto options = + torch::TensorOptions().dtype(TypeInfo::torchDtype).device(torch::kCPU); + CATCH_BENCHMARK(std::string(torchName)) + { + int64_t elements = 0; + { + auto result = torch::empty({side, side}, options); + elements = result.numel(); + } + return elements; + }; +} + +template +void benchmarkViews(int64_t side) +{ + const size_t elements = static_cast(side * side); + std::vector raw(elements, T{1}); + const auto options = + torch::TensorOptions().dtype(TypeInfo::torchDtype).device(torch::kCPU); + + const auto amsRawName = benchmarkName( + "AMS", "raw_view", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(amsRawName)) + { + const void* pointer = nullptr; + { + auto result = AMSTensor::view(raw.data(), + shape(side, side), + strides(side), + ams::AMS_HOST); + pointer = result.data_ptr(); + } + return pointer; + }; + + const auto torchRawName = benchmarkName( + "Torch", "raw_view", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(torchRawName)) + { + const void* pointer = nullptr; + { + auto result = torch::from_blob(raw.data(), {side, side}, options); + pointer = result.data_ptr(); + } + return pointer; + }; + + auto amsSource = makeAmsTensor(side, side); + auto torchSource = makeTorchTensor(side, side); + const auto amsAliasName = benchmarkName( + "AMS", "retained_alias", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(amsAliasName)) + { + const void* pointer = nullptr; + { + auto result = AMSTensor::view(amsSource); + pointer = result.data_ptr(); + } + return pointer; + }; + + const auto torchAliasName = benchmarkName( + "Torch", "retained_alias", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(torchAliasName)) + { + const void* pointer = nullptr; + { + auto result = torchSource.alias(); + pointer = result.data_ptr(); + } + return pointer; + }; +} + +template +void benchmarkMetadataAndTranspose(int64_t side) +{ + auto amsSource = makeAmsTensor(side, side); + auto torchSource = makeTorchTensor(side, side); + + const auto amsMetadataName = benchmarkName( + "AMS", "metadata_batch16", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(amsMetadataName)) + { + size_t total = 0; + for (int batch = 0; batch < 16; ++batch) { + total += static_cast(amsSource.elements()); + total += amsSource.nbytes(); + total += amsSource.dim(); + total += static_cast(amsSource.shape()[0] + amsSource.shape()[1]); + total += + static_cast(amsSource.strides()[0] + amsSource.strides()[1]); + total += static_cast(amsSource.contiguous()); + } + return total; + }; + + const auto torchMetadataName = benchmarkName( + "Torch", "metadata_batch16", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(torchMetadataName)) + { + size_t total = 0; + for (int batch = 0; batch < 16; ++batch) { + total += static_cast(torchSource.numel()); + total += + static_cast(torchSource.numel() * torchSource.element_size()); + total += static_cast(torchSource.dim()); + total += static_cast(torchSource.size(0) + torchSource.size(1)); + total += + static_cast(torchSource.stride(0) + torchSource.stride(1)); + total += static_cast(torchSource.is_contiguous()); + } + return total; + }; + + const auto amsTransposeName = benchmarkName( + "AMS", "transpose", TypeInfo::name, "strided", side, side); + CATCH_BENCHMARK(std::string(amsTransposeName)) + { + const void* pointer = nullptr; + { + auto result = amsSource.transpose(0, 1); + pointer = result.data_ptr(); + } + return pointer; + }; + + const auto torchTransposeName = benchmarkName( + "Torch", "transpose", TypeInfo::name, "strided", side, side); + CATCH_BENCHMARK(std::string(torchTransposeName)) + { + const void* pointer = nullptr; + { + auto result = torchSource.transpose(0, 1); + pointer = result.data_ptr(); + } + return pointer; + }; +} + +template +void benchmarkClone(int64_t side) +{ + auto amsBase = makeAmsTensor(side, side); + auto amsTransposed = amsBase.transpose(0, 1); + auto torchBase = makeTorchTensor(side, side); + auto torchTransposed = torchBase.transpose(0, 1); + + const auto amsContiguousName = benchmarkName( + "AMS", "clone", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(amsContiguousName)) + { + size_t bytes = 0; + { + auto result = amsBase.clone(); + bytes = result.nbytes(); + } + return bytes; + }; + + const auto torchContiguousName = benchmarkName( + "Torch", "clone", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(torchContiguousName)) + { + int64_t elements = 0; + { + auto result = torchBase.clone(torch::MemoryFormat::Contiguous); + elements = result.numel(); + } + return elements; + }; + + const auto amsStridedName = benchmarkName( + "AMS", "clone", TypeInfo::name, "transposed", side, side); + CATCH_BENCHMARK(std::string(amsStridedName)) + { + size_t bytes = 0; + { + auto result = amsTransposed.clone(); + bytes = result.nbytes(); + } + return bytes; + }; + + const auto torchStridedName = benchmarkName( + "Torch", "clone", TypeInfo::name, "transposed", side, side); + CATCH_BENCHMARK(std::string(torchStridedName)) + { + int64_t elements = 0; + { + auto result = torchTransposed.clone(torch::MemoryFormat::Contiguous); + elements = result.numel(); + } + return elements; + }; +} + +template +void benchmarkConcat(int64_t side, int inputCount) +{ + const int64_t inputColumns = side / inputCount; + ams::SmallVector amsInputs; + std::vector torchInputs; + amsInputs.reserve(static_cast(inputCount)); + torchInputs.reserve(static_cast(inputCount)); + for (int input = 0; input < inputCount; ++input) { + amsInputs.push_back(makeAmsTensor(side, inputColumns)); + torchInputs.push_back(makeTorchTensor(side, inputColumns)); + } + + const std::string operation = "concat" + std::to_string(inputCount); + const auto amsName = benchmarkName( + "AMS", operation.c_str(), TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(amsName)) + { + size_t bytes = 0; + { + auto result = AMSTensor::concat(amsInputs, TypeInfo::amsDtype); + bytes = result.nbytes(); + } + return bytes; + }; + + const auto torchName = benchmarkName( + "Torch", operation.c_str(), TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(torchName)) + { + int64_t elements = 0; + { + auto result = torch::cat(torchInputs, 1); + elements = result.numel(); + } + return elements; + }; +} + +template +void benchmarkInterop(int64_t side) +{ + auto amsSource = makeAmsTensor(side, side); + auto torchSource = makeTorchTensor(side, side); + + const auto fromViewName = benchmarkName( + "Interop", "fromTorchView", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(fromViewName)) + { + const void* pointer = nullptr; + { + auto result = ams::fromTorchView(torchSource); + pointer = result.data_ptr(); + } + return pointer; + }; + + const auto toViewName = benchmarkName( + "Interop", "toTorchView", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(toViewName)) + { + const void* pointer = nullptr; + { + auto result = ams::toTorchView(amsSource); + pointer = result.data_ptr(); + } + return pointer; + }; + + const auto fromCopyName = benchmarkName( + "Interop", "fromTorchCopy", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(fromCopyName)) + { + size_t bytes = 0; + { + auto result = ams::fromTorchCopy(torchSource); + bytes = result.nbytes(); + } + return bytes; + }; + + const auto toCopyName = benchmarkName( + "Interop", "toTorchCopy", TypeInfo::name, "contiguous", side, side); + CATCH_BENCHMARK(std::string(toCopyName)) + { + int64_t elements = 0; + { + auto result = ams::toTorchCopy(amsSource); + elements = result.numel(); + } + return elements; + }; +} + +template +void verifyOperations() +{ + constexpr int64_t side = 4; + auto amsBase = makeAmsTensor(side, side); + auto torchBase = makeTorchTensor(side, side); + + auto amsAlias = AMSTensor::view(amsBase); + auto torchAlias = torchBase.alias(); + CATCH_REQUIRE(amsAlias.data_ptr() == amsBase.data_ptr()); + CATCH_REQUIRE(torchAlias.data_ptr() == torchBase.data_ptr()); + + auto amsTransposed = amsBase.transpose(0, 1); + auto torchTransposed = torchBase.transpose(0, 1); + auto amsClone = amsTransposed.clone(); + auto torchClone = torchTransposed.clone(torch::MemoryFormat::Contiguous); + CATCH_REQUIRE(amsClone.contiguous()); + CATCH_REQUIRE(torchClone.is_contiguous()); + CATCH_REQUIRE(amsClone.template data()[0] == T{1}); + CATCH_REQUIRE(torchClone.template data_ptr()[0] == T{1}); + + ams::SmallVector amsInputs; + std::vector torchInputs; + for (int input = 0; input < 2; ++input) { + amsInputs.push_back(makeAmsTensor(side, side / 2)); + torchInputs.push_back(makeTorchTensor(side, side / 2)); + } + auto amsConcat = AMSTensor::concat(amsInputs, TypeInfo::amsDtype); + auto torchConcat = torch::cat(torchInputs, 1); + CATCH_REQUIRE(amsConcat.shape()[0] == side); + CATCH_REQUIRE(amsConcat.shape()[1] == side); + CATCH_REQUIRE(torchConcat.size(0) == side); + CATCH_REQUIRE(torchConcat.size(1) == side); + + auto fromView = ams::fromTorchView(torchBase); + auto fromCopy = ams::fromTorchCopy(torchBase); + auto toView = ams::toTorchView(amsBase); + auto toCopy = ams::toTorchCopy(amsBase); + CATCH_REQUIRE(fromView.data_ptr() == torchBase.data_ptr()); + CATCH_REQUIRE(fromCopy.data_ptr() != torchBase.data_ptr()); + CATCH_REQUIRE(toView.data_ptr() == amsBase.data_ptr()); + CATCH_REQUIRE(toCopy.data_ptr() != amsBase.data_ptr()); +} + +template +void benchmarkType() +{ + verifyOperations(); + for (const int64_t side : benchmarkSizes) + benchmarkAllocation(side); + benchmarkViews(128); + benchmarkMetadataAndTranspose(128); + for (const int64_t side : benchmarkSizes) + benchmarkClone(side); + for (const int64_t side : benchmarkSizes) { + benchmarkConcat(side, 2); + benchmarkConcat(side, 4); + } + benchmarkInterop(128); +} +} // namespace + +CATCH_TEST_CASE("AMSTensor and Torch CPU tensor-container primitives", + "[benchmark][performance][amstensor][torch]") +{ + CATCH_REQUIRE(torch::get_num_threads() == 1); + CATCH_REQUIRE(torch::get_num_interop_threads() == 1); + ams::AMSInit(); + + benchmarkType(); + benchmarkType(); + benchmarkType(); + benchmarkType(); +} + +int main(int argc, char** argv) +{ + ::setenv("OMP_NUM_THREADS", "1", 1); + ::setenv("MKL_NUM_THREADS", "1", 1); + ::setenv("OPENBLAS_NUM_THREADS", "1", 1); + torch::set_num_threads(1); + torch::set_num_interop_threads(1); + return ams::test::runCatchSession(argc, argv); +} diff --git a/tests/AMSlib/core/amstorch_interop.cpp b/tests/AMSlib/core/amstorch_interop.cpp new file mode 100644 index 00000000..17c42ccf --- /dev/null +++ b/tests/AMSlib/core/amstorch_interop.cpp @@ -0,0 +1,53 @@ +#include + +#include + +#include "AMSTorchInterop.hpp" + +using namespace ams; + +CATCH_TEST_CASE("Torch interop views are zero-copy and retain storage", + "[ams][torch][interop]") +{ + auto source = torch::arange(6, torch::TensorOptions().dtype(torch::kFloat32)) + .reshape({2, 3}); + void* pointer = source.data_ptr(); + auto view = fromTorchView(source); + CATCH_REQUIRE(view.data_ptr() == pointer); + source = torch::Tensor(); + view.data()[4] = 99.0f; + CATCH_REQUIRE(view.data()[4] == 99.0f); + + auto torchView = toTorchView(view); + CATCH_REQUIRE(torchView.data_ptr() == view.data_ptr()); + torchView[0][0] = 17.0f; + CATCH_REQUIRE(view.data()[0] == 17.0f); +} + +CATCH_TEST_CASE("Torch interop copies own independent contiguous storage", + "[ams][torch][interop]") +{ + auto source = torch::arange(12, torch::TensorOptions().dtype(torch::kInt64)) + .reshape({3, 4}) + .transpose(0, 1); + auto copy = fromTorchCopy(source); + CATCH_REQUIRE(copy.contiguous()); + CATCH_REQUIRE(copy.data_ptr() != source.data_ptr()); + source[0][0] = 999; + CATCH_REQUIRE(copy.data()[0] == 0); + + auto torchCopy = toTorchCopy(copy); + CATCH_REQUIRE(torchCopy.is_contiguous()); + CATCH_REQUIRE(torchCopy.data_ptr() != copy.data_ptr()); + copy.data()[0] = 123; + CATCH_REQUIRE(torchCopy[0][0].item() == 0); +} + +CATCH_TEST_CASE("Torch interop validates unsupported dtype and layout", + "[ams][torch][interop]") +{ + CATCH_REQUIRE_THROWS_AS(fromTorchView(torch::ones({2}, torch::kBool)), + std::invalid_argument); + auto expanded = torch::ones({1, 3}).expand({4, 3}); + CATCH_REQUIRE_THROWS_AS(fromTorchView(expanded), std::invalid_argument); +} diff --git a/tests/AMSlib/wf/tensor_bundle.cpp b/tests/AMSlib/core/tensor_bundle.cpp similarity index 99% rename from tests/AMSlib/wf/tensor_bundle.cpp rename to tests/AMSlib/core/tensor_bundle.cpp index d8338608..f2d0377b 100644 --- a/tests/AMSlib/wf/tensor_bundle.cpp +++ b/tests/AMSlib/core/tensor_bundle.cpp @@ -175,4 +175,4 @@ CATCH_TEST_CASE("TensorBundle at() bounds checking", "[tensorbundle]") // Out of bounds access should throw CATCH_REQUIRE_THROWS_AS(tb.at(2), std::out_of_range); CATCH_REQUIRE_THROWS_AS(tb.at(100), std::out_of_range); -} +} \ No newline at end of file diff --git a/tests/AMSlib/db/CMakeLists.txt b/tests/AMSlib/db/CMakeLists.txt index f858bdbb..5bda6f4f 100644 --- a/tests/AMSlib/db/CMakeLists.txt +++ b/tests/AMSlib/db/CMakeLists.txt @@ -7,13 +7,24 @@ function(BUILD_UNIT_TEST exe source) target_include_directories(${exe} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) # DB tests use the AMS-owned main to avoid HIP shutdown finalizer crashes. target_sources(${exe} PRIVATE ../ams_catch_main.cpp) - target_link_libraries(${exe} PRIVATE stdc++fs AMS torch Catch2::Catch2) + target_link_libraries(${exe} PRIVATE stdc++fs AMS Catch2::Catch2) + if (ENABLE_TORCH) + target_link_libraries(${exe} PRIVATE torch) + endif() + target_include_directories(${exe} PRIVATE ${AMS_TEST_ROOT}) target_link_libraries(${exe} PRIVATE Threads::Threads) + target_link_libraries(${exe} PRIVATE fmt::fmt) target_compile_features(${exe} PRIVATE cxx_std_17) target_link_libraries(${exe} PRIVATE ${AMS_HDF5_LINK_TARGETS}) + if (ENABLE_CUDA) + target_link_libraries(${exe} PRIVATE CUDA::cudart) + elseif (ENABLE_HIP) + target_link_libraries(${exe} PRIVATE hip::host) + endif() + if (ENABLE_CALIPER) message(STATUS "Building with caliper ${exe}") target_link_libraries(${exe} PRIVATE caliper) @@ -41,6 +52,13 @@ function(ADD_DB_UNIT_TEST name exec) set_tests_properties(${name} PROPERTIES LABELS HDF5_UNIT_TEST) endfunction() -BUILD_UNIT_TEST(db_hdf5 db_hdf5.cpp) -target_link_libraries(db_hdf5 PRIVATE fmt::fmt) -ADD_DB_UNIT_TEST(DB::HDF5 db_hdf5) +# AMSTensor-only tests (always built) +BUILD_UNIT_TEST(db_hdf5_ams db_hdf5_ams.cpp Catch2::Catch2 ../ams_catch_main.cpp) +ADD_DB_UNIT_TEST(DB::HDF5_AMSTENSOR db_hdf5_ams) + +# db_hdf5 test currently uses torch::Tensor for test data generation/validation. + +if (ENABLE_TORCH) + BUILD_UNIT_TEST(db_hdf5_torch db_hdf5_torch.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_DB_UNIT_TEST(DB::HDF5_TORCH db_hdf5_torch) +endif() diff --git a/tests/AMSlib/db/db_hdf5.cpp b/tests/AMSlib/db/db_hdf5.cpp deleted file mode 100644 index 15c2b45f..00000000 --- a/tests/AMSlib/db/db_hdf5.cpp +++ /dev/null @@ -1,375 +0,0 @@ -#define CATCH_CONFIG_PREFIX_ALL -#include -#include -#include -#include -#include -#include - -#include "AMSTypes.hpp" -#include "wf/basedb.hpp" - -CATCH_TEST_CASE("DBManager tracks instances and materializes files", - "[ams][db][instances]") -{ - namespace fs = std::filesystem; - - auto db_dir = (std::filesystem::temp_directory_path() / "ams_workflow_tests"); - std::filesystem::create_directories(db_dir); - - std::string tmp_dir = db_dir / "ams-test-XXXXXX"; - std::vector tmp(tmp_dir.begin(), tmp_dir.end()); - tmp.push_back('\0'); - char* dirname = mkdtemp(tmp.data()); - if (!dirname) { - perror("mkdtemp"); - } - - db_dir = std::filesystem::path(dirname); - std::filesystem::create_directories(db_dir); - - - auto& db = ams::db::DBManager::getInstance(); - db.instantiate_fs_db(ams::AMSDBType::AMS_HDF5, db_dir.string() + "/"); - - // Touch two domains twice each - std::vector domains = {"domain_1", - "domain_2", - "domain_1", - "domain_2"}; - for (auto& dn : domains) { - auto file_db = db.getDB(dn); - (void)file_db; - } - - // Only two unique instances should exist - CATCH_REQUIRE(db.getNumInstances() == 2); - - // Clean triggers destructors & resets instance tracking - db.clean(); - CATCH_REQUIRE(db.getNumInstances() == 0); - - // Files must exist on disk even after clean() - for (auto const& dn : {"domain_1", "domain_2"}) { - const fs::path fn = db_dir / (std::string(dn) + "_0.h5"); - CATCH_INFO("Checking file exists: " << fn.string()); - CATCH_REQUIRE(fs::exists(fn)); - } - - // Best-effort cleanup of temp artifacts - std::error_code ec; - db.clean(); - fs::remove_all(db_dir, ec); -} - - -// --- Helper to read an HDF5 dataset into a std::vector --- -template -static std::vector readHDF5Dataset(const std::string& filePath, - const std::string& datasetName, - hid_t expectedNativeType) -{ - hid_t file_id = H5Fopen(filePath.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - CATCH_REQUIRE(file_id >= 0); - - hid_t dset_id = H5Dopen2(file_id, datasetName.c_str(), H5P_DEFAULT); - if (dset_id < 0) { - H5Fclose(file_id); - CATCH_FAIL("Failed to open dataset: " << datasetName); - } - - hid_t space_id = H5Dget_space(dset_id); - CATCH_REQUIRE(space_id >= 0); - - int ndims = H5Sget_simple_extent_ndims(space_id); - CATCH_REQUIRE(ndims >= 0); - - std::vector dims(ndims, 0); - CATCH_REQUIRE(H5Sget_simple_extent_dims(space_id, dims.data(), nullptr) >= 0); - - size_t total_elems = 1; - for (auto d : dims) - total_elems *= static_cast(d); - - // (Optional) sanity check the dataset type size vs T - hid_t dtype_id = H5Dget_type(dset_id); - CATCH_REQUIRE(dtype_id >= 0); - const size_t dtype_size = H5Tget_size(dtype_id); - CATCH_REQUIRE(dtype_size == sizeof(T)); - - std::vector out(total_elems); - CATCH_REQUIRE(H5Dread(dset_id, - expectedNativeType, - H5S_ALL, - H5S_ALL, - H5P_DEFAULT, - out.data()) >= 0); - - H5Tclose(dtype_id); - H5Sclose(space_id); - H5Dclose(dset_id); - H5Fclose(file_id); - return out; -} - -CATCH_TEST_CASE("hdf5DB creates file and stores domain_name dataset", - "[ams][db][hdf5]") -{ - // Choose test inputs (you can parameterize if you like) - const std::string domain_name = "domain_1"; - - auto db_dir = (std::filesystem::temp_directory_path() / "ams_workflow_tests"); - std::filesystem::create_directories(db_dir); - - std::string tmp_dir = db_dir / "ams-test-XXXXXX"; - std::vector tmp(tmp_dir.begin(), tmp_dir.end()); - tmp.push_back('\0'); - char* dirname = mkdtemp(tmp.data()); - if (!dirname) { - perror("mkdtemp"); - } - - db_dir = std::filesystem::path(dirname); - std::filesystem::create_directories(db_dir); - // Create DB (rid=0), like your original code - std::string filename; - { - ams::db::hdf5DB db(db_dir.string() + "/", domain_name, /*rid*/ 0); - filename = db.getFilename(); - } - - CATCH_REQUIRE(std::filesystem::exists(filename)); - - { - ams::db::hdf5DB db(db_dir.string() + "/", domain_name, /*rid*/ 0); - CATCH_REQUIRE(std::filesystem::exists(db.getFilename())); - } - - // Read dataset "domain_name" as chars and compare to domain_name - const std::string dataset = "domain_name"; - auto data = readHDF5Dataset(filename, dataset, H5T_NATIVE_CHAR); - - // Some files might store a trailing '\0'; accept either exact or '\0'-terminated. - std::string read_str(data.begin(), data.end()); - // Trim a single trailing NUL if present - if (!read_str.empty() && read_str.back() == '\0') read_str.pop_back(); - - CATCH_INFO("HDF5 file: " << filename); - CATCH_INFO("Read domain_name dataset: '" << read_str << "'"); - CATCH_REQUIRE(read_str == domain_name); - - std::filesystem::remove_all(db_dir); -} - -static bool verifyDatasetContents_f32_flat( - const std::string& fileName, - const std::string& datasetName, - const std::vector& expectedTensors) -{ - hid_t file_id = H5Fopen(fileName.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - if (file_id < 0) - throw std::runtime_error("Failed to open HDF5 file: " + fileName); - - hid_t dset_id = H5Dopen2(file_id, datasetName.c_str(), H5P_DEFAULT); - if (dset_id < 0) { - H5Fclose(file_id); - throw std::runtime_error("Failed to open dataset: " + datasetName); - } - - hid_t space_id = H5Dget_space(dset_id); - if (space_id < 0) { - H5Dclose(dset_id); - H5Fclose(file_id); - throw std::runtime_error("Failed to get dataspace."); - } - - int ndims = H5Sget_simple_extent_ndims(space_id); - if (ndims < 0) { - H5Sclose(space_id); - H5Dclose(dset_id); - H5Fclose(file_id); - throw std::runtime_error("Bad ndims"); - } - - std::vector dims(ndims); - if (H5Sget_simple_extent_dims(space_id, dims.data(), nullptr) < 0) { - H5Sclose(space_id); - H5Dclose(dset_id); - H5Fclose(file_id); - throw std::runtime_error("Bad dims"); - } - H5Sclose(space_id); - - size_t total = 1; - for (auto d : dims) - total *= d; - - torch::Tensor readTensor = - torch::empty({static_cast(total)}, torch::kFloat32); - herr_t st = H5Dread(dset_id, - H5T_NATIVE_FLOAT, - H5S_ALL, - H5S_ALL, - H5P_DEFAULT, - readTensor.data_ptr()); - H5Dclose(dset_id); - H5Fclose(file_id); - if (st < 0) throw std::runtime_error("H5Dread failed"); - - auto expectedTensor = - torch::cat(expectedTensors).flatten().to(torch::kFloat32).cpu(); - return torch::allclose(readTensor, - expectedTensor, - /*rtol=*/1e-5, - /*atol=*/1e-8); -} - -template -static std::vector readVectorDataset(const std::string& filePath, - const std::string& datasetName, - hid_t DataType) -{ - hid_t file_id = H5Fopen(filePath.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - if (file_id < 0) - throw std::runtime_error("Failed to open HDF5 file: " + filePath); - - hid_t dataset_id = H5Dopen(file_id, datasetName.c_str(), H5P_DEFAULT); - if (dataset_id < 0) { - H5Fclose(file_id); - throw std::runtime_error("Failed to open dataset: " + datasetName); - } - - hid_t dataspace_id = H5Dget_space(dataset_id); - if (dataspace_id < 0) { - H5Dclose(dataset_id); - H5Fclose(file_id); - throw std::runtime_error("Failed to get dataspace for dataset: " + - datasetName); - } - - int ndims = H5Sget_simple_extent_ndims(dataspace_id); - std::vector dims(ndims); - H5Sget_simple_extent_dims(dataspace_id, dims.data(), nullptr); - - hid_t datatype_id = H5Dget_type(dataset_id); - size_t datatype_size = H5Tget_size(datatype_id); - - std::vector data; - if (datatype_size == sizeof(T)) { - size_t total = 1; - for (auto d : dims) - total *= d; - data.resize(total); - if (H5Dread( - dataset_id, DataType, H5S_ALL, H5S_ALL, H5P_DEFAULT, data.data()) < - 0) { - H5Tclose(datatype_id); - H5Sclose(dataspace_id); - H5Dclose(dataset_id); - H5Fclose(file_id); - throw std::runtime_error("Failed to read dataset: " + datasetName); - } - } else { - H5Tclose(datatype_id); - H5Sclose(dataspace_id); - H5Dclose(dataset_id); - H5Fclose(file_id); - throw std::runtime_error("Unsupported data type for dataset: " + - datasetName); - } - - H5Tclose(datatype_id); - H5Sclose(dataspace_id); - H5Dclose(dataset_id); - H5Fclose(file_id); - return data; -} - -// ---------- Tests ---------- - -CATCH_TEST_CASE("HDF5 DB: append and verify input/output datasets", - "[ams][db][hdf5]") -{ - auto db_dir = (std::filesystem::temp_directory_path() / "ams_workflow_tests"); - std::filesystem::create_directories(db_dir); - - std::string tmp_dir = db_dir / "ams-test-XXXXXX"; - std::vector tmp(tmp_dir.begin(), tmp_dir.end()); - tmp.push_back('\0'); - char* dirname = mkdtemp(tmp.data()); - if (!dirname) { - perror("mkdtemp"); - } - - db_dir = std::filesystem::path(dirname); - std::filesystem::create_directories(db_dir); - - const std::string directory = db_dir.string() + "/"; - const std::string domain_name = "domain_foo"; - std::string filename; - - std::vector inputTensors, outputTensors; - - // Two iterations: create then reopen+append; verify after each - for (int iter = 0; iter < 2; ++iter) { - { - ams::db::hdf5DB db(directory, domain_name, /*rid=*/0); - filename = db.getFilename(); - - torch::Tensor IData = - torch::rand({21, 4}, torch::TensorOptions().dtype(torch::kFloat32)); - torch::Tensor OData = - torch::rand({21, 4}, torch::TensorOptions().dtype(torch::kFloat32)); - - db.store(IData, OData); - - inputTensors.emplace_back(std::move(IData)); - outputTensors.emplace_back(std::move(OData)); - } - - CATCH_CAPTURE(filename); - CATCH_REQUIRE(std::filesystem::exists(filename)); - CATCH_REQUIRE( - verifyDatasetContents_f32_flat(filename, "input_data", inputTensors)); - CATCH_REQUIRE( - verifyDatasetContents_f32_flat(filename, "output_data", outputTensors)); - } - std::filesystem::remove_all(db_dir); -} - -CATCH_TEST_CASE("HDF5 DB: 'domain_name' dataset matches provided name", - "[ams][db][hdf5][metadata]") -{ - auto db_dir = (std::filesystem::temp_directory_path() / "ams_workflow_tests"); - std::filesystem::create_directories(db_dir); - - std::string tmp_dir = db_dir / "ams-test-XXXXXX"; - std::vector tmp(tmp_dir.begin(), tmp_dir.end()); - tmp.push_back('\0'); - char* dirname = mkdtemp(tmp.data()); - if (!dirname) { - perror("mkdtemp"); - } - - db_dir = std::filesystem::path(dirname); - std::filesystem::create_directories(db_dir); - - const std::string directory = db_dir.string() + "/"; - const std::string domain_name = "domain_bar"; - std::string filename; - - { - ams::db::hdf5DB db(directory, domain_name, /*rid=*/0); - filename = db.getFilename(); - } - CATCH_REQUIRE(std::filesystem::exists(filename)); - - // Expect dataset named "domain_name" with the char contents of domain_name - const std::string dataset = "domain_name"; - std::vector expected(domain_name.begin(), domain_name.end()); - - auto vec = readVectorDataset(filename, dataset, H5T_NATIVE_CHAR); - // Helpful diagnostic if it ever differs - CATCH_REQUIRE(vec == expected); - std::filesystem::remove_all(db_dir); -} diff --git a/tests/AMSlib/db/db_hdf5_ams.cpp b/tests/AMSlib/db/db_hdf5_ams.cpp new file mode 100644 index 00000000..efede526 --- /dev/null +++ b/tests/AMSlib/db/db_hdf5_ams.cpp @@ -0,0 +1,205 @@ +/* + * Copyright 2021-2026 Lawrence Livermore National Security, LLC and other + * AMSLib Project Developers + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include "AMS.h" +#include "db_hdf5_helpers.hpp" +#include "wf/basedb.hpp" + +using IDT = ams::AMSTensor::IntDimType; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +CATCH_TEST_CASE("DBManager tracks instances and materializes files", + "[ams][db][instances]") +{ + auto db_dir = makeTempDir(); + auto& db = ams::db::DBManager::getInstance(); + db.instantiate_fs_db(ams::AMSDBType::AMS_HDF5, db_dir.string() + "/"); + + std::vector domains = {"domain_1", + "domain_2", + "domain_1", + "domain_2"}; + for (auto& dn : domains) { + auto file_db = db.getDB(dn); + (void)file_db; + } + + CATCH_REQUIRE(db.getNumInstances() == 2); + + db.clean(); + CATCH_REQUIRE(db.getNumInstances() == 0); + + for (auto const& dn : {"domain_1", "domain_2"}) { + const std::filesystem::path fn = db_dir / (std::string(dn) + "_0.h5"); + CATCH_INFO("Checking file exists: " << fn.string()); + CATCH_REQUIRE(std::filesystem::exists(fn)); + } + + std::error_code ec; + db.clean(); + std::filesystem::remove_all(db_dir, ec); +} + + +CATCH_TEST_CASE("hdf5DB creates file and stores domain_name dataset", + "[ams][db][hdf5]") +{ + auto db_dir = makeTempDir(); + const std::string domain_name = "domain_1"; + std::string filename; + { + ams::db::hdf5DB db(db_dir.string() + "/", domain_name, 0); + filename = db.getFilename(); + } + + CATCH_REQUIRE(std::filesystem::exists(filename)); + + { + ams::db::hdf5DB db(db_dir.string() + "/", domain_name, 0); + CATCH_REQUIRE(std::filesystem::exists(db.getFilename())); + } + + auto data = readHDF5Dataset(filename, "domain_name", H5T_NATIVE_CHAR); + std::string read_str(data.begin(), data.end()); + if (!read_str.empty() && read_str.back() == '\0') read_str.pop_back(); + + CATCH_INFO("HDF5 file: " << filename); + CATCH_INFO("Read domain_name dataset: '" << read_str << "'"); + CATCH_REQUIRE(read_str == domain_name); + + std::filesystem::remove_all(db_dir); +} + + +CATCH_TEST_CASE("HDF5 DB: 'domain_name' dataset matches provided name", + "[ams][db][hdf5][metadata]") +{ + auto db_dir = makeTempDir(); + const std::string directory = db_dir.string() + "/"; + const std::string domain_name = "domain_bar"; + std::string filename; + + { + ams::db::hdf5DB db(directory, domain_name, 0); + filename = db.getFilename(); + } + CATCH_REQUIRE(std::filesystem::exists(filename)); + + std::vector expected(domain_name.begin(), domain_name.end()); + auto vec = readVectorDataset(filename, "domain_name", H5T_NATIVE_CHAR); + CATCH_REQUIRE(vec == expected); + std::filesystem::remove_all(db_dir); +} + + +CATCH_TEST_CASE("HDF5 DB: append and verify input/output datasets", + "[ams][db][hdf5]") +{ + auto db_dir = makeTempDir(); + const std::string directory = db_dir.string() + "/"; + const std::string domain_name = "domain_foo"; + std::string filename; + + const IDT nRows = 21; + const IDT nCols = 4; + const size_t nElems = static_cast(nRows * nCols); + std::vector shape = {nRows, nCols}; + std::vector strides = {nCols, 1}; + + std::vector> expectedInputs; + std::vector> expectedOutputs; + unsigned seed = 42; + + // Two iterations: create then reopen+append; verify after each + for (int iter = 0; iter < 2; ++iter) { + std::vector inputBuf(nElems); + std::vector outputBuf(nElems); + fillRandom(inputBuf.data(), nElems, seed++); + fillRandom(outputBuf.data(), nElems, seed++); + + { + ams::db::hdf5DB db(directory, domain_name, 0); + filename = db.getFilename(); + + auto IData = ams::AMSTensor::view(inputBuf.data(), + shape, + strides, + ams::AMSResourceType::AMS_HOST); + auto OData = ams::AMSTensor::view(outputBuf.data(), + shape, + strides, + ams::AMSResourceType::AMS_HOST); + + db.store(IData, OData); + } + + expectedInputs.push_back(inputBuf); + expectedOutputs.push_back(outputBuf); + + CATCH_CAPTURE(filename); + CATCH_REQUIRE(std::filesystem::exists(filename)); + CATCH_REQUIRE( + verifyDatasetContents_f32(filename, "input_data", expectedInputs)); + CATCH_REQUIRE( + verifyDatasetContents_f32(filename, "output_data", expectedOutputs)); + } + std::filesystem::remove_all(db_dir); +} + + +CATCH_TEST_CASE("HDF5 DB: collects multiple AMSTensors as flat rows", + "[ams][db][hdf5][collection]") +{ + ams::AMSInit(); + auto db_dir = makeTempDir(); + const std::string domain_name = "multi_tensor_collection"; + std::string filename; + + std::vector input_a = {1.0f, 2.0f, 3.0f, 4.0f}; + std::vector input_b = {5.0f, 6.0f}; + std::vector output_a = {7.0f, 8.0f}; + std::vector output_b = {9.0f, 10.0f, 11.0f, 12.0f}; + std::vector two_columns = {2, 2}; + std::vector one_column = {2, 1}; + std::vector strides_two = {2, 1}; + std::vector strides_one = {1, 1}; + + { + ams::db::hdf5DB db(db_dir.string() + "/", domain_name, 0); + filename = db.getFilename(); + + ams::SmallVector inputs; + inputs.push_back(ams::AMSTensor::view( + input_a.data(), two_columns, strides_two, ams::AMS_HOST)); + inputs.push_back(ams::AMSTensor::view( + input_b.data(), one_column, strides_one, ams::AMS_HOST)); + + ams::SmallVector outputs; + outputs.push_back(ams::AMSTensor::view( + output_a.data(), one_column, strides_one, ams::AMS_HOST)); + outputs.push_back(ams::AMSTensor::view( + output_b.data(), two_columns, strides_two, ams::AMS_HOST)); + + db.store(inputs, outputs); + } + + const std::vector expected_inputs = { + 1.0f, 2.0f, 5.0f, 3.0f, 4.0f, 6.0f}; + const std::vector expected_outputs = { + 7.0f, 9.0f, 10.0f, 8.0f, 11.0f, 12.0f}; + CATCH_REQUIRE(readVectorDataset(filename, + "input_data", + H5T_NATIVE_FLOAT) == expected_inputs); + CATCH_REQUIRE(readVectorDataset(filename, + "output_data", + H5T_NATIVE_FLOAT) == expected_outputs); + + std::filesystem::remove_all(db_dir); +} diff --git a/tests/AMSlib/db/db_hdf5_helpers.hpp b/tests/AMSlib/db/db_hdf5_helpers.hpp new file mode 100644 index 00000000..df53dc08 --- /dev/null +++ b/tests/AMSlib/db/db_hdf5_helpers.hpp @@ -0,0 +1,191 @@ +/* + * Copyright 2021-2026 Lawrence Livermore National Security, LLC and other + * AMSLib Project Developers + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#pragma once + +#define CATCH_CONFIG_PREFIX_ALL +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Temp directory +// --------------------------------------------------------------------------- + +static inline std::filesystem::path makeTempDir() +{ + auto db_dir = std::filesystem::temp_directory_path() / "ams_workflow_tests"; + std::filesystem::create_directories(db_dir); + + std::string tmp_dir = db_dir / "ams-test-XXXXXX"; + std::vector tmp(tmp_dir.begin(), tmp_dir.end()); + tmp.push_back('\0'); + char* dirname = mkdtemp(tmp.data()); + if (!dirname) perror("mkdtemp"); + db_dir = std::filesystem::path(dirname); + std::filesystem::create_directories(db_dir); + return db_dir; +} + +// --------------------------------------------------------------------------- +// HDF5 dataset readers +// --------------------------------------------------------------------------- + +template +static std::vector readHDF5Dataset(const std::string& filePath, + const std::string& datasetName, + hid_t expectedNativeType) +{ + hid_t file_id = H5Fopen(filePath.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); + CATCH_REQUIRE(file_id >= 0); + + hid_t dset_id = H5Dopen2(file_id, datasetName.c_str(), H5P_DEFAULT); + if (dset_id < 0) { + H5Fclose(file_id); + CATCH_FAIL("Failed to open dataset: " << datasetName); + } + + hid_t space_id = H5Dget_space(dset_id); + CATCH_REQUIRE(space_id >= 0); + + int ndims = H5Sget_simple_extent_ndims(space_id); + CATCH_REQUIRE(ndims >= 0); + + std::vector dims(ndims, 0); + CATCH_REQUIRE(H5Sget_simple_extent_dims(space_id, dims.data(), nullptr) >= 0); + + size_t total_elems = 1; + for (auto d : dims) + total_elems *= static_cast(d); + + hid_t dtype_id = H5Dget_type(dset_id); + CATCH_REQUIRE(dtype_id >= 0); + CATCH_REQUIRE(H5Tget_size(dtype_id) == sizeof(T)); + + std::vector out(total_elems); + CATCH_REQUIRE(H5Dread(dset_id, + expectedNativeType, + H5S_ALL, + H5S_ALL, + H5P_DEFAULT, + out.data()) >= 0); + + H5Tclose(dtype_id); + H5Sclose(space_id); + H5Dclose(dset_id); + H5Fclose(file_id); + return out; +} + +template +static std::vector readVectorDataset(const std::string& filePath, + const std::string& datasetName, + hid_t DataType) +{ + hid_t file_id = H5Fopen(filePath.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); + if (file_id < 0) + throw std::runtime_error("Failed to open HDF5 file: " + filePath); + + hid_t dataset_id = H5Dopen(file_id, datasetName.c_str(), H5P_DEFAULT); + if (dataset_id < 0) { + H5Fclose(file_id); + throw std::runtime_error("Failed to open dataset: " + datasetName); + } + + hid_t dataspace_id = H5Dget_space(dataset_id); + if (dataspace_id < 0) { + H5Dclose(dataset_id); + H5Fclose(file_id); + throw std::runtime_error("Failed to get dataspace for dataset: " + + datasetName); + } + + int ndims = H5Sget_simple_extent_ndims(dataspace_id); + std::vector dims(ndims); + H5Sget_simple_extent_dims(dataspace_id, dims.data(), nullptr); + + hid_t datatype_id = H5Dget_type(dataset_id); + size_t datatype_size = H5Tget_size(datatype_id); + + std::vector data; + if (datatype_size == sizeof(T)) { + size_t total = 1; + for (auto d : dims) + total *= d; + data.resize(total); + if (H5Dread( + dataset_id, DataType, H5S_ALL, H5S_ALL, H5P_DEFAULT, data.data()) < + 0) { + H5Tclose(datatype_id); + H5Sclose(dataspace_id); + H5Dclose(dataset_id); + H5Fclose(file_id); + throw std::runtime_error("Failed to read dataset: " + datasetName); + } + } else { + H5Tclose(datatype_id); + H5Sclose(dataspace_id); + H5Dclose(dataset_id); + H5Fclose(file_id); + throw std::runtime_error("Unsupported data type for dataset: " + + datasetName); + } + + H5Tclose(datatype_id); + H5Sclose(dataspace_id); + H5Dclose(dataset_id); + H5Fclose(file_id); + return data; +} + +// --------------------------------------------------------------------------- +// Random data generation and comparison +// --------------------------------------------------------------------------- + +static inline void fillRandom(float* data, size_t count, unsigned seed) +{ + std::mt19937 gen(seed); + std::uniform_real_distribution dist(0.0f, 1.0f); + for (size_t i = 0; i < count; ++i) + data[i] = dist(gen); +} + +static inline bool allClose(const std::vector& a, + const std::vector& b, + float rtol = 1e-5f, + float atol = 1e-8f) +{ + if (a.size() != b.size()) return false; + for (size_t i = 0; i < a.size(); ++i) { + if (std::fabs(a[i] - b[i]) > atol + rtol * std::fabs(b[i])) return false; + } + return true; +} + +/// Verify that an HDF5 dataset contains the expected float data. +/// Each buffer in expectedBuffers is one store() call's flat float data, +/// concatenated along the first dimension to form the expected HDF5 content. +static inline bool verifyDatasetContents_f32( + const std::string& fileName, + const std::string& datasetName, + const std::vector>& expectedBuffers) +{ + std::vector expected; + for (auto& buf : expectedBuffers) + expected.insert(expected.end(), buf.begin(), buf.end()); + + auto actual = readHDF5Dataset(fileName, datasetName, H5T_NATIVE_FLOAT); + return allClose(actual, expected); +} \ No newline at end of file diff --git a/tests/AMSlib/db/db_hdf5_torch.cpp b/tests/AMSlib/db/db_hdf5_torch.cpp new file mode 100644 index 00000000..a2cb6414 --- /dev/null +++ b/tests/AMSlib/db/db_hdf5_torch.cpp @@ -0,0 +1,155 @@ +/* + * Copyright 2021-2026 Lawrence Livermore National Security, LLC and other + * AMSLib Project Developers + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include + +#include "AMS.h" +#include "db_hdf5_helpers.hpp" +#include "wf/basedb.hpp" + +using IDT = ams::AMSTensor::IntDimType; + + +/// Create an AMSTensor view over a CPU-contiguous float32 torch::Tensor. +static ams::AMSTensor torchToAMSView(torch::Tensor& t) +{ + std::vector shape(t.sizes().begin(), t.sizes().end()); + std::vector strides(t.strides().begin(), t.strides().end()); + return ams::AMSTensor::view(t.data_ptr(), + shape, + strides, + ams::AMSResourceType::AMS_HOST); +} + + +CATCH_TEST_CASE("DBManager tracks instances and materializes files (torch)", + "[ams][db][instances][torch]") +{ + auto db_dir = makeTempDir(); + auto& db = ams::db::DBManager::getInstance(); + db.instantiate_fs_db(ams::AMSDBType::AMS_HDF5, db_dir.string() + "/"); + + std::vector domains = {"domain_1", + "domain_2", + "domain_1", + "domain_2"}; + for (auto& dn : domains) { + auto file_db = db.getDB(dn); + (void)file_db; + } + + CATCH_REQUIRE(db.getNumInstances() == 2); + + db.clean(); + CATCH_REQUIRE(db.getNumInstances() == 0); + + for (auto const& dn : {"domain_1", "domain_2"}) { + const std::filesystem::path fn = db_dir / (std::string(dn) + "_0.h5"); + CATCH_INFO("Checking file exists: " << fn.string()); + CATCH_REQUIRE(std::filesystem::exists(fn)); + } + + std::error_code ec; + db.clean(); + std::filesystem::remove_all(db_dir, ec); +} + + +CATCH_TEST_CASE("hdf5DB creates file and stores domain_name dataset (torch)", + "[ams][db][hdf5][torch]") +{ + auto db_dir = makeTempDir(); + const std::string domain_name = "domain_1"; + std::string filename; + { + ams::db::hdf5DB db(db_dir.string() + "/", domain_name, 0); + filename = db.getFilename(); + } + + CATCH_REQUIRE(std::filesystem::exists(filename)); + + { + ams::db::hdf5DB db(db_dir.string() + "/", domain_name, 0); + CATCH_REQUIRE(std::filesystem::exists(db.getFilename())); + } + + auto data = readHDF5Dataset(filename, "domain_name", H5T_NATIVE_CHAR); + std::string read_str(data.begin(), data.end()); + if (!read_str.empty() && read_str.back() == '\0') read_str.pop_back(); + + CATCH_INFO("HDF5 file: " << filename); + CATCH_INFO("Read domain_name dataset: '" << read_str << "'"); + CATCH_REQUIRE(read_str == domain_name); + + std::filesystem::remove_all(db_dir); +} + + +CATCH_TEST_CASE("HDF5 DB: 'domain_name' dataset matches provided name (torch)", + "[ams][db][hdf5][metadata][torch]") +{ + auto db_dir = makeTempDir(); + const std::string directory = db_dir.string() + "/"; + const std::string domain_name = "domain_bar"; + std::string filename; + + { + ams::db::hdf5DB db(directory, domain_name, 0); + filename = db.getFilename(); + } + CATCH_REQUIRE(std::filesystem::exists(filename)); + + std::vector expected(domain_name.begin(), domain_name.end()); + auto vec = readVectorDataset(filename, "domain_name", H5T_NATIVE_CHAR); + CATCH_REQUIRE(vec == expected); + std::filesystem::remove_all(db_dir); +} + + +CATCH_TEST_CASE("HDF5 DB (Torch): append and verify input/output datasets", + "[ams][db][hdf5][torch]") +{ + auto db_dir = makeTempDir(); + const std::string directory = db_dir.string() + "/"; + const std::string domain_name = "domain_torch"; + std::string filename; + + std::vector> expectedInputs; + std::vector> expectedOutputs; + + for (int iter = 0; iter < 2; ++iter) { + torch::Tensor IData = + torch::rand({21, 4}, torch::TensorOptions().dtype(torch::kFloat32)); + torch::Tensor OData = + torch::rand({21, 4}, torch::TensorOptions().dtype(torch::kFloat32)); + + { + ams::db::hdf5DB db(directory, domain_name, 0); + filename = db.getFilename(); + + auto amsI = torchToAMSView(IData); + auto amsO = torchToAMSView(OData); + db.store(amsI, amsO); + } + + // Capture expected data as flat float vectors + { + auto* iPtr = IData.data_ptr(); + expectedInputs.emplace_back(iPtr, iPtr + IData.numel()); + auto* oPtr = OData.data_ptr(); + expectedOutputs.emplace_back(oPtr, oPtr + OData.numel()); + } + + CATCH_CAPTURE(filename); + CATCH_REQUIRE(std::filesystem::exists(filename)); + CATCH_REQUIRE( + verifyDatasetContents_f32(filename, "input_data", expectedInputs)); + CATCH_REQUIRE( + verifyDatasetContents_f32(filename, "output_data", expectedOutputs)); + } + std::filesystem::remove_all(db_dir); +} \ No newline at end of file diff --git a/tests/AMSlib/perf_regression/CMakeLists.txt b/tests/AMSlib/perf_regression/CMakeLists.txt index 56ca1c67..03c510e1 100644 --- a/tests/AMSlib/perf_regression/CMakeLists.txt +++ b/tests/AMSlib/perf_regression/CMakeLists.txt @@ -3,7 +3,10 @@ function(BUILD_TEST exe source) target_include_directories(${exe} PRIVATE "${PROJECT_SOURCE_DIR}/src/AMSlib/" "${PROJECT_SOURCE_DIR}/src/AMSlib/include" ${caliper_INCLUDE_DIR} ${MPI_INCLUDE_PATH}) target_compile_definitions(${exe} PRIVATE ${AMS_APP_DEFINES}) message("On test defines are ${AMS_APP_DEFINES}") - target_link_libraries(${exe} PRIVATE AMS torch) + target_link_libraries(${exe} PRIVATE AMS) + if (ENABLE_TORCH) + target_link_libraries(${exe} PRIVATE torch) + endif() target_link_libraries(${exe} PRIVATE Threads::Threads) target_link_libraries(${exe} PRIVATE ${AMS_HDF5_LINK_TARGETS}) if (ENABLE_CALIPER) diff --git a/tests/AMSlib/perf_regression/ams_bench_db.cpp b/tests/AMSlib/perf_regression/ams_bench_db.cpp index 098b1b27..e1db7885 100644 --- a/tests/AMSlib/perf_regression/ams_bench_db.cpp +++ b/tests/AMSlib/perf_regression/ams_bench_db.cpp @@ -34,7 +34,7 @@ struct Problem { { } - void run(long num_elements, DType** inputs, DType** outputs) + void run(long num_elements, const DType* const* inputs, DType** outputs) { for (int i = 0; i < num_elements; i++) { DType sum = 0; @@ -97,7 +97,7 @@ struct Problem { [&](const ams::SmallVector& ams_ins, ams::SmallVector& ams_inouts, ams::SmallVector& ams_outs) { - DType* ins[num_inputs]; + const DType* ins[num_inputs]; DType* outs[num_outputs]; if (num_inputs != ams_ins.size()) throw std::runtime_error( diff --git a/tests/AMSlib/torch/CMakeLists.txt b/tests/AMSlib/torch/CMakeLists.txt index 9642a71a..49a001af 100644 --- a/tests/AMSlib/torch/CMakeLists.txt +++ b/tests/AMSlib/torch/CMakeLists.txt @@ -14,6 +14,9 @@ function(BUILD_UNIT_TEST exe source) target_link_libraries(${exe} PRIVATE hip::host) target_link_libraries(${exe} PRIVATE Threads::Threads) endif() + + target_link_libraries(${exe} PRIVATE fmt::fmt) + target_link_libraries(${exe} PRIVATE nlohmann_json::nlohmann_json) target_include_directories(${exe} PRIVATE ${CMAKE_SOURCE_DIR}/src/AMSlib/) target_include_directories(${exe} PRIVATE ${CMAKE_BINARY_DIR}/include/) @@ -36,13 +39,10 @@ endfunction() BUILD_UNIT_TEST(ams_surrogate_tests evaluate_model.cpp Catch2::Catch2) -target_link_libraries(ams_surrogate_tests PRIVATE fmt::fmt) ADD_TORCH_UNIT_TEST(Surrogate ams_surrogate_tests) BUILD_UNIT_TEST(ams_test_base_model test_model.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(ams_test_base_model PRIVATE fmt::fmt nlohmann_json::nlohmann_json) ADD_TORCH_UNIT_TEST(BaseModel ams_test_base_model) BUILD_UNIT_TEST(ams_test_inference_model test_inference_model.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(ams_test_inference_model PRIVATE fmt::fmt nlohmann_json::nlohmann_json) ADD_TORCH_UNIT_TEST(InferenceModel ams_test_inference_model) diff --git a/tests/AMSlib/wf/CMakeLists.txt b/tests/AMSlib/wf/CMakeLists.txt index 0b37f17c..e51da42c 100644 --- a/tests/AMSlib/wf/CMakeLists.txt +++ b/tests/AMSlib/wf/CMakeLists.txt @@ -21,11 +21,15 @@ function(BUILD_UNIT_TEST exe source) if (${ARGC} GREATER 3) target_sources(${exe} PRIVATE ${ARGV3}) endif() - target_link_libraries(${exe} PRIVATE stdc++fs AMS torch ${catch2_target}) + target_link_libraries(${exe} PRIVATE stdc++fs AMS ${catch2_target}) + if (ENABLE_TORCH) + target_link_libraries(${exe} PRIVATE torch) + endif() target_link_libraries(${exe} PRIVATE tl::expected) target_compile_definitions(${exe} PRIVATE ${AMS_APP_DEFINES} CATCH_CONFIG_PREFIX_ALL) target_link_libraries(${exe} PRIVATE ${AMS_HDF5_LINK_TARGETS}) + target_link_libraries(${exe} PRIVATE fmt::fmt) if (ENABLE_CUDA) @@ -54,36 +58,27 @@ function(BUILD_UNIT_TEST exe source) endif() endfunction() -BUILD_UNIT_TEST(operations operations.cpp Catch2::Catch2) -target_link_libraries(operations PRIVATE fmt::fmt) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::OPERATIONS operations) - -BUILD_UNIT_TEST(evaluate_in_and_outs evaluate_in_and_outs.cpp Catch2::Catch2) -target_link_libraries(evaluate_in_and_outs PRIVATE fmt::fmt) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::EVALUATE_IN_OUTS evaluate_in_and_outs) +# Tests that do NOT require torch -BUILD_UNIT_TEST(tensor_bundle tensor_bundle.cpp Catch2::Catch2 ../ams_catch_main.cpp) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::TENSOR_BUNDLE tensor_bundle) +if (ENABLE_TORCH) + BUILD_UNIT_TEST(operations operations.cpp Catch2::Catch2) + ADD_WORKFLOW_UNIT_TEST(WORKFLOW::OPERATIONS operations) -BUILD_UNIT_TEST(eval_context eval_context.cpp Catch2::Catch2 ../ams_catch_main.cpp) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::EVAL_CONTEXT eval_context) + BUILD_UNIT_TEST(evaluate_in_and_outs evaluate_in_and_outs.cpp Catch2::Catch2) + ADD_WORKFLOW_UNIT_TEST(WORKFLOW::EVALUATE_IN_OUTS evaluate_in_and_outs) -BUILD_UNIT_TEST(pointwise pointwise_layout_transform.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(pointwise PRIVATE fmt::fmt) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::POINTWISE pointwise) + BUILD_UNIT_TEST(eval_context eval_context.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_WORKFLOW_UNIT_TEST(WORKFLOW::EVAL_CONTEXT eval_context) -BUILD_UNIT_TEST(action action.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(action PRIVATE fmt::fmt) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::ACTION action) + BUILD_UNIT_TEST(action action.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_WORKFLOW_UNIT_TEST(WORKFLOW::ACTION action) -BUILD_UNIT_TEST(pipeline pipeline.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(pipeline PRIVATE fmt::fmt) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::PIPELINE pipeline) + BUILD_UNIT_TEST(pipeline pipeline.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_WORKFLOW_UNIT_TEST(WORKFLOW::PIPELINE pipeline) -BUILD_UNIT_TEST(policy policy.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(policy PRIVATE fmt::fmt) -ADD_WORKFLOW_UNIT_TEST(WORKFLOW::POLICY policy) + BUILD_UNIT_TEST(pointwise pointwise_layout_transform.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_WORKFLOW_UNIT_TEST(WORKFLOW::POINTWISE pointwise) -BUILD_UNIT_TEST(int_tensors int_tensors.cpp Catch2::Catch2 ../ams_catch_main.cpp) -target_link_libraries(int_tensors PRIVATE fmt::fmt) -ADD_WORKFLOW_UNIT_TEST(AMS_INT_TENSOR int_tensors) + BUILD_UNIT_TEST(policy policy.cpp Catch2::Catch2 ../ams_catch_main.cpp) + ADD_WORKFLOW_UNIT_TEST(WORKFLOW::POLICY policy) +endif() diff --git a/tests/AMSlib/wf/evaluate_in_and_outs.cpp b/tests/AMSlib/wf/evaluate_in_and_outs.cpp index 98384009..d49d0d89 100644 --- a/tests/AMSlib/wf/evaluate_in_and_outs.cpp +++ b/tests/AMSlib/wf/evaluate_in_and_outs.cpp @@ -159,7 +159,7 @@ static void compute(ams::AMSWorkflow& wf, for (auto& V : pruned_ins) { c10::IntArrayRef shape(V.shape().begin(), V.shape().size()); - in.push_back(torch::from_blob((void*)V.data(), + in.push_back(torch::from_blob(const_cast(V.data_ptr()), shape, torch::TensorOptions().dtype(DType).device( DeviceType))); @@ -167,13 +167,13 @@ static void compute(ams::AMSWorkflow& wf, for (auto& V : pruned_inouts) { c10::IntArrayRef shape(V.shape().begin(), V.shape().size()); inout.push_back(torch::from_blob( - (void*)V.data(), + V.data_ptr(), shape, torch::TensorOptions().dtype(DType).device(DeviceType))); } for (auto& V : pruned_outs) { c10::IntArrayRef shape(V.shape().begin(), V.shape().size()); - out.push_back(torch::from_blob((void*)V.data(), + out.push_back(torch::from_blob(V.data_ptr(), shape, torch::TensorOptions().dtype(DType).device( DeviceType))); diff --git a/tests/AMSlib/wf/int_tensors.cpp b/tests/AMSlib/wf/int_tensors.cpp deleted file mode 100644 index 323a2f7c..00000000 --- a/tests/AMSlib/wf/int_tensors.cpp +++ /dev/null @@ -1,444 +0,0 @@ -/* - * Copyright 2021-2023 Lawrence Livermore National Security, LLC and other - * AMSLib Project Developers - * - * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - */ - -#include -#include -#include -#include - -#ifdef __AMS_ENABLE_CUDA__ -#include -#elif defined(__AMS_ENABLE_HIP__) -#include -#endif - -#include "AMS.h" -#include "AMSTensor.hpp" -#include "wf/resource_manager.hpp" -#include "wf/utils.hpp" - -using namespace ams; - -// Compiled HIP/CUDA support does not guarantee a usable device on the test node. -static bool amsDeviceAvailable() -{ -#if defined(__AMS_ENABLE_CUDA__) - int count = 0; - return cudaGetDeviceCount(&count) == cudaSuccess && count > 0; -#elif defined(__AMS_ENABLE_HIP__) - int count = 0; - return hipGetDeviceCount(&count) == hipSuccess && count > 0; -#else - return false; -#endif -} - -static void skipUnavailableDevice(AMSResourceType device) -{ - if (device == AMSResourceType::AMS_DEVICE && !amsDeviceAvailable()) { - CATCH_SKIP("GPU device not available"); - } -} - -CATCH_TEST_CASE("AMSTensor: int32_t tensor creation and basic properties", - "[ams][tensor][int32]") -{ - AMSInit(); - - const auto device = - GENERATE(AMSResourceType::AMS_HOST, AMSResourceType::AMS_DEVICE); - skipUnavailableDevice(device); - - CATCH_SECTION("Create 1D int32_t tensor") - { - std::vector shape = {10}; - std::vector strides = {1}; - - auto tensor = AMSTensor::create(shape, strides, device); - - CATCH_REQUIRE(tensor.dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensor.elements() == 10); - CATCH_REQUIRE(tensor.element_size() == sizeof(int32_t)); - CATCH_REQUIRE(tensor.location() == device); - CATCH_REQUIRE(tensor.shape().size() == 1); - CATCH_REQUIRE(tensor.shape()[0] == 10); - // Note: contiguous() check removed due to pre-existing AMSTensor bug - } - - CATCH_SECTION("Create 2D int32_t tensor") - { - std::vector shape = {5, 8}; - std::vector strides = {8, 1}; - - auto tensor = AMSTensor::create(shape, strides, device); - - CATCH_REQUIRE(tensor.dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensor.elements() == 40); - CATCH_REQUIRE(tensor.element_size() == sizeof(int32_t)); - CATCH_REQUIRE(tensor.shape().size() == 2); - CATCH_REQUIRE(tensor.shape()[0] == 5); - CATCH_REQUIRE(tensor.shape()[1] == 8); - } - - CATCH_SECTION("Create 3D int32_t tensor") - { - std::vector shape = {4, 3, 2}; - std::vector strides = {6, 2, 1}; - - auto tensor = AMSTensor::create(shape, strides, device); - - CATCH_REQUIRE(tensor.dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensor.elements() == 24); - CATCH_REQUIRE(tensor.element_size() == sizeof(int32_t)); - } -} - -CATCH_TEST_CASE("AMSTensor: int64_t tensor creation and basic properties", - "[ams][tensor][int64]") -{ - AMSInit(); - - const auto device = - GENERATE(AMSResourceType::AMS_HOST, AMSResourceType::AMS_DEVICE); - skipUnavailableDevice(device); - - CATCH_SECTION("Create 1D int64_t tensor") - { - std::vector shape = {15}; - std::vector strides = {1}; - - auto tensor = AMSTensor::create(shape, strides, device); - - CATCH_REQUIRE(tensor.dType() == AMSDType::AMS_INT64); - CATCH_REQUIRE(tensor.elements() == 15); - CATCH_REQUIRE(tensor.element_size() == sizeof(int64_t)); - CATCH_REQUIRE(tensor.location() == device); - // Note: contiguous() check removed due to pre-existing AMSTensor bug - } - - CATCH_SECTION("Create 2D int64_t tensor") - { - std::vector shape = {6, 7}; - std::vector strides = {7, 1}; - - auto tensor = AMSTensor::create(shape, strides, device); - - CATCH_REQUIRE(tensor.dType() == AMSDType::AMS_INT64); - CATCH_REQUIRE(tensor.elements() == 42); - CATCH_REQUIRE(tensor.element_size() == sizeof(int64_t)); - } -} - -CATCH_TEST_CASE("AMSTensor: int32_t tensor view operations", - "[ams][tensor][int32][view]") -{ - AMSInit(); - - const auto device = GENERATE(AMSResourceType::AMS_HOST); - - CATCH_SECTION("Create view from existing int32_t data") - { - std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - std::vector shape = {10}; - std::vector strides = {1}; - - auto tensor_view = - AMSTensor::view(data.data(), shape, strides, device); - - CATCH_REQUIRE(tensor_view.dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensor_view.elements() == 10); - CATCH_REQUIRE(tensor_view.location() == device); - - // Verify we can access the data - auto* ptr = tensor_view.data(); - CATCH_REQUIRE(ptr != nullptr); - CATCH_REQUIRE(ptr[0] == 1); - CATCH_REQUIRE(ptr[9] == 10); - } - - CATCH_SECTION("Create 2D view from int32_t data") - { - std::vector data(20, 42); // 20 elements, all set to 42 - std::vector shape = {4, 5}; - std::vector strides = {5, 1}; - - auto tensor_view = - AMSTensor::view(data.data(), shape, strides, device); - - CATCH_REQUIRE(tensor_view.dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensor_view.elements() == 20); - CATCH_REQUIRE(tensor_view.shape()[0] == 4); - CATCH_REQUIRE(tensor_view.shape()[1] == 5); - - auto* ptr = tensor_view.data(); - CATCH_REQUIRE(ptr[0] == 42); - CATCH_REQUIRE(ptr[19] == 42); - } -} - -CATCH_TEST_CASE("AMSTensor: int64_t tensor view operations", - "[ams][tensor][int64][view]") -{ - AMSInit(); - - const auto device = GENERATE(AMSResourceType::AMS_HOST); - - CATCH_SECTION("Create view from existing int64_t data") - { - std::vector data = {100, 200, 300, 400, 500}; - std::vector shape = {5}; - std::vector strides = {1}; - - auto tensor_view = - AMSTensor::view(data.data(), shape, strides, device); - - CATCH_REQUIRE(tensor_view.dType() == AMSDType::AMS_INT64); - CATCH_REQUIRE(tensor_view.elements() == 5); - - auto* ptr = tensor_view.data(); - CATCH_REQUIRE(ptr[0] == 100); - CATCH_REQUIRE(ptr[4] == 500); - } -} - -CATCH_TEST_CASE("AMSTensor: int tensor transpose operations", - "[ams][tensor][transpose]") -{ - AMSInit(); - - const auto device = GENERATE(AMSResourceType::AMS_HOST); - - CATCH_SECTION("Transpose 2D int32_t tensor") - { - std::vector shape = {3, 4}; - std::vector strides = {4, 1}; - - auto tensor = AMSTensor::create(shape, strides, device); - auto transposed = tensor.transpose(0, 1); - - CATCH_REQUIRE(transposed.dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(transposed.shape()[0] == 4); - CATCH_REQUIRE(transposed.shape()[1] == 3); - CATCH_REQUIRE(transposed.elements() == 12); - } - - CATCH_SECTION("Transpose 2D int64_t tensor") - { - std::vector shape = {5, 6}; - std::vector strides = {6, 1}; - - auto tensor = AMSTensor::create(shape, strides, device); - auto transposed = tensor.transpose(0, 1); - - CATCH_REQUIRE(transposed.dType() == AMSDType::AMS_INT64); - CATCH_REQUIRE(transposed.shape()[0] == 6); - CATCH_REQUIRE(transposed.shape()[1] == 5); - CATCH_REQUIRE(transposed.elements() == 30); - } -} - -CATCH_TEST_CASE("AMSTensor: int tensor move semantics", "[ams][tensor][move]") -{ - AMSInit(); - - const auto device = GENERATE(AMSResourceType::AMS_HOST); - - CATCH_SECTION("Move int32_t tensor") - { - std::vector shape = {10}; - std::vector strides = {1}; - - auto tensor1 = AMSTensor::create(shape, strides, device); - auto* original_ptr = tensor1.data(); - - // Move construct - auto tensor2 = std::move(tensor1); - - CATCH_REQUIRE(tensor2.dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensor2.elements() == 10); - CATCH_REQUIRE(tensor2.data() == original_ptr); - } - - CATCH_SECTION("Move int64_t tensor") - { - std::vector shape = {20}; - std::vector strides = {1}; - - auto tensor1 = AMSTensor::create(shape, strides, device); - auto* original_ptr = tensor1.data(); - - // Move construct (not move assign, to avoid existing AMSTensor bug) - auto tensor2 = std::move(tensor1); - - CATCH_REQUIRE(tensor2.dType() == AMSDType::AMS_INT64); - CATCH_REQUIRE(tensor2.elements() == 20); - CATCH_REQUIRE(tensor2.data() == original_ptr); - } -} - -CATCH_TEST_CASE("AMSTensor: dtype_to_size utility for int types", - "[ams][utils]") -{ - CATCH_SECTION("Verify int32_t size") - { - size_t size = dtype_to_size(AMSDType::AMS_INT32); - CATCH_REQUIRE(size == sizeof(int32_t)); - CATCH_REQUIRE(size == 4); - } - - CATCH_SECTION("Verify int64_t size") - { - size_t size = dtype_to_size(AMSDType::AMS_INT64); - CATCH_REQUIRE(size == sizeof(int64_t)); - CATCH_REQUIRE(size == 8); - } - - CATCH_SECTION("Compare sizes") - { - size_t size_int32 = dtype_to_size(AMSDType::AMS_INT32); - size_t size_int64 = dtype_to_size(AMSDType::AMS_INT64); - size_t size_float = dtype_to_size(AMSDType::AMS_SINGLE); - size_t size_double = dtype_to_size(AMSDType::AMS_DOUBLE); - - CATCH_REQUIRE(size_int32 == size_float); // Both 4 bytes - CATCH_REQUIRE(size_int64 == size_double); // Both 8 bytes - CATCH_REQUIRE(size_int64 == 2 * size_int32); - } -} - -CATCH_TEST_CASE("AMSTensor: SmallVector of int tensors", - "[ams][tensor][smallvector]") -{ - AMSInit(); - - const auto device = GENERATE(AMSResourceType::AMS_HOST); - - CATCH_SECTION("Create vector of int32_t tensors") - { - ams::SmallVector tensors; - - std::vector shape1 = {5}; - std::vector shape2 = {10}; - std::vector strides = {1}; - - tensors.push_back(AMSTensor::create(shape1, strides, device)); - tensors.push_back(AMSTensor::create(shape2, strides, device)); - - CATCH_REQUIRE(tensors.size() == 2); - CATCH_REQUIRE(tensors[0].dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensors[1].dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensors[0].elements() == 5); - CATCH_REQUIRE(tensors[1].elements() == 10); - } - - CATCH_SECTION("Create vector of int64_t tensors") - { - ams::SmallVector tensors; - - std::vector shape = {7}; - std::vector strides = {1}; - - for (int i = 0; i < 3; ++i) { - tensors.push_back(AMSTensor::create(shape, strides, device)); - } - - CATCH_REQUIRE(tensors.size() == 3); - for (const auto& tensor : tensors) { - CATCH_REQUIRE(tensor.dType() == AMSDType::AMS_INT64); - CATCH_REQUIRE(tensor.elements() == 7); - } - } - - CATCH_SECTION("Mixed type tensors in SmallVector") - { - ams::SmallVector tensors; - - std::vector shape = {8}; - std::vector strides = {1}; - - tensors.push_back(AMSTensor::create(shape, strides, device)); - tensors.push_back(AMSTensor::create(shape, strides, device)); - tensors.push_back(AMSTensor::create(shape, strides, device)); - tensors.push_back(AMSTensor::create(shape, strides, device)); - - CATCH_REQUIRE(tensors.size() == 4); - CATCH_REQUIRE(tensors[0].dType() == AMSDType::AMS_SINGLE); - CATCH_REQUIRE(tensors[1].dType() == AMSDType::AMS_INT32); - CATCH_REQUIRE(tensors[2].dType() == AMSDType::AMS_DOUBLE); - CATCH_REQUIRE(tensors[3].dType() == AMSDType::AMS_INT64); - } -} - -CATCH_TEST_CASE("AMSTensor: int tensor data access and modification", - "[ams][tensor][data]") -{ - AMSInit(); - - const auto device = GENERATE(AMSResourceType::AMS_HOST); - - CATCH_SECTION("Write and read int32_t data") - { - std::vector shape = {5}; - std::vector strides = {1}; - - auto tensor = AMSTensor::create(shape, strides, device); - auto* data = tensor.data(); - - // Write data - for (int i = 0; i < 5; ++i) { - data[i] = i * 10; - } - - // Read data back - CATCH_REQUIRE(data[0] == 0); - CATCH_REQUIRE(data[1] == 10); - CATCH_REQUIRE(data[2] == 20); - CATCH_REQUIRE(data[3] == 30); - CATCH_REQUIRE(data[4] == 40); - } - - CATCH_SECTION("Write and read int64_t data") - { - std::vector shape = {3}; - std::vector strides = {1}; - - auto tensor = AMSTensor::create(shape, strides, device); - auto* data = tensor.data(); - - // Write large values - data[0] = 1000000000LL; - data[1] = 2000000000LL; - data[2] = 3000000000LL; - - // Read data back - CATCH_REQUIRE(data[0] == 1000000000LL); - CATCH_REQUIRE(data[1] == 2000000000LL); - CATCH_REQUIRE(data[2] == 3000000000LL); - } - - CATCH_SECTION("2D int32_t tensor data access") - { - std::vector shape = {3, 4}; - std::vector strides = {4, 1}; - - auto tensor = AMSTensor::create(shape, strides, device); - auto* data = tensor.data(); - - // Fill with row-major data - for (int i = 0; i < 3; ++i) { - for (int j = 0; j < 4; ++j) { - data[i * 4 + j] = i * 10 + j; - } - } - - // Verify access - CATCH_REQUIRE(data[0] == 0); // [0,0] - CATCH_REQUIRE(data[3] == 3); // [0,3] - CATCH_REQUIRE(data[4] == 10); // [1,0] - CATCH_REQUIRE(data[11] == 23); // [2,3] - } -}