5.4. Open MPI Java绑定

Open MPI v5.0.x 为基于Java的MPI应用程序提供支持。

警告

Open MPI的Java绑定是以"临时"方式提供的——也就是说,它们不属于当前或提议的MPI标准。因此,标准并不要求必须包含Java支持。Java绑定的持续保留取决于用户的积极需求和开发者的持续支持。

本文档的其余部分提供了逐步指导,介绍如何构建带有Java绑定的OMPI,以及如何编译和运行基于Java的MPI应用程序。同时,通过示例解释了部分功能。关于Open MPI中Java绑定的设计、实现和使用的更多详细信息,可以在其标准参考论文[1]中找到。这些绑定采用JNI方法,也就是说,我们并不提供MPI原语的纯Java实现,而是在C实现之上提供一个薄层。这与mpiJava[2]采用的方法相同;事实上,Open MPI的Java绑定最初是以mpiJava为起点开发的,但后来进行了彻底的重写。

5.4.1. 构建Java绑定

Java支持要求Open MPI至少构建为共享库(即--enable-shared)。请注意,这是Open MPI的默认设置,因此您无需显式添加该选项。仅当指定了--enable-mpi-java并且在系统默认位置找到JDK时,才会构建Java绑定。

如果JDK不在我们自动查找的位置,您可以指定其路径。例如,在Mac平台上就需要这样做,因为JDK头文件位于非典型位置。为此提供了两个选项:

  1. --with-jdk-bindir=: javacjavah 的位置

  2. --with-jdk-headers=: 包含jni.h文件的目录

在Open MPI配置平台文件中提供了一些示例配置,位于contrib/platform/hadoop目录下。这些示例可以作为您自定义配置的起点。

综上所述,您可以通过以下与Java相关的选项来配置系统:

$ ./configure --with-platform=contrib/platform/hadoop/<your-platform> ...

或:

$ ./configure --enable-mpi-java --with-jdk-bindir=<foo> --with-jdk-headers=<bar> ...

或者简而言之:

$ ./configure --enable-mpi-java ...

如果JDK位于configure能够自动检测到的"标准"位置。

5.4.2. 构建Java MPI应用程序

mpijavac包装编译器可用于编译基于Java的MPI应用程序。它能确保所有必需的Open MPI库和类路径均已正确定义。例如:

$ mpijavac Hello.java

您可以使用--showme选项查看所调用的Java编译器的完整命令行:

$ mpijavac Hello.java --showme
/usr/bin/javac -cp /opt/openmpi/lib/mpi.jar Hello.java

请注意,如果您在命令行中指定-cp参数来传递应用程序特定的类路径,Open MPI将扩展该参数以包含mpi.jar

$ mpijavac -cp /path/to/my/app.jar Hello.java --showme
/usr/bin/javac -cp /path/to/my/app.jar:/opt/openmpi/lib/mpi.jar Hello.java

类似地,如果您定义了CLASSPATH环境变量, mpijavac会将其转换为-cp参数并扩展它 以包含mpi.jar文件:

$ export CLASSPATH=/path/to/my/app.jar
$ mpijavac Hello.java --showme
/usr/bin/javac -cp /path/to/my/app.jar:/opt/openmpi/lib/mpi.jar Hello.java

5.4.3. 运行Java MPI应用程序

当您的应用程序编译完成后,可以使用标准的mpirun命令行来运行它:

$ mpirun <options> java <your-java-options> <my-app>

mpirun 会自动识别 java 标识,并确保已定义所需的 MPI 库和类路径以支持执行。因此您无需手动指定 MPI 安装的 Java 库路径或 MPI 类路径。应用程序所需的 任何类路径定义应通过命令行或 CLASSPATH 环境变量 指定。请注意,如果未指定任何路径, 当前工作目录将被自动添加到类路径中。

注意

java可执行文件、所有必需的库以及您的应用程序类必须在所有节点上可用。

5.4.4. Java绑定的基本用法

有一个MPI软件包包含了MPI Java绑定的所有类:CommDatatypeRequest等。这些类与MPI标准定义的句柄类型直接对应。MPI原语就是这些类中包含的方法。Java方法和类的命名遵循常见的驼峰命名约定,例如MPI_File_set_info(fh,info)对应的Java方法是fh.setInfo(info),其中fhFile类的对象。

除了类之外,MPI包还在一个便捷类MPI下预定义了公共属性。例如预定义的通信器MPI.COMM_WORLD和预定义的数据类型如MPI.DOUBLE。此外,MPI初始化和终止都是MPI类的方法,必须由所有MPI Java应用程序调用。以下示例说明了这些概念:

import mpi.*;

class ComputePi {

   public static void main(String args[]) throws MPIException {

       MPI.Init(args);

       int rank = MPI.COMM_WORLD.getRank(),
           size = MPI.COMM_WORLD.getSize(),
           nint = 100; // Intervals.
       double h = 1.0/(double)nint, sum = 0.0;

       for (int i=rank+1; i<=nint; i+=size) {
           double x = h * ((double)i - 0.5);
           sum += (4.0 / (1.0 + x * x));
       }

       double sBuf[] = { h * sum },
              rBuf[] = new double[1];

       MPI.COMM_WORLD.reduce(sBuf, rBuf, 1, MPI.DOUBLE, MPI.SUM, 0);

       if (rank == 0) {
           System.out.println("PI: " + rBuf[0]);
       }
       MPI.Finalize();
   }
}

5.4.5. 异常处理

Open MPI中的Java绑定支持异常处理。默认情况下,错误是致命的,但可以更改此行为。如果设置了MPI.ERRORS_RETURN错误处理程序,Java API将抛出异常:

MPI.COMM_WORLD.setErrhandler(MPI.ERRORS_RETURN);

如果在程序中添加此语句,当发生错误时,它将显示中断行号,而不仅仅是崩溃。例如,通过try-catch块可以将错误处理代码与主应用程序代码分离:

try
{
    File file = new File(MPI.COMM_SELF, "filename", MPI.MODE_RDONLY);
}
catch(MPIException ex)
{
    System.err.println("Error Message: "+ ex.getMessage());
    System.err.println("  Error Class: "+ ex.getErrorClass());
    ex.printStackTrace();
    System.exit(-1);
}

5.4.6. 如何指定缓冲区

在需要缓冲区(无论是发送还是接收)的MPI原语中,Java API允许使用Java数组。由于Java数组可能被Java运行时环境重新定位,MPI Java绑定需要将数组内容复制到临时缓冲区,然后将指向该缓冲区的指针传递给底层的C实现。从实际角度来看,这意味着所有由Java数组表示的缓冲区都会带来额外开销。对于小缓冲区来说开销很小,但随着数组增大而增加。

有一个默认容量为64K的临时缓冲区池。如果需要64K或更小的临时缓冲区,则会从池中获取该缓冲区。但如果缓冲区更大,则需要分配该缓冲区并在之后释放。

可以通过Open MPI MCA参数修改池缓冲区的默认容量:

$ mpirun --mca ompi_mpi_java_eager SIZE ...

SIZE 的值可以是:

  • N: 字节数的整数值

  • Nk: 以千字节为单位的整数值(后缀为k

  • Nm: 一个整数(以m为后缀)表示兆字节数

另一种方法是使用Java SDK中提供的标准类如ByteBuffer所支持的"直接缓冲区"。为了方便起见,Open MPI在MPI类中提供了一些静态方法new[Type]Buffer来为多种基本数据类型创建直接缓冲区。可以通过put()get()方法访问直接缓冲区中的元素,并使用capacity()方法获取缓冲区中的元素数量。以下示例展示了其用法:

int myself = MPI.COMM_WORLD.getRank();
int tasks  = MPI.COMM_WORLD.getSize();

IntBuffer in  = MPI.newIntBuffer(MAXLEN * tasks),
          out = MPI.newIntBuffer(MAXLEN);

for (int i = 0; i < MAXLEN; i++)
    out.put(i, myself);      // fill the buffer with the rank

Request request = MPI.COMM_WORLD.iAllGather(
                  out, MAXLEN, MPI.INT, in, MAXLEN, MPI.INT);
request.waitFor();
request.free();

for (int i = 0; i < tasks; i++) {
    for (int k = 0; k < MAXLEN; k++) {
        if (in.get(k + i * MAXLEN) != i)
            throw new AssertionError("Unexpected value");
    }
}

直接缓冲区可用于:BYTECHARSHORTINTLONGFLOATDOUBLE

注意

没有直接针对布尔值的缓冲区。

直接缓冲区并不能完全替代数组,因为它们的分配和释放成本比数组更高。在某些情况下,数组会是更好的选择。您可以轻松地将缓冲区转换为数组,反之亦然。

重要

所有非阻塞方法必须使用直接缓冲区。只有阻塞方法可以在数组和直接缓冲区之间进行选择。

上述示例还说明,对于实现了Freeable接口的类对象,必须调用free()方法。否则将会发生内存泄漏。

5.4.7. 在缓冲区中指定偏移量

在C程序中,通常使用&array[i]array+i来指定数组中的偏移量,以便从数组的指定位置开始发送数据。在Java绑定中的等效形式是对缓冲区进行slice()操作以从偏移量开始。只有当偏移量不为零时,才需要对缓冲区进行slice()操作。切片操作既适用于数组也适用于直接缓冲区。

import static mpi.MPI.slice;
// ...
int numbers[] = new int[SIZE];
// ...
MPI.COMM_WORLD.send(slice(numbers, offset), count, MPI.INT, 1, 0);

5.4.8. 支持的API

Open MPI Java绑定提供了完整的MPI-3.1功能支持,但存在少数例外情况:

  • MPI_Neighbor_alltoallwMPI_Ineighbor_alltoallw 函数的绑定尚未实现。

  • 同样排除的还包括涉及显式虚拟内存寻址概念的函数,例如MPI_Win_shared_query

5.4.9. 已知问题

Omnipath (PSM2)互连在与Java配合使用时存在问题。这些问题在PSM2 v10.2版本中确实存在;我们尚未测试过之前的版本。

截至2016年11月,尚未发布能完全解决该问题的PSM2版本。

以下 mpirun 命令选项将禁用 PSM2:

shell$ mpirun ... --mca mtl ^psm2 java ...your-java-options... your-app-class

5.4.10. 有问题?遇到困难?

Java API文档在构建时生成于$prefix/share/doc/openmpi/javadoc目录下。

此外,这篇思科博客文章包含了大量关于Open MPI Java绑定的信息。

如果您遇到任何问题或发现任何错误,请随时向Open MPI用户邮件列表报告。

脚注