commit b75b6d1a105675f712d5b8f6536f6d191043e9b6 Author: xxq250 Date: Fri Jun 12 09:28:17 2026 +0800 feat: add HPC stencil demo app diff --git a/.github/workflows/ci-basic.yml b/.github/workflows/ci-basic.yml new file mode 100644 index 0000000..dedad54 --- /dev/null +++ b/.github/workflows/ci-basic.yml @@ -0,0 +1,37 @@ +name: Demo CI Basic + +on: + pull_request: + push: + branches: [main, master, develop] + workflow_dispatch: + +jobs: + build-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build gfortran openmpi-bin libopenmpi-dev + + - name: Configure + run: | + cmake -S . -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_MPI=ON \ + -DENABLE_TESTS=ON \ + -DENABLE_BENCHMARKS=ON + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure --parallel 2 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51bbbaa --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +dist/ +*.log +*.out +*.err +*.tar.gz + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c2db988 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.18) + +project(hpc_stencil_demo LANGUAGES CXX) + +option(ENABLE_MPI "Build MPI executable" ON) +option(ENABLE_TESTS "Build tests" ON) +option(ENABLE_BENCHMARKS "Enable benchmark CTest labels" ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_library(stencil_core + src/stencil.cpp +) +target_include_directories(stencil_core + PUBLIC + ${PROJECT_SOURCE_DIR}/include +) +target_compile_options(stencil_core + PRIVATE + $<$:-Wall -Wextra -Wpedantic> +) + +if(ENABLE_MPI) + find_package(MPI REQUIRED) + + add_executable(stencil_mpi + src/main_mpi.cpp + ) + target_link_libraries(stencil_mpi + PRIVATE + stencil_core + MPI::MPI_CXX + ) +endif() + +if(ENABLE_TESTS) + enable_testing() + + add_executable(stencil_unit_tests + tests/test_stencil.cpp + ) + target_link_libraries(stencil_unit_tests + PRIVATE + stencil_core + ) + + add_test(NAME stencil_unit_tests COMMAND stencil_unit_tests) + + if(ENABLE_MPI) + add_test(NAME stencil_mpi_smoke COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 2 $ --size 1000 --steps 20) + endif() + + if(ENABLE_BENCHMARKS AND ENABLE_MPI) + add_test(NAME benchmark_stencil_mpi COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 $ --size 200000 --steps 200) + set_tests_properties(benchmark_stencil_mpi PROPERTIES LABELS benchmark) + endif() +endif() + +install(TARGETS stencil_core + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib +) + +if(ENABLE_MPI) + install(TARGETS stencil_mpi RUNTIME DESTINATION bin) +endif() + +install(DIRECTORY include/ DESTINATION include) + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c65ee9c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:24.04 + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + cmake \ + gfortran \ + libopenmpi-dev \ + ninja-build \ + openmpi-bin \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace +COPY . . + +RUN cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_MPI=ON -DENABLE_TESTS=ON \ + && cmake --build build --parallel \ + && ctest --test-dir build --output-on-failure + +CMD ["mpirun", "--allow-run-as-root", "-np", "2", "./build/stencil_mpi", "--size", "10000", "--steps", "50"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..0370c4c --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# HPC Stencil Demo + +这是一个用于验证“超算应用 CI/CD 流水线编排”的最小可运行 demo。项目使用 CMake + C++17 + MPI 实现一维热扩散 stencil 计算,并提供单元测试、MPI 冒烟测试、性能基准标签和容器构建入口。 + +## 目录结构 + +```text +. +├── CMakeLists.txt +├── Dockerfile +├── include/stencil.hpp +├── src/stencil.cpp +├── src/main_mpi.cpp +├── tests/test_stencil.cpp +└── .github/workflows/ci-basic.yml +``` + +## 本地构建 + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_MPI=ON -DENABLE_TESTS=ON +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` + +## MPI 运行 + +```bash +mpirun -np 4 ./build/stencil_mpi --size 200000 --steps 200 +``` + +输出示例: + +```text +hpc_stencil_demo +ranks=4 +global_size=200000 +steps=200 +checksum=... +elapsed_seconds=... +``` + +## Slurm 作业示例 + +```bash +#!/usr/bin/env bash +#SBATCH --job-name=hpc-stencil-demo +#SBATCH --partition=normal +#SBATCH --nodes=1 +#SBATCH --ntasks=4 +#SBATCH --time=00:10:00 +#SBATCH --output=stencil-%j.out +#SBATCH --error=stencil-%j.err + +set -euo pipefail + +module purge +module load gcc openmpi cmake + +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_MPI=ON -DENABLE_TESTS=ON +cmake --build build --parallel +ctest --test-dir build --output-on-failure +srun ./build/stencil_mpi --size 1000000 --steps 500 +``` + +## 容器构建 + +```bash +docker build -t hpc-stencil-demo . +docker run --rm hpc-stencil-demo +``` + +## 与流水线模板配合 + +本 demo 的 `.github/workflows/ci-basic.yml` 可直接用于 GitHub Actions 基础验证。你也可以把前面生成的模板目录中的其他 workflow 复制进来: + +- `ci-matrix.yml`:验证不同 MPI 实现或构建类型。 +- `hpc-remote-slurm.yml`:提交到真实超算 Slurm 集群。 +- `performance-benchmark.yml`:运行带 `benchmark` 标签的 CTest。 +- `container-build.yml`:发布容器镜像到 GHCR。 +- `pipeline-orchestrator.yml`:作为一体化编排入口。 + diff --git a/include/stencil.hpp b/include/stencil.hpp new file mode 100644 index 0000000..3465c60 --- /dev/null +++ b/include/stencil.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace hpc_demo { + +struct StencilConfig { + std::size_t global_size = 100000; + int steps = 100; + double alpha = 0.25; +}; + +std::vector make_initial_field(std::size_t size, std::size_t global_offset = 0); + +void stencil_step(const std::vector& current, + std::vector& next, + double left_ghost, + double right_ghost, + double alpha); + +double checksum(const std::vector& values); + +} // namespace hpc_demo + diff --git a/src/main_mpi.cpp b/src/main_mpi.cpp new file mode 100644 index 0000000..83c0eb8 --- /dev/null +++ b/src/main_mpi.cpp @@ -0,0 +1,112 @@ +#include "stencil.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +hpc_demo::StencilConfig parse_args(int argc, char** argv) { + hpc_demo::StencilConfig config; + + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg == "--size" && i + 1 < argc) { + config.global_size = static_cast(std::stoull(argv[++i])); + } else if (arg == "--steps" && i + 1 < argc) { + config.steps = std::stoi(argv[++i]); + } else if (arg == "--alpha" && i + 1 < argc) { + config.alpha = std::stod(argv[++i]); + } else if (arg == "--help") { + std::cout << "Usage: stencil_mpi [--size N] [--steps N] [--alpha VALUE]\n"; + std::exit(0); + } else { + throw std::invalid_argument("unknown or incomplete argument: " + arg); + } + } + + return config; +} + +std::size_t local_size_for_rank(std::size_t global_size, int rank, int world_size) { + const std::size_t base = global_size / static_cast(world_size); + const std::size_t extra = global_size % static_cast(world_size); + return base + (static_cast(rank) < extra ? 1 : 0); +} + +std::size_t offset_for_rank(std::size_t global_size, int rank, int world_size) { + std::size_t offset = 0; + for (int r = 0; r < rank; ++r) { + offset += local_size_for_rank(global_size, r, world_size); + } + return offset; +} + +} // namespace + +int main(int argc, char** argv) { + MPI_Init(&argc, &argv); + + int rank = 0; + int world_size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + + try { + const auto config = parse_args(argc, argv); + const auto local_size = local_size_for_rank(config.global_size, rank, world_size); + const auto global_offset = offset_for_rank(config.global_size, rank, world_size); + + auto current = hpc_demo::make_initial_field(local_size, global_offset); + std::vector next(local_size, 0.0); + + const auto start = std::chrono::steady_clock::now(); + + for (int step = 0; step < config.steps; ++step) { + double left_ghost = local_size == 0 ? 0.0 : current.front(); + double right_ghost = local_size == 0 ? 0.0 : current.back(); + + const int left_rank = rank == 0 ? MPI_PROC_NULL : rank - 1; + const int right_rank = rank + 1 == world_size ? MPI_PROC_NULL : rank + 1; + + MPI_Sendrecv(local_size == 0 ? MPI_BOTTOM : ¤t.front(), 1, MPI_DOUBLE, left_rank, 10, + &right_ghost, 1, MPI_DOUBLE, right_rank, 10, + MPI_COMM_WORLD, MPI_STATUS_IGNORE); + + MPI_Sendrecv(local_size == 0 ? MPI_BOTTOM : ¤t.back(), 1, MPI_DOUBLE, right_rank, 20, + &left_ghost, 1, MPI_DOUBLE, left_rank, 20, + MPI_COMM_WORLD, MPI_STATUS_IGNORE); + + hpc_demo::stencil_step(current, next, left_ghost, right_ghost, config.alpha); + current.swap(next); + } + + const double local_checksum = hpc_demo::checksum(current); + double global_checksum = 0.0; + MPI_Reduce(&local_checksum, &global_checksum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); + + const auto end = std::chrono::steady_clock::now(); + const auto elapsed = std::chrono::duration(end - start).count(); + + if (rank == 0) { + std::cout << "hpc_stencil_demo\n"; + std::cout << "ranks=" << world_size << "\n"; + std::cout << "global_size=" << config.global_size << "\n"; + std::cout << "steps=" << config.steps << "\n"; + std::cout << "checksum=" << global_checksum << "\n"; + std::cout << "elapsed_seconds=" << elapsed << "\n"; + } + } catch (const std::exception& ex) { + std::cerr << "rank " << rank << " failed: " << ex.what() << "\n"; + MPI_Abort(MPI_COMM_WORLD, 1); + } + + MPI_Finalize(); + return 0; +} + diff --git a/src/stencil.cpp b/src/stencil.cpp new file mode 100644 index 0000000..8981d99 --- /dev/null +++ b/src/stencil.cpp @@ -0,0 +1,51 @@ +#include "stencil.hpp" + +#include +#include +#include + +namespace hpc_demo { + +std::vector make_initial_field(std::size_t size, std::size_t global_offset) { + std::vector values(size); + constexpr double pi = 3.14159265358979323846; + + for (std::size_t i = 0; i < size; ++i) { + const double x = static_cast(global_offset + i); + values[i] = std::sin(x * pi / 180.0) + 0.1 * std::cos(x * pi / 17.0); + } + + return values; +} + +void stencil_step(const std::vector& current, + std::vector& next, + double left_ghost, + double right_ghost, + double alpha) { + if (current.empty()) { + next.clear(); + return; + } + + if (next.size() != current.size()) { + next.assign(current.size(), 0.0); + } + + if (alpha < 0.0 || alpha > 0.5) { + throw std::invalid_argument("alpha must be in [0, 0.5] for a stable diffusion step"); + } + + for (std::size_t i = 0; i < current.size(); ++i) { + const double left = (i == 0) ? left_ghost : current[i - 1]; + const double right = (i + 1 == current.size()) ? right_ghost : current[i + 1]; + next[i] = current[i] + alpha * (left - 2.0 * current[i] + right); + } +} + +double checksum(const std::vector& values) { + return std::accumulate(values.begin(), values.end(), 0.0); +} + +} // namespace hpc_demo + diff --git a/tests/test_stencil.cpp b/tests/test_stencil.cpp new file mode 100644 index 0000000..b6e34b5 --- /dev/null +++ b/tests/test_stencil.cpp @@ -0,0 +1,66 @@ +#include "stencil.hpp" + +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) { + throw std::runtime_error(message); + } +} + +void test_initial_field_size() { + const auto values = hpc_demo::make_initial_field(16); + require(values.size() == 16, "initial field should have requested size"); +} + +void test_constant_field_is_stable() { + const std::vector current(8, 3.0); + std::vector next; + + hpc_demo::stencil_step(current, next, 3.0, 3.0, 0.25); + + for (double value : next) { + require(std::fabs(value - 3.0) < 1e-12, "constant field should remain constant"); + } +} + +void test_checksum() { + const std::vector values{1.0, 2.0, 3.5}; + require(std::fabs(hpc_demo::checksum(values) - 6.5) < 1e-12, "checksum should sum values"); +} + +void test_invalid_alpha_fails() { + const std::vector current(4, 1.0); + std::vector next; + + try { + hpc_demo::stencil_step(current, next, 1.0, 1.0, 0.75); + } catch (const std::invalid_argument&) { + return; + } + + throw std::runtime_error("invalid alpha should throw"); +} + +} // namespace + +int main() { + try { + test_initial_field_size(); + test_constant_field_is_stable(); + test_checksum(); + test_invalid_alpha_fails(); + } catch (const std::exception& ex) { + std::cerr << "test failed: " << ex.what() << "\n"; + return 1; + } + + std::cout << "all stencil tests passed\n"; + return 0; +} +