在 mpi4py 中包装和调用 C MPI 程序

上一篇中我们介绍了使用 scalapy 调用 ScaLAPACK 进行分布式内存的线性代数运算。Python 作为一种胶水语言,可以非常容易地包装和调用其它计算机语言已有的程序代码和工具库,如果我们有用 C,C++,Fortran 或其它计算机语言编写的 MPI 计算程序,也能很容易地将其包装后在 mpi4py 中进行调用。另外我们也可以用这些计算机语言编写一些运算速度更快的 MPI 扩展模块供 mpi4py 程序调用。在下面的章节中我们将介绍怎么在 mpi4py 程序中包装和调用 C,C++,Fortran 的 MPI 程序。下面我们首先介绍直接使用 Python/C API 进行 C 语言 MPI 程序包装的方法。

Python 文档中有使用 C/C++ 写扩展模块的详细文档和完整的 Python/C API 参考,此处不会专门讲解怎么使用 Python/C API 编写扩展模块,只会介绍如何在 mpi4py 程序中包装和调用已经存在的 C 语言 MPI 程序代码。

假设我们有如下使用 Python/C API 编写的扩展 C 语言 MPI 程序,其中定义了 sayhello 函数,这个函数接受一个 MPI 通信子作为参数,在其中调用 MPI 的相关函数完成计算工作:

/* helloworld.c */

#define MPICH_SKIP_MPICXX 1
#define OMPI_SKIP_MPICXX  1
#include <mpi4py/mpi4py.h>

/* -------------------------------------------------------------------------- */

static void
sayhello(MPI_Comm comm) {
  int size, rank;
  char pname[MPI_MAX_PROCESSOR_NAME]; int len;
  if (comm == MPI_COMM_NULL) {
    printf("You passed MPI_COMM_NULL !!!\n");
    return;
  }
  MPI_Comm_size(comm, &size);
  MPI_Comm_rank(comm, &rank);
  MPI_Get_processor_name(pname, &len);
  pname[len] = 0;
  printf("Hello, World! I am process %d of %d on %s.\n",
         rank, size, pname);
}

/* -------------------------------------------------------------------------- */

static PyObject *
hw_sayhello(PyObject *self, PyObject *args)
{
  PyObject *py_comm = NULL;
  MPI_Comm *comm_p  = NULL;

  if (!PyArg_ParseTuple(args, "O:sayhello", &py_comm))
    return NULL;

  comm_p = PyMPIComm_Get(py_comm);
  if (comm_p == NULL)
    return NULL;

  sayhello(*comm_p);

  Py_INCREF(Py_None);
  return Py_None;
}

static struct PyMethodDef hw_methods[] = {
  {"sayhello", (PyCFunction)hw_sayhello, METH_VARARGS, NULL},
  {NULL,       NULL,                     0,            NULL} /* sentinel */
};

#if PY_MAJOR_VERSION < 3
/* --- Python 2 --- */

PyMODINIT_FUNC inithelloworld(void)
{
  PyObject *m = NULL;

  /* Initialize mpi4py C-API */
  if (import_mpi4py() < 0) goto bad;

  /* Module initialization  */
  m = Py_InitModule("helloworld", hw_methods);
  if (m == NULL) goto bad;

  return;

 bad:
  return;
}

#else
/* --- Python 3 --- */

static struct PyModuleDef hw_module = {
  PyModuleDef_HEAD_INIT,
  "helloworld", /* m_name */
  NULL,         /* m_doc */
  -1,           /* m_size */
  hw_methods    /* m_methods */,
  NULL,         /* m_reload */
  NULL,         /* m_traverse */
  NULL,         /* m_clear */
  NULL          /* m_free */
};

PyMODINIT_FUNC
PyInit_helloworld(void)
{
  PyObject *m = NULL;

  /* Initialize mpi4py's C-API */
  if (import_mpi4py() < 0) goto bad;

  /* Module initialization  */
  m = PyModule_Create(&hw_module);
  if (m == NULL) goto bad;

  return m;

 bad:
  return NULL;
}

#endif

/* -------------------------------------------------------------------------- */

/*
  Local variables:
  c-basic-offset: 2
  indent-tabs-mode: nil
  End:
*/

我们希望在 mpi4py 程序中调用以上定义的 C 语言函数 sayhello:

# test.py

from mpi4py import MPI
from mpi4py import MPI
import helloworld as hw

null = MPI.COMM_NULL
hw.sayhello(null)

comm = MPI.COMM_WORLD
hw.sayhello(comm)

try:
    hw.sayhello(list())
except:
    pass
else:
    assert 0, "exception not raised"

为此我们需要将以上的 C 语言程序编译成一个扩展模块 helloworld.so (此处以 linux 为例,其它系统会有不同的后缀)。因为是 MPI 程序,我们需要使用一个支持 MPI 的 C 编译器,如 mpicc,例外还需要指定编译过程中需要用到的头文件的路径和需要链接的库文件,此处用到了 Python/C API 和 mpi4py.h 头文件,因此需要指定 Python.h 头文件所在的路径和 mpi4py.h 头文件所在的路径,链接 python2.7 等库文件,例外需加上指令 -fPIC -shared 以指导其编译成一个动态链接库。编译使用类似下面的命令(注意将其中的头文件路径改成你的系统中实际的路径):

$ mpicc -I/path/to/python/include/python2.7 -I/path/to/python/lib/python2.7/site-packages/mpi4py/include -o helloworld.so helloworld.c -fPIC -shared -lpthread -ldl -lutil -lm -lpython2.7

如果不知道自己系统中的 Python.h 头文件路径和 mpi4py.h 头文件路径,可以用下面的命令获得:

$ python -c "import sysconfig; print( sysconfig.get_path('include') )"
$ python -c "import mpi4py; print( mpi4py.get_include() )"

编译成功后会生成扩展模块 helloworld.so,然后就可以在我们的 mpi4py 程序中像使用其它 Python 模块一样导入该模块并调用该模块中定义的 sayhello 函数,可以向此函数传递一个 mpi4py 中定义的通信子,如 MPI.COMM_WORLD 或者其它通信子对象。

执行以上 test.py 的结果如下:

$ mpiexec -n 4 python test.py
You passed MPI_COMM_NULL !!!
Hello, World! I am process 2 of 4 on node4.
You passed MPI_COMM_NULL !!!
Hello, World! I am process 3 of 4 on node4.
You passed MPI_COMM_NULL !!!
Hello, World! I am process 0 of 4 on node4.
You passed MPI_COMM_NULL !!!
Hello, World! I am process 1 of 4 on node4.

以上设置头文件路径和指定编译库的编译方法比较麻烦且容易出错,也不适用于比较大型的程序项目,一种更好的方法是写一个 Makefile,将编译的命令及编译的依赖关系写在此 Makefile 中,使用 GNU make 工具来完成项目的编译工作。另外,头文件的路径也可以使用程序自动查找和设定。我们可以使用使用以下的 python-config 文件和 Makefile 文件。

#!/usr/bin/env python
# -*- python -*-

# python-config

import sys, os
import getopt
try:
    import sysconfig
except ImportError:
    from distutils import sysconfig

valid_opts = ['help', 'prefix', 'exec-prefix', 'includes', 'libs', 'cflags',
              'ldflags', 'extension-suffix', 'abiflags', 'configdir']

def exit_with_usage(code=1):
    sys.stderr.write("Usage: %s [%s]\n" % (
        sys.argv[0], '|'.join('--'+opt for opt in valid_opts)))
    sys.exit(code)

try:
    opts, args = getopt.getopt(sys.argv[1:], '', valid_opts)
except getopt.error:
    exit_with_usage()

if not opts:
    exit_with_usage()

getvar = sysconfig.get_config_var
pyver = getvar('VERSION')
try:
    abiflags = sys.abiflags
except AttributeError:
    abiflags = ''

opt_flags = [flag for (flag, val) in opts]

if '--help' in opt_flags:
    exit_with_usage(code=0)

for opt in opt_flags:
    if opt == '--prefix':
        print(getvar('prefix'))

    elif opt == '--exec-prefix':
        print(getvar('exec_prefix'))

    elif opt in ('--includes', '--cflags'):
        try:
            include = sysconfig.get_path('include')
            platinclude = sysconfig.get_path('platinclude')
        except AttributeError:
            include = sysconfig.get_python_inc()
            platinclude = sysconfig.get_python_inc(plat_specific=True)
        flags = ['-I' + include]
        if include != platinclude:
            flags.append('-I' + platinclude)
        if opt == '--cflags':
            flags.extend(getvar('CFLAGS').split())
        print(' '.join(flags))

    elif opt in ('--libs', '--ldflags'):
        libs = getvar('LIBS').split() + getvar('SYSLIBS').split()
        libs.append('-lpython' + pyver + abiflags)
        if opt == '--ldflags':
            if not getvar('Py_ENABLE_SHARED'):
                libs.insert(0, '-L' + getvar('LIBPL'))
            if not getvar('PYTHONFRAMEWORK'):
                libs.extend(getvar('LINKFORSHARED').split())
        print(' '.join(libs))

    elif opt == '--extension-suffix':
        ext_suffix = getvar('EXT_SUFFIX')
        if ext_suffix is None:
            ext_suffix = getvar('SO')
        print(ext_suffix)

    elif opt == '--abiflags':
        print(abiflags)

    elif opt == '--configdir':
        print(getvar('LIBPL'))
# Makefile

.PHONY: default
default: build test clean

PYTHON = python
PYTHON_CONFIG = ${PYTHON} ./python-config
MPI4PY_INCLUDE = ${shell ${PYTHON} -c 'import mpi4py; print( mpi4py.get_include() )'}


MPICC = mpicc
CFLAGS = -fPIC ${shell ${PYTHON_CONFIG} --includes}
LDFLAGS = -shared ${shell ${PYTHON_CONFIG} --libs}
SO = ${shell ${PYTHON_CONFIG} --extension-suffix}
.PHONY: build
build: helloworld${SO}
helloworld${SO}: helloworld.c
    ${MPICC} ${CFLAGS} -I${MPI4PY_INCLUDE} -o $@ $< ${LDFLAGS}


MPIEXEC = mpiexec
NP_FLAG = -n
NP = 5
.PHONY: test
test: build
    ${MPIEXEC} ${NP_FLAG} ${NP} ${PYTHON} test.py


.PHONY: clean
clean:
    ${RM} helloworld${SO}

有了以上的 python-config 和 Makefile,编译扩展库,执行程序及清理可以分别使用如下命令,非常方便:

$ make build
$ make test
$ make clean

以上我们介绍了在 mpi4py 中包装和调用 C 语言 MPI 程序方法。在实际应用中直接使用 Python/C API 编写 Python 扩展模块是比较麻烦的,要求对 Python/C API 非常熟悉才能很好地运用,更常用的做法是使用像 SWIG 这样的工具来包装 C/C++ 程序文件,在下一篇中我们将介绍用 SWIG 包装 C 语言 MPI 程序以供 mpi4py 调用的方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 前言 计算机编程语言很多,但是适合高性能数值计算的语言却并不多,在高性能计算的项目中通常会使用到的语言有 Fort...
    自可乐阅读 19,603评论 3 22
  • 数据结构与算法 1.算法的有穷性是指( )。答案:A A)算法程序的运行时间是有限的 B)算法程序所处理的数据量是...
    织梦学生阅读 3,349评论 1 15
  • 作者:邵正将PytLab,Python 中文社区专栏作者。主要从事科学计算与高性能计算领域的应用,主要语言为Pyt...
    Python中文社区阅读 4,824评论 1 31
  • 我所期待的爱情 像一颗参天的大树 遇到风时挺立身躯乘风而立 遇到雨时根茎盘地毫无畏惧 遇到烈日枝叶繁茂决不凋敝 从...
    西兰河阅读 134评论 0 0
  • 云卷云舒的水波流动 明媚的阳光洒在脸上点点滴滴 女人的花和花洒 男人的头探出窗外 金毛在草地打滚 孩子的心在飞扬 ...
    文乎阅读 146评论 0 1