使用HIP和OpenMP卸载的Jacobi求解器

news2024/10/19 6:35:31

Jacobi Solver with HIP and OpenMP offloading — ROCm Blogs (amd.com)

作者:Asitav Mishra, Rajat Arora, Justin Chang
发布日期:2023年9月15日

Jacobi方法作为求解偏微分方程(PDE)的基本迭代线性求解器在高性能计算(HPC)应用中具有广泛的应用。通过数值方法(如有限差分法、有限体积法、有限元法或其他方法)对PDE进行离散化,会产生大型稀疏方程组。像Jacobi这样的静止迭代法可以利用现代异构分层系统(包括CPU和GPU)的优势,因为它更适合并行化并且相比传统的直接方法需要更少的内存。Jacobi迭代涉及大量重复的矩阵-向量乘积操作,在很大程度上限制了每次迭代中所有组件之间的通信。这使得Jacobi方法在GPU卸载方面更具优势。

在这篇博客中,我们探索了使用HIP和OpenMP目标指令进行GPU卸载的方法,并讨论了它们在实现成本和性能方面的相对优劣。

注意: 尽管大多数HPC应用今天都使用消息传递接口(MPI)进行分布式内存并行化,但在这篇博客中,我们仅考虑Jacobi方法的单进程实现以进行演示。

Jacobi 迭代方法

为了讨论 Jacobi 迭代方法,我们考虑一个具有 Dirichlet 边界条件的二维领域上的偏微分方程(Partial Differential Equation, PDE),以求解泊松方程:−∇²u(x,y) = f,这是一个广义拉普拉斯方程。在这里,u(x,y) 是域内的光滑函数。方程使用有限差分法 [1] 在笛卡尔坐标系下进行离散,采用 5 点模板:

-\frac{u_{i-1,j} + 2u_{i,j} - u_{i+1,j}}{\Delta x^2} + -\frac{u_{i,j-1} + 2u_{i,j} - u_{i,j+1}}{\Delta y^2} = f_{i,j}

上述有限差分拉普拉斯算子导致了矢量未知数 u 上的稀疏矩阵算子 A:Au = f。这里:

u = [u_{1,1}, u_{1,2}, \cdots, u_{2,1}, \cdots, u_{i,j}, \cdots, u_{n_i, n_j}]

如果矩阵分解为对角线部分(D)、下三角部分(L)和上三角部分(U)(A = D + L + U),则 Jacobi 迭代方法可以表示为:

u^{k+1} = u^k + D^{-1}(f - Au^k)

这里,k 指的是迭代次数。u^{k=0} 的值是初始条件。

Jacobi代码结构

串行(CPU)Jacobi求解器的主要组件包括:

  • 设置Jacobi对象

    • CreateMesh()

    • InitializeData()

  • 执行Jacobi方法

    • Laplacian()

    • BoundaryConditions()

    • Update()

    • Norm()

以下代码显示了Jacobi求解器程序。在代码中,Jacobi_t Jacobi(mesh); 完成了Jacobi对象的设置,而 Jacobi.Run() 执行了Jacobi求解器。

int main(int argc, char ** argv)
{
  mesh_t mesh;

  //  从命令行参数中提取拓扑结构和域维度
  ParseCommandLineArguments(argc, argv, mesh);

  Jacobi_t Jacobi(mesh);
  Jacobi.Run();

  return STATUS_OK;
}

设置Jacobi对象

Jacobi对象由两个主要部分组成:

  • 设置Jacobi对象

    • CreateMesh()

    • InitializeData()

例程 CreateMesh() 创建一个具有均匀网格间距的二维笛卡尔网格——参见图1。例程 InitializeData() 初始化主机变量u(代码中的 h_U),矩阵-向量乘积Au(h_AU),右侧向量f(h_RHS),以及残差向量f−Au(h_RES)。它还为这些变量应用边界条件。

../../_images/grid.png

图1:用于离散化计算域的均匀矩形网格

执行雅可比方法

雅可比求解器的主要执行包括以下函数:

  • 运行雅可比方法

    • Laplacian()

    • BoundaryConditions()

    • Update()

    • Norm()

下面的代码展示了每个雅可比迭代中函数调用的顺序:

  // 在雅可比方法中使用的标量因子
  const dfloat factor = 1/(2.0/(mesh.dx*mesh.dx) + 2.0/(mesh.dy*mesh.dy));
  const dfloat _1bydx2 = 1.0/(mesh.dx*mesh.dx);
  const dfloat _1bydy2 = 1.0/(mesh.dy*mesh.dy);

  while ((iterations < JACOBI_MAX_LOOPS) && (residual > JACOBI_TOLERANCE))
  {
    // 计算拉普拉斯算子
    Laplacian(mesh, _1bydx2, _1bydy2, h_U, h_AU);

    // 应用边界条件
    BoundaryConditions(mesh, _1bydx2, _1bydy2, h_U, h_AU);

    // 更新解
    Update(mesh, factor, h_RHS, h_AU, h_RES, h_U);

    // 计算残差 = ||U||
    residual = Norm(mesh, h_RES);

    ++iterations;
  }

Laplacian()函数执行如前所述的主要拉普拉斯算子运算,代码如下:

// AU_i,j = (-U_i+1,j + 2U_i,j - U_i-1,j)/dx^2 +
//          (-U_i,j+1 + 2U_i,j - U_i,j-1)/dy^2
void Laplacian(mesh_t& mesh,
               const dfloat _1bydx2,
               const dfloat _1bydx2,
               const dfloat * U,
               dfloat * AU) {
  int stride = mesh.Nx;
  int localNx = mesh.Nx - 2;
  int localNy = mesh.Ny - 2;

  for (int j=0;j<localNy;j++) {
    for (int i=0;i<localNx;i++) {

      const int id = (i+1) + (j+1)*stride;

      const int id_l = id - 1;
      const int id_r = id + 1;
      const int id_d = id - stride;
      const int id_u = id + stride;

      AU[id] = (-U[id_l] + 2*U[id] - U[id_r])/(dx*dx) +
               (-U[id_d] + 2*U[id] - U[id_u])/(dy*dy);
    }
  }
}

BoundaryConditions()函数用于将边界条件应用于Au项,代码如下:

void BoundaryConditions(mesh_t& mesh,
                        const dfloat _1bydx2,
                        const dfloat _1bydy2,
                        dfloat* U,
                        dfloat* AU) {

  const int Nx = mesh.Nx;
  const int Ny = mesh.Ny;

  for (int id=0;id<2*Nx+2*Ny-2;id++) {

    //根据id的大小获取节点的(i,j)坐标
    int i, j;
    if (id < Nx) { //底部
      i = id;
      j = 0;
    } else if (id<2*Nx) { //顶部
      i = id - Nx;
      j = Ny-1;
    } else if (id < 2*Nx + Ny-1) { //左侧
      i = 0;
      j = id - 2*Nx + 1;
    } else { //右侧
      i = Nx-1;
      j = id - 2*Nx - Ny + 2;
    }

    const int iid = i+j*Nx;

    const dfloat U_d = (j==0)    ?  0.0 : U[iid - Nx];
    const dfloat U_u = (j==Ny-1) ?  0.0 : U[iid + Nx];
    const dfloat U_l = (i==0)    ?  0.0 : U[iid - 1];
    const dfloat U_r = (i==Nx-1) ?  0.0 : U[iid + 1];

     AU[iid] = (-U_l + 2*U[iid] - U_r)*_1bydx2 +
               (-U_d + 2*U[iid] - U_u)*_1bydy2;
  }
}

雅可比求解器的核心在于`Update()`函数,该函数执行雅可比迭代:

// U = U + D^{-1}*(RHS - AU)

void Update(mesh_t& mesh,
            const dfloat factor,
            dfloat* RHS,
            dfloat* AU,
            dfloat* RES,
            dfloat* U)
{
  const int N = mesh.N;

  for (int id=0;id<N;id++)
  {
    const dfloat r_res = RHS[id] - AU[id];
    RES[id] = r_res;
    U[id] += r_res*factor;
  }
}

Norm()函数是执行归约运算并返回用于检查求解器收敛性的标量残差的主函数:

dfloat Norm(mesh_t& mesh, dfloat *U) {

  dfloat norm = 0;
  const int N = mesh.N;
  const dfloat dx = mesh.dx;
  const dfloat dy = mesh.dy;

  for (int id=0; id < N; id++) {
    *norm2 += U[id] * U[id] * dx * dy;
  }
  return sqrt(norm)/N;
}

接下来的两个部分将描述将此求解器移植到GPU上的两种可能方法。

使用 HIP 的 GPU 卸载

首先,我们考虑使用 HIP 进行端口移植,将 Jacobi 求解器卸载到 GPU 上。我们编写了 HIP 内核来移植上述四个主要代码区域。请注意,为了获得最优化的性能,我们为所有内核使用了 256 (BLOCK_SIZE) 的启动边界。以下代码片段显示了每次 Jacobi 迭代中的函数调用,其顺序与串行代码相同:

  // Jacobi 方法中使用的标量因子
  const dfloat factor = 1/(2.0/(mesh.dx*mesh.dx) + 2.0/(mesh.dy*mesh.dy));
  const dfloat _1bydx2 = 1.0/(mesh.dx*mesh.dx);
  const dfloat _1bydy2 = 1.0/(mesh.dy*mesh.dy);

  auto timerStart = std::chrono::high_resolution_clock::now();

  while ((iterations < JACOBI_MAX_LOOPS) && (residual > JACOBI_TOLERANCE))
  {
    // 计算拉普拉斯算子
    Laplacian(mesh, _1bydx2, _1bydy2, HD_U, HD_AU);

    // 应用边界条件
    BoundaryConditions(mesh, _1bydx2, _1bydy2, HD_U, HD_AU);

    //更新解
    Update(mesh, factor, HD_RHS, HD_AU, HD_RES, HD_U);

    // 计算残差 = ||U||
    residual = Norm(mesh, HD_RES);

    ++iterations;
  }

一些内核启动配置在 InitializeData() 中设置,如以下代码片段所示:

  // ...
  // 内核启动配置
  mesh.block.x = BLOCK_SIZE;
  mesh.grid.x = std::ceil(static_cast<double>(mesh.Nx * mesh.Ny) / mesh.block.x);
  mesh.grid2.x = std::ceil((2.0*mesh.Nx+2.0*mesh.Ny-2.0)/mesh.block.x);
  // ...

LaplacianKernel() 和启动该内核的 Laplacian() 例程的代码如下:

__global__
__launch_bounds__(BLOCK_SIZE)
void LaplacianKernel(const int localNx,
                     const int localNy,
                     const int stride,
                     const dfloat fac_dx2,
                     const dfloat fac_dy2,
                     const dfloat * U,
                     dfloat * AU) 
{
    int tid = GET_GLOBAL_ID_0;

    if (tid > localNx + localNy * stride || tid < stride + 1)
        return;

    const int tid_l = tid - 1;
    const int tid_r = tid + 1;
    const int tid_d = tid - stride;
    const int tid_u = tid + stride;

    __builtin_nontemporal_store((-U[tid_l] + 2*U[tid] - U[tid_r])*fac_dx2 +
                                (-U[tid_d] + 2*U[tid] - U[tid_u])*fac_dy2, &(AU[tid]));
}

void Laplacian(mesh_t& mesh,
               const dfloat _1bydx2,
               const dfloat _1bydy2,
               dfloat* U,
               dfloat* AU)
{
  int stride = mesh.Nx;
  int localNx = mesh.Nx-2;
  int localNy = mesh.Ny-2;

  LaplacianKernel<<<mesh.grid,mesh.block>>>(localNx, localNy, stride, _1bydx2, _1bydy2, U, AU);
}

内核 BoundaryConditionsKernel() 和其启动函数如下所示:

__global__
__launch_bounds__(BLOCK_SIZE)
void BoundaryConditionsKernel(const int Nx,
                              const int Ny,
                              const int stride,
                              const dfloat fac_dx2,
                              const dfloat fac_dy2,
                              const dfloat * U,
                              dfloat * AU)
{
    const int id = GET_GLOBAL_ID_0;

    if (id < 2*Nx+2*Ny-2)
    {
    //根据 id 的大小获取节点的 (i,j) 坐标
    int i = Nx-1;
    int j = id - 2*Nx - Ny + 2;

    if (id < Nx)
    { //底部
        i = id; j = 0;
    }
    else if (id<2*Nx)
    { //顶部
        i = id - Nx; j = Ny-1;
    }
    else if (id < 2*Nx + Ny-1)
    { //左侧
        i = 0; j = id - 2*Nx + 1;
    }

    const int iid = i+j*stride;

    const dfloat U_d = (j==0)    ?  0.0 : U[iid - stride];
    const dfloat U_u = (j==Ny-1) ?  0.0 : U[iid + stride];
    const dfloat U_l = (i==0)    ?  0.0 : U[iid - 1];
    const dfloat U_r = (i==Nx-1) ?  0.0 : U[iid + 1];

    __builtin_nontemporal_store((-U_l + 2*U[iid] - U_r)*fac_dx2 +
                                (-U_d + 2*U[iid] - U_u)*fac_dy2, &(AU[iid]));
    }
}

void BoundaryConditions(mesh_t& mesh,
                        const dfloat _1bydx2,
                        const dfloat _1bydy2,
                        dfloat* U,
                        dfloat* AU) {

  const int Nx = mesh.Nx;
  const int Ny = mesh.Ny;
  BoundaryConditionsKernel<<<mesh.grid2,mesh.block>>>(Nx, Ny, Nx, _1bydx2, _1bydy2, U, AU);
}

以下代码展示了 UpdateKernel() 的 HIP 实现:

__global__
__launch_bounds__(BLOCK_SIZE)
void UpdateKernel(const int N,
                  const dfloat factor,
                  const dfloat *__restrict__ RHS,
                  const dfloat *__restrict__ AU,
                  dfloat *__restrict__ RES,
                  dfloat *__restrict__ U)
{
    int tid = GET_GLOBAL_ID_0;
    dfloat r_res;
    for (int i = tid; i < N; i += gridDim.x * blockDim.x)
    {
        r_res = RHS[i] - AU[i];
        RES[i] = r_res;
        U[i] += r_res*factor;
    }
}

void Update(mesh_t& mesh,
            const dfloat factor,
            dfloat* RHS,
            dfloat* AU,
            dfloat* RES,
            dfloat* U)
{
  UpdateKernel<<<mesh.grid,mesh.block>>>(mesh.N, factor, RHS, AU, RES, U);
}

以下代码展示了 NormKernel() 的 HIP 实现,该实现基于 BabelStream 中的 HIP dot 实现:

#define NORM_BLOCK_SIZE 512
#define NORM_NUM_BLOCKS 256
__global__
__launch_bounds__(NORM_BLOCK_SIZE)
void NormKernel(const dfloat * a, dfloat * sum, int N)
{
  __shared__ dfloat smem[NORM_BLOCK_SIZE];

  int i = GET_GLOBAL_ID_0;
  const size_t si = GET_LOCAL_ID_0;

  smem[si] = 0.0;
  for (; i < N; i += NORM_BLOCK_SIZE * NORM_NUM_BLOCKS)
    smem[si] += a[i] * a[i];

  for (int offset = NORM_BLOCK_SIZE >> 1; offset > 0; offset >>= 1)
  {
    __syncthreads();
    if (si < offset) smem[si] += smem[si+offset];
  }

  if (si == 0)
    sum[GET_BLOCK_ID_0] = smem[si];
}

void NormSumMalloc(mesh_t &mesh)
{
  hipHostMalloc(&mesh.norm_sum, NORM_NUM_BLOCKS*sizeof(dfloat), hipHostMallocNonCoherent);
}

dfloat Norm(mesh_t& mesh, dfloat *U)
{
  dfloat norm = 0.0;
  const int N = mesh.N;
  const dfloat dx = mesh.dx;
  const dfloat dy = mesh.dy;
  NormKernel<<<NORM_NUM_BLOCKS,NORM_BLOCK_SIZE>>>(U, mesh.norm_sum, N);
  hipDeviceSynchronize();
  for (int id=0; id < NORM_NUM_BLOCKS; id++)
    norm += mesh.norm_sum[id];
  return sqrt(norm*dx*dy)*mesh.invNtotal;
}

请注意,缓冲区 mesh.norm_sum 是非一致性的固定内存,这意味着它被映射到 GPU 的地址空间中。有关这种内存的更详细讨论,请参阅这篇文章。参数 NORM_BLOCK_SIZE 和 NORM_NUM_BLOCKS 可以且应该根据您的硬件进行优化。

下表总结了 Jacobi 求解器中的主要内核在 HIP 端口化后的每次迭代耗时。网格大小为 4096×4096。在 1000 次迭代中,整个过程的总壁钟时间为 858 毫秒,单个 MI250 GPU 核心上实现了 332 GFLOP/s 的计算性能。最耗时的代码区域是 Jacobi 的 Update() 例程。

内核

平均时间 (ms)

百分比

Update

0.51

60.9

Laplacian

0.22

25.7

Norm

0.10

12.3

BoundaryConditions

0.01

0.9

使用OpenMP进行GPU卸载

在本节中,我们探讨了OpenMP卸载。我们考虑了结构化和非结构化的目标数据映射方法。使用    -fopenmp标志的clang++编译器用于构建openmp目标卸载区域。更多详情可以在codes/Makefile中找到。例如,以下命令显示了如何编译Jacobi.cpp文件:

/opt/rocm/llvm/bin/clang++ -Ofast -g -fopenmp --offload-arch=gfx90a -c Jacobi.cpp -o Jacobi.o

结构化目标数据映射

让我们考虑在HIP移植的Jacobi求解器中观察到的最昂贵的代码区域,Update()。一种简单的OpenMP目标卸载方法如下:

void Update(mesh_t& mesh,
            const dfloat factor,
            dfloat* RHS,
            dfloat* AU,
            dfloat* RES,
            dfloat* U)
{
  const int N = mesh.N;
  #pragma omp target data map(to:RHS[0:N],AU[0:N]) map(from:RES[0:N]) map(tofrom:U[0:N])
  #pragma omp target teams distribute parallel for
  for (int id=0;id<N;id++)
  {
    const dfloat r_res = RHS[id] - AU[id];
    RES[id] = r_res;
    U[id] += r_res*factor;
  }
}

状态变量被映射到设备区域,并在目标区域中调用Jacobi更新函数。这是一个结构化目标映射构造的示例。对于Laplacian和Norm函数,也进行了类似的结构化目标映射。

void Laplacian(mesh_t& mesh,
               const dfloat _1bydx2,
               const dfloat _1bydy2,
               dfloat* U,
               dfloat* AU)
{
  int stride = mesh.Nx;
  int localNx = mesh.Nx-2;
  int localNy = mesh.Ny-2;

  #pragma omp target data map(to:U[0:mesh.N]) map(tofrom:AU[0:mesh.N])
  #pragma omp target teams distribute parallel for collapse(2)
  for (int j=0;j<localNy;j++) {
    for (int i=0;i<localNx;i++) {

      const int id = (i+1) + (j+1)*stride;

      const int id_l = id - 1;
      const int id_r = id + 1;
      const int id_d = id - stride;
      const int id_u = id + stride;

       AU[id] = (-U[id_l] + 2*U[id] - U[id_r])*_1bydx2 +
                (-U[id_d] + 2*U[id] - U[id_u])*_1bydy2;
    }
  }
}
dfloat Norm(mesh_t& mesh, dfloat *U)
{
  dfloat norm = 0.0;
  const int N = mesh.N;
  const dfloat dx = mesh.dx;
  const dfloat dy = mesh.dy;
  #pragma omp target data map(to: U[0:mesh.N])
  #pragma omp target teams distribute parallel for reduction(+:norm)
  for (int id=0; id < N; id++) {
    norm += U[id] * U[id] * dx * dy;
  }
  return sqrt(norm)/N;
}

然而,这种实现导致求解器的性能非常差,每次主循环迭代需要51.4毫秒,而使用HIP则只需要1.1毫秒。这大约慢了超过46倍。查看图2,我们发现大量时间花费在`hsa_signal_wait_scacquire`和特别是`async-copy`内核中,这是由于每次迭代所需的映射。

../../_images/roctx_ompt_strMap.PNG

图 2:使用结构化目标数据映射的Jacobi卸载内核的时间轴

非结构化 target 数据映射

我们注意到,Jacobi 求解器所需的大多数状态变量可以驻留在设备上。仅在输出最终的残差和需要状态解的数据时,才需要将数据映射到主机(CPU)。因此,我们可以切换到_非结构化_目标数据映射构造,它确保数据在 target enter data 和 target exit data 映射构造之间驻留在设备上。在 Jacobi 迭代之前执行主机到设备的拷贝,并在循环结束之后释放设备数据,如下所示:

void Jacobi_t::Run()
{
  const int N = mesh.N;
  #pragma omp target enter data map(to: mesh,h_U[0:N],h_AU[0:N],h_RES[0:N],h_RHS[0:N])

  ...
  while ((iterations < JACOBI_MAX_LOOPS) && (residual > JACOBI_TOLERANCE)) 
  {
    Laplacian(mesh, _1bydx2, _1bydy2, HD_U, HD_AU);
    BoundaryConditions(mesh, _1bydx2, _1bydy2, HD_U, HD_AU);
    Update(mesh, factor, HD_RHS, HD_AU, HD_RES, HD_U);
    residual = Norm(mesh, HD_RES);

    ++iterations;
  }
  ...

  #pragma omp target exit data map(release: h_U[0:N],h_AU[0:N],h_RES[0:N],h_RHS[0:N])
}

这避免了在每次迭代中需要对主机到设备和设备到主机的数据进行重复映射,这些在_结构化_target数据映射构造在所有例程中都需要:Laplacian、BoundaryConditions、Update 和 Norm。这意味着在这些例程中移除了 #pragma omp target data map() 构造。

下面代码示例了现在更新的 Norm 函数在目标区域不再有结构化映射构造。这里,状态向量 U 在 Jacobi 求解器开始时已经使用非结构化数据映射构造映射到设备。

dfloat Norm(mesh_t& mesh, dfloat *U)
{
  dfloat norm = 0.0;
  const int N = mesh.N;
  const dfloat dx = mesh.dx;
  const dfloat dy = mesh.dy;
  #pragma omp target teams distribute parallel for reduction(+:norm)
  for (int id=0; id < N; id++) {
    norm += U[id] * U[id] * dx * dy;
  }
  return sqrt(norm)/N;
}

下图3展示了主要的 Jacobi 求解器内核的时间轴。图中清楚地显示,与图 2 中的第一次简单卸载实现相比,总的 hsa_signal_wait_scacquire 内核调用次数显著减少。

图 3:使用非结构化映射优化 Jacobi 卸载内核的时间轴

下表总结了使用 Clang 编译器进行 OpenMP 卸载实现的 Jacobi 求解器主要内核的时间。网格尺寸为4096×4096,与 HIP 版本一致。在一千次迭代中的总墙时钟时间为999毫秒,与早期在 MI250 GPU 上观察到的 HIP 实现值非常接近。285 GFLOPs 的计算性能与 HIP 实现中观察到的值相当。总体而言,使用 OpenMP 卸载的主要代码区域计算时间与 HIP 端口化所得的数值非常相近。

内核

平均时间 (ms)

百分比

Update

0.51

57.9

Laplacian

0.25

27.6

Norm

0.11

12.2

BoundaryConditions

0.02

2.2

HIP 与 OpenMP Offload

由于稀疏矩阵操作的雅可比求解器(其算术强度 AI = FLOPs/带宽约为 0.17)处于内存受限状态,因此我们对比了 HIP 和 OpenMP offload 实现中内核实现的内存带宽(BW)。下表对比了 HIP 和 OpenMP offload 实现的 HBM 带宽(GB/s)值。

内核

HIP HBM  带宽(GB/s)

OpenMP Offload HBM 带宽 (GB/s)

更新Update

1306

1297

拉普拉斯Laplacian

1240

1091

范数Norm

1325

1239

边界条件BoundaryConditions

151

50

大多数内核的带宽值非常接近 MI250 的可实现峰值带宽(约 1300-1400 GB/s)。边界条件的工作量太小,无法使 GPU 饱和,因此值较小。下图(图4)展示了使用 HIP 和 OpenMP offload 实现的更新内核的屋顶线(Roofline [3] )。从屋顶线图中可以看出,更新内核在两种实现中都非常接近内存受限状态下的屋顶线。这与上表中的 HBM 数据一致。

../../_images/roofline_Update_ompt.png

图4: 使用 HIP(左)和 OpenMP offload(右)的更新内核屋顶线

类似的观察结果也适用于拉普拉斯内核中,从下图(图5)展示的两种 GPU offload 实现的屋顶线可以看出。

../../_images/roofline_Laplacian_hip.png

../../_images/roofline_Laplacian_ompt.png

图5: 使用 HIP(左)和 OpenMP offload(右)的拉普拉斯内核屋顶线 

结论

在这篇博客文章中,我们展示了使用 HIP 和 OpenMP 目标卸载进行有限差分 Jacobi 求解器的 GPU 卸载。HIP 实现需要将主机代码替换为新的 HIP 内核,并优化内核启动参数,包括线程块和网格大小以及启动界限。而 OpenMP 卸载实现则是基于指令,仅需对现有主机代码库进行较少的修改。对于所考虑的问题规模,OpenMP 卸载可以实现与 HIP GPU 移植相媲美甚至有时更好的计算性能和带宽指标,而编码工作量则大大减少。这说明了像 OpenMP 卸载这样的指令集 GPU 移植相较于需要重写代码的 HIP 移植方法所能提供的优势。值得一提的是,对于更复杂的应用程序,OpenMP 可能在优化目标卸载结构和参数方面提供的选择有限,从而限制了额外的性能提升。

未来,我们将跟进这篇博客文章,发布 Jacobi 求解器的 Fortran OpenMP 卸载版本。

作者要感谢 Brian Cornille 和 Mahdieh Ghazimirsaeed 对文稿的有益评论和反馈。

附带代码示例

如果您有任何问题或评论,请在 GitHub 讨论区 与我们联系。


[1]

请参见 有限差分 博客文章。

[2]

结果是通过 ROCM v.5.6.0-67 获得的。这些数据并非经过验证的性能数据,仅用于展示代码修改带来的相对性能提升。实际性能结果将依赖于多个因素,包括系统配置和环境设置。

[3]

这些屋顶线图是使用 AMD 的 Omniperf 工具获取的。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2218296.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java实现油画滤镜效果【参数可调】

油画滤镜的基本原理 油画滤镜的基本思想是通过改变图像的像素&#xff0c;将每个像素用周围随机选择的像素来代替&#xff0c;从而产生类似油画笔触的效果。这种处理方式可以模糊图像的细节&#xff0c;使得图像的色块更加连贯&#xff0c;从而模仿油画的艺术效果。 核心步骤…

后台管理员登录实现--系统篇

我的小系统后台原来就有一个上传图片的功能还夹带个删除图片的功能&#xff0c;还嵌到了一个菜单里面。之前效果如下 那么现在为了加大安全力度&#xff0c;想增加一个登录页面。通过登录再到这个页面。看着貌似很简单&#xff0c;但是听我细细说来&#xff0c;要新增些什么东西…

OpenLayers:构建现代Web地图应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 OpenLayers&#xff1a;构建现代Web地图应用 文章目录 OpenLayers&#xff1a;构建现代Web地图应用1. 简介2. 为什么选择 OpenLa…

Redis 高可用:从主从到集群的全面解析

目录 一、主从复制 (基础)1. 同步复制a. 全量数据同步b. 增量数据同步c. 可能带来的数据不一致 2. 环形缓冲区a. 动态调整槽位 3. runid4. 主从复制解决单点故障a. 单点故障b. 可用性问题 5. 注意事项a. Replica 主动向 Master 建立连接b. Replica 主动向 Master 拉取数据 二、…

腾讯云宝塔面板前后端项目发版

后端发版 1. 打开“网站”页面&#xff0c;找到java项目&#xff0c;点击状态暂停服务 2.打开“文件”页面&#xff0c;进入jar包目录&#xff0c;删除原有的jar包&#xff0c;上传新jar包 3. 再回到第一步中的网站页面&#xff0c;找到jar项目&#xff0c;启动项目即可 前端发…

C#的小数位保留以及四舍五入

C#使用Math.Round("数值","保留位","保留方式")进行小数位保留以及四舍五入 //1.MidpointRounding.ToEven(四舍六入五成双) //当保留小数位后一位为0~4时&#xff0c;舍去末位 var x1 Math.Round(1.124, 2, MidpointRo…

立仪科技:光谱共焦传感器精准测量玻璃

光谱共焦测量技术作为一种创新的光学检测方法&#xff0c;近年来在工业领域引起了广泛关注。 它以其高精度、非接触式的特点&#xff0c;特别适用于透明或半透明材料如玻璃的厚度和表面形貌测量。 接下来&#xff0c;立仪科技小编将深入探讨光谱共焦技术在玻璃测量上的应用及其…

计算机毕业设计Hadoop+Hive+Spark+Flink广告推荐系统 广告预测 广告数据分析可视化 广告爬虫 大数据毕业设计 深度学习 机器学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 专业 小四号宋体 班级 小…

飞机大战告尾

参考 PPO算法逐行代码详解 链接 通过网盘分享的文件&#xff1a;PlaneWar 链接: https://pan.baidu.com/s/1cbLKTcBxL6Aem3WkyDtPzg?pwd1234 提取码: 1234 10.17关于博客发了又改这件事 悲催的事 今天训练了一早上ppo模型&#xff0c;满怀期待的检测成果时发现一点长进都…

mac安装brew时踩坑解决方案

安装包 mac上如果按照git等工具可能会使用brew&#xff0c;例如使用&#xff1a;$ brew install git命令&#xff0c;如果电脑没有按照brew&#xff0c;则会提示&#xff1a;zsh: command not found: brew 解决方案 需要我们打开brew的官网https://brew.sh/&#xff0c;复制…

动态规划一>下降路径最小和

1.题目&#xff1a; 2.解析&#xff1a; 代码&#xff1a; /**1.创建dp表2.初始化3.填表4.返回值*/public int minFallingPathSum(int[][] matrix) {int n matrix.length;int[][] dp new int[n1][n2];int minNum Integer.MAX_VALUE; for(int i 1; i < n; i) dp[i][0]…

【CSS】纯CSS Loading动画组件

<template><div class"ai-loader-box"><!-- AI loader --><div class"ai-loader"><div class"text"><p>AI智能分析中....</p></div><div class"horizontal"><div class&quo…

简单说说 spring是如何实现AOP的(源码分析)

在spring生命周期流程中&#xff0c;有一个过程是执行BeanPostProcessor的后置方法 BeanPostProcessor 是一个接口&#xff0c;其实现有 aop实现的核心类是AbstractAutoProxyCreator&#xff0c;其位于spring-aop包下&#xff0c;实现了BeanPostProcessor //BeanPostProcesso…

【Java小白图文教程】-04-分支结构

本套课程将会从0基础讲解Java语言核心技术&#xff0c;适合人群&#xff1a; 大学中开设了Java语言课程的同学想要专升本或者考研的同学想要考计算机等级证书的同学想要从事Java相关开发开发的同学 精品专题&#xff1a; 01.《C语言从不挂科到高绩点》课程详细笔记 https:/…

transformers 推理 Qwen2.5 等大模型技术细节详解(一)transformers 初始化和对象加载(文末免费送书)

上周收到一位网友的私信&#xff0c;希望老牛同学写一篇有关使用 transformers 框架推理大模型的技术细节的文章。 老牛同学刚开始以为这类的文章网上应该会有很多&#xff0c;于是想着百度几篇质量稍高一点的回复这位网友。结果&#xff0c;老牛同学搜索后发现&#xff0c;类…

力扣61~65题

题61&#xff08;中等&#xff09;&#xff1a; 分析&#xff1a; python代码&#xff1a; # Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nextNone): # self.val val # self.next next class Solution:def rot…

【含开题报告+文档+PPT+源码】基于SpringBoot电脑DIY装机教程网站的设计与实现

开题报告 随着科技的发展和人们对电脑需求的增加&#xff0c;越来越多的人开始自己组装电脑。然而&#xff0c;针对初学者来说&#xff0c;如何选择合适的硬件配置并进行装机是一个相对复杂的过程。随着各种品牌、型号和规格的硬件不断增多&#xff0c;用户需要一个方便快捷的…

Java项目编译不通过,IDEA无法运行或调试Unit test类

mvn test可以通过&#xff0c;但是通过IDEA无法运行或调试&#xff0c;总是弹出一些依赖错误比如&#xff1a; 程序包xxx.xxx.xxx 不存在或找不到符号 解决办法 步骤1&#xff1a;IDEA 打开 File -> Setting ->Compiler &#xff0c;找到“Automatically show first …

20 Shell Script输入与输出

标出输入、标准输出、错误输出 一、程序的基本三个IO流 一&#xff09;文件描述符 ​ 任何程序在Linux系统中都有3个基本的文件描述符 ​ 比如: ​ cd/proc/$$/fd ​ 进入当前shell程序对于内核在文件系统的映射目录中: [rootlocalhost ~]# cd /proc/$$/fd [rootlocalhos…

基于System.js的微前端实现(插件化)

目录​​​​​​​ 写在前面 一、微前端相关知识 &#xff08;一&#xff09;概念 &#xff08;二&#xff09; 优势 &#xff08;三&#xff09; 缺点 &#xff08;四&#xff09;应用场景 &#xff08;五&#xff09;现有框架 1. qiankun 2. single-spa 3. SystemJ…