以下是科研实习第三周的进度

psc-config/psc 代码学习

论文学习

psc-config/psc 的论文以The Plasma Simulation Code: A modern particle-in-cell code with patch-based load-balancing为题发表在Journal of Computational Physics上,为了更好的理解psc-config/psc的代码,翻阅一下论文。

论文第四章中讲述了psc-config/psc中的并行策略,但是由于并行策略是关于粒子系统网格化的详细讨论,对于当前项目没有特别大的意义,但是其中的负载均衡策略是值得学习的。

负载均衡:用于在多个服务器或资源之间分配工作负载,以确保系统的高可用性和高性能。其主要目标是优化资源使用、最大化吞吐量、最小化响应时间,并避免某个单一资源过载。

论文指出:给定一定数量的处理单元N_proc,将域划分成比处理单元数量更多的补丁N_patch,每个处理单元处理多个补丁(通常10-100个)。通过动态调整补丁到处理单元的分配,我们可以确保每个处理单元被分配到几乎相等的负载。

demo:

我们将域划分为16×16个子域,如图所示。由于现在Patch(256个)远多于进程(16个),每个进程需要处理多个Patch,因此需要定义一个将Patch分配给进程的策略。图(b)中沿着1维曲线,每个Patch被访问一次。然后将这条256个Patch的曲线划分成与进程数量相同的段,在这种情况下我们得到16个每个包含16个Patch的段。然后将这些Patch段依次分配给每个MPI进程。最终(见图(c)),得到的空间分解与使用4×4子域的标准分区方式基本相同,但获得了额外的灵活性,可以通过将Patch从负载较高的进程移动到负载较低的相邻进程来应对负载变化。

简单说就是进一步拆分一个进程中的处理,使一个进程中的处理离散化,从而可以使一个进程中的运行项可以成比例的产生负载,每个进程的Patch可以伪动态进行分配。

代码学习-变量/方法名的统一性

#ifdef USE_CUDA
using PscConfig = PscConfig1vbecCuda<Dim>;
#else
using PscConfig = PscConfig1vbecDouble<Dim>;
#endif

在主程序中,我们会引用不同的变量,对于同一个功能不同设备的变量,尽可能保证两个设备上的变量的成员变量具有一定的统一性,这样我们完全可以 使用同一个变量来指代不同设备上的配置。

同样,我们可以在不同的头文件中定义同一个方法,具有相同的参数、返回值,然后引入的头文件不同,实现不同位置的计算:

代码学习-使用类对底层进行封装

使用类进行封装,使其在主要代码中不可知底层实现(同样以上周的为例),我们在后面的引用中,只使用Psc这个类(C++的结构体和类已经泛化,下统称类), 使用C++模板语法,传入的PscConfig可以根据需求使用不同设备的PscConfig(不同设备的PscConfigXXXX均继承PscConfig类)

template <typename PscConfig, typename Diagnostics, typename InjectParticles>
struct Psc
{
  using Mparticles = typename PscConfig::Mparticles;
  using MfieldsState = typename PscConfig::MfieldsState;
  using Balance = typename PscConfig::Balance;
  using Sort = typename PscConfig::Sort;
  using Collision = typename PscConfig::Collision;
  using Checks = typename PscConfig::Checks;
  using Marder = typename PscConfig::Marder;
  using PushParticles = typename PscConfig::PushParticles;
  using PushFields = typename PscConfig::PushFields;
  using Bnd = typename PscConfig::Bnd;
  using BndFields = typename PscConfig::BndFields;
  using BndParticles = typename PscConfig::BndParticles;
  using Dim = typename PscConfig::Dim;
//...省略
}

代码学习-CMAKE模板的使用

#ifndef PSCCONFIG_H_
#define PSCCONFIG_H_

/* PSC Version Information */
#define PSC_VERSION_MAJOR @PSC_VERSION_MAJOR@
#define PSC_VERSION_MINOR @PSC_VERSION_MINOR@
#define PSC_VERSION_PATCH @PSC_VERSION_PATCH@
#define PSC_VERSION       @PSC_VERSION@

/*
 * PSC Build Information:
 *
 * Compiler:
 *   C: @CMAKE_C_COMPILER@
 *     Id: @CMAKE_C_COMPILER_ID@ @CMAKE_C_COMPILER_VERSION@
 *   CXX: @CMAKE_CXX_COMPILER@
 *     Id: @CMAKE_CXX_COMPILER_ID@ @CMAKE_CXX_COMPILER_VERSION@
 *   Fortran: @CMAKE_CXX_COMPILER@
 *     Id: @CMAKE_Fortran_COMPILER_ID@ @CMAKE_CXX_COMPILER_VERSION@
 */

#cmakedefine USE_CUDA
#cmakedefine USE_VPIC

/* Everything past this line is programatically generated */

@PSC_CONFIG_DEFINES@

#endif /* PSCCONFIG_H_ */

使用Cmake模板,自动生成配置在CMakeLists.txt中的头文件,这里将cmake定义的环境变量引入了代码。

代码学习-rmm的使用

RAPIDS Memory Manager(rmm)可以轻松的将host上的内存转换到device上的显存,同时如无显卡则不变,是一个快速的内存管理系统。

psc-config/psc中大量使用了rmm来简化内存分配问题

  static bool first_time = true;
  if (!first_time)
    return;

  first_time = false;

#ifdef PSC_HAVE_RMM
  rmm::logger().set_level(spdlog::level::trace);

  device_mr_type* mr =
    rmm::mr::get_current_device_resource(); // Points to `cuda_memory_resource`
  static log_mr_type _log_mr{mr, std::cout, true};
  static pool_mr_type pool_mr{&_log_mr, 15000000000};
  static track_mr_type track_mr{&pool_mr};
#if 0
  static log_mr_type log_mr{&track_mr, std::cout, true};
  rmm::mr::set_current_device_resource(&log_mr);

代码实践

使用psc-config/psc类似的方法,求解二维方形边界上关于第一类边界条件的拉普拉斯方程。

文件结构:

gp-demo/
├── CMakeLists.txt
├── include
│   ├── laplace.cuh
│   └── laplace.h
└── src
    ├── CMakeLists.txt
    ├── laplace.cpp
    ├── laplace.cu
    └── main.cpp

根据psc-config的思路,我们在CMakeLists.txt中指定环境变量

# ./CMakeLists.txt


cmake_minimum_required(VERSION 3.0)
project(gp_demo)
option(USE_CUDA "Build CUDA components" ON)

if (USE_CUDA)
    enable_language(CUDA CXX)
else ()
    enable_language(CXX)
endif()
set(CMAKE_CUDA_ARCHITECTURES all-major)
set(CMAKE_CUDA_STANDARD 17)

include_directories("./include")
add_subdirectory(./src)
# ./src/CMakeLists.txt

include_directories("/usr/local/cuda-11.8/targets/x86_64-linux/include")

if (USE_CUDA)
    find_package(CUDA REQUIRED)
    add_executable(gp-main main.cpp
            laplace.cu)
    set_target_properties(gp-main PROPERTIES
            CUDA_SEPARABLE_COMPILATION ON)
else()
    add_executable(gp-main main.cpp
        laplace.cpp)
endif()

在C++代码中,我们使用psc-config/psc的方法

// ./src/main.cpp

#ifdef USE_CUDA
#include <laplace.cuh>
#else
#include <laplace.h>
#endif

然后我们采用不同文件下的同名方法

// ./include/laplace.h

#ifndef LAPLACE_H
#define LAPLACE_H

#define IDX(i, j, cols) ((i) * (cols) + (j))
void solveLaplace(double* grid, int rows, int cols, int max_iter = 10000, double tol = 1e-6);
#endif


// ./include/laplace.cuh

#ifndef LAPLACE_CUH
#define LAPLACE_CUH
#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#define IDX(i, j, cols) ((i) * (cols) + (j))
__global__ void laplaceKernel(double* grid, double* new_grid, int rows, int cols);
void solveLaplace(double* grid, int rows, int cols, int max_iter = 10000, double tol = 1e-6);
#endif 

对应不同的实现为:

// openmp
void solveLaplace(double* grid, int rows, int cols, int max_iter, double tol) {
    double* new_grid = new double[rows * cols];

    for (int iter = 0; iter < max_iter; ++iter) {
        double max_error = 0.0;
#pragma omp parallel for reduction(max:max_error)
        for (int i = 1; i < rows - 1; ++i) {
            for (int j = 1; j < cols - 1; ++j) {
                int idx = IDX(i, j, cols);
                new_grid[idx] = 0.25 * (grid[IDX(i-1, j, cols)] + grid[IDX(i+1, j, cols)] + grid[IDX(i, j-1, cols)] + grid[IDX(i, j+1, cols)]);
                double error = std::fabs(new_grid[idx] - grid[idx]);
                max_error = std::max(max_error, error);
            }
        }
#pragma omp parallel for
        for (int i = 1; i < rows - 1; ++i) {
            for (int j = 1; j < cols - 1; ++j) {
                grid[IDX(i, j, cols)] = new_grid[IDX(i, j, cols)];
            }
        }
        if (max_error < tol) {
            std::cout << "Converged after " << iter + 1 << " iterations with error " << max_error << std::endl;
            break;
        }
    }
    delete[] new_grid;
}


// cuda
__global__ void laplaceKernel(double* grid, double* new_grid, int rows, int cols) {
    int i = blockIdx.y * blockDim.y + threadIdx.y;
    int j = blockIdx.x * blockDim.x + threadIdx.x;

    if (i > 0 && i < rows - 1 && j > 0 && j < cols - 1) {
        new_grid[IDX(i, j, cols)] = 0.25 * (grid[IDX(i-1, j, cols)] + grid[IDX(i+1, j, cols)]
                                            + grid[IDX(i, j-1, cols)] + grid[IDX(i, j+1, cols)]);
    }
}

void solveLaplace(double* grid, int rows, int cols, int max_iter, double tol) {
    double *d_grid, *d_new_grid;
    cudaMalloc(&d_grid, rows * cols * sizeof(double));
    cudaMalloc(&d_new_grid, rows * cols * sizeof(double));

    cudaMemcpy(d_grid, grid, rows * cols * sizeof(double), cudaMemcpyHostToDevice);

    //这里我指定了大小
    dim3 blockSize(10, 10);
    dim3 gridSize(10,10);

    for (int iter = 0; iter < max_iter; ++iter) {
        laplaceKernel<<<gridSize, blockSize>>>(d_grid, d_new_grid, rows, cols);
        cudaDeviceSynchronize();
        cudaMemcpy(grid, d_new_grid, rows * cols * sizeof(double), cudaMemcpyDeviceToHost);

        double max_error = 0.0;
        for (int i = 1; i < rows - 1; ++i) {
            for (int j = 1; j < cols - 1; ++j) {
                double error = std::fabs(grid[IDX(i, j, cols)] - d_grid[IDX(i, j, cols)]);
                if (error > max_error) {
                    max_error = error;
                }
            }
        }

        if (max_error < tol) {
            std::cout << "Converged after " << iter + 1 << " iterations with error " << max_error << std::endl;
            break;
        }

        std::swap(d_grid, d_new_grid);
    }

    cudaMemcpy(grid, d_grid, rows * cols * sizeof(double), cudaMemcpyDeviceToHost);

    cudaFree(d_grid);
    cudaFree(d_new_grid);
}

主函数信息:

int main() {
    int rows = 100;
    int cols = 100;
    double* grid = new double[rows * cols];

    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            grid[IDX(i, j, cols)] = 0.0;
        }
    }

    for (int i = 0; i < rows; ++i) {
        grid[IDX(i, 0, cols)] = 0.0; // 左边界条件
        grid[IDX(i, cols-1, cols)] = 1.0; // 右边界条件
    }
    for (int j = 0; j < cols; ++j) {
        grid[IDX(0, j, cols)] = 0.0; // 上边界条件
        grid[IDX(rows-1, j, cols)] = 1.0; // 下边界条件
    }

    solveLaplace(grid, rows, cols);

    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            std::cout << grid[IDX(i, j, cols)] << " ";
        }
        std::cout << std::endl;
    }
    delete[] grid;
    return 0;
}