feat: add HPC stencil demo app

This commit is contained in:
xxq250 2026-06-12 09:28:17 +08:00
commit b75b6d1a10
9 changed files with 473 additions and 0 deletions

37
.github/workflows/ci-basic.yml vendored Normal file
View File

@ -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

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
build/
dist/
*.log
*.out
*.err
*.tar.gz

71
CMakeLists.txt Normal file
View File

@ -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
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-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 $<TARGET_FILE:stencil_mpi> --size 1000 --steps 20)
endif()
if(ENABLE_BENCHMARKS AND ENABLE_MPI)
add_test(NAME benchmark_stencil_mpi COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 $<TARGET_FILE:stencil_mpi> --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)

22
Dockerfile Normal file
View File

@ -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"]

82
README.md Normal file
View File

@ -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`:作为一体化编排入口。

25
include/stencil.hpp Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include <cstddef>
#include <vector>
namespace hpc_demo {
struct StencilConfig {
std::size_t global_size = 100000;
int steps = 100;
double alpha = 0.25;
};
std::vector<double> make_initial_field(std::size_t size, std::size_t global_offset = 0);
void stencil_step(const std::vector<double>& current,
std::vector<double>& next,
double left_ghost,
double right_ghost,
double alpha);
double checksum(const std::vector<double>& values);
} // namespace hpc_demo

112
src/main_mpi.cpp Normal file
View File

@ -0,0 +1,112 @@
#include "stencil.hpp"
#include <mpi.h>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
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::size_t>(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<std::size_t>(world_size);
const std::size_t extra = global_size % static_cast<std::size_t>(world_size);
return base + (static_cast<std::size_t>(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<double> 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 : &current.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 : &current.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<double>(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;
}

51
src/stencil.cpp Normal file
View File

@ -0,0 +1,51 @@
#include "stencil.hpp"
#include <cmath>
#include <numeric>
#include <stdexcept>
namespace hpc_demo {
std::vector<double> make_initial_field(std::size_t size, std::size_t global_offset) {
std::vector<double> values(size);
constexpr double pi = 3.14159265358979323846;
for (std::size_t i = 0; i < size; ++i) {
const double x = static_cast<double>(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<double>& current,
std::vector<double>& 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<double>& values) {
return std::accumulate(values.begin(), values.end(), 0.0);
}
} // namespace hpc_demo

66
tests/test_stencil.cpp Normal file
View File

@ -0,0 +1,66 @@
#include "stencil.hpp"
#include <cmath>
#include <iostream>
#include <stdexcept>
#include <vector>
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<double> current(8, 3.0);
std::vector<double> 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<double> 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<double> current(4, 1.0);
std::vector<double> 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;
}