forked from NSCCN/hpc-app
feat: add HPC stencil demo app
This commit is contained in:
commit
b75b6d1a10
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
build/
|
||||
dist/
|
||||
*.log
|
||||
*.out
|
||||
*.err
|
||||
*.tar.gz
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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`:作为一体化编排入口。
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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 : ¤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<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;
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue