MPI中的网络通信的原理,需要解决以下几个问题:
1. MPI使用什么网络协议进行通信?
2.中央数据库是存储在哪一台机器上?
3.集群中如果有一台机器挂掉了是否会影响其他机器?
参考: https://aosabook.org/en/openmpi.html
根据MCA, 每个框架下的模块是可变的,例如, btl (字节传输层)框架下有N多个网络协议模块:
既然是可变的,但是我们运行的时候都没有传入对应的选择参数,也就是说明有默认值。 官方文档也说了,工程师和科学家尽可能帮我们选择一个合理的默认值,但是对于不同的机器集群会有不同的效果,所以建议我们自己测试最好的参数。
当每个通信域(包括MPI_COMM_WORLD和MPI_COMM_SELF)被创建时,每个可用模块被询问是否需要在新通信域中使用。模块可以拒绝被使用,例如,一个基于共享内存的模块只有当通信域中的所有进程都在相同的物理节点上时,才允许被使用。通信域将会选择最高优先级的模块使用。
当然,这个也是可以让用户更改的
根据 https://aosabook.org/en/openmpi.html 中介绍的,用户可以通过传入MCA命令行参数去改变运行时的模块
再看到位于 c/send.c 文件中的Send函数定义:
#if OMPI_BUILD_MPI_PROFILING #if OPAL_HAVE_WEAK_SYMBOLS #pragma weak MPI_Send = PMPI_Send #endif #define MPI_Send PMPI_Send #endif static const char FUNC_NAME[] = "MPI_Send"; int MPI_Send(const void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm) { int rc = MPI_SUCCESS; MEMCHECKER( memchecker_datatype(type); memchecker_call(&opal_memchecker_base_isdefined, buf, count, type); memchecker_comm(comm); ); if ( MPI_PARAM_CHECK ) { OMPI_ERR_INIT_FINALIZE(FUNC_NAME); if (ompi_comm_invalid(comm)) { return OMPI_ERRHANDLER_INVOKE(MPI_COMM_WORLD, MPI_ERR_COMM, FUNC_NAME); } else if (count < 0) { rc = MPI_ERR_COUNT; } else if (tag < 0 || tag > mca_pml.pml_max_tag) { rc = MPI_ERR_TAG; } else if (ompi_comm_peer_invalid(comm, dest) && (MPI_PROC_NULL != dest)) { rc = MPI_ERR_RANK; } else { OMPI_CHECK_DATATYPE_FOR_SEND(rc, type, count); OMPI_CHECK_USER_BUFFER(rc, buf, type, count); } OMPI_ERRHANDLER_CHECK(rc, comm, rc, FUNC_NAME); } if (MPI_PROC_NULL == dest) { return MPI_SUCCESS; } OPAL_CR_ENTER_LIBRARY(); rc = MCA_PML_CALL(send(buf, count, type, dest, tag, MCA_PML_BASE_SEND_STANDARD, comm)); OMPI_ERRHANDLER_RETURN(rc, comm, rc, FUNC_NAME); }
前面一堆都是错误检查,会让不合法的操作不会真正的进行 send 这个操作。
最后看到关键的发送代码:
rc = MCA_PML_CALL(send(buf, count, type, dest, tag, MCA_PML_BASE_SEND_STANDARD, comm));
MCA_PML_CALL 是一个宏,我们在 pml.h 中可以找到它:
#if MCA_ompi_pml_DIRECT_CALL
#include MCA_ompi_pml_DIRECT_CALL_HEADER
#define MCA_PML_CALL_STAMP(a, b) mca_pml_ ## a ## _ ## b
#define MCA_PML_CALL_EXPANDER(a, b) MCA_PML_CALL_STAMP(a,b)
#define MCA_PML_CALL(a) MCA_PML_CALL_EXPANDER(MCA_ompi_pml_DIRECT_CALL_COMPONENT, a)
#else
#define MCA_PML_CALL(a) mca_pml.pml_ ## a
#endif
由于 if 下代码块搜索不到,所以我们直接看 else 中的 mca_pml.pml_ send(buf, count, type, dest, tag, MCA_PML_BASE_SEND_STANDARD, comm)
其实 mca_pm 是一个导出的 mca_pml_base_module_t 变量:
OMPI_DECLSPEC extern mca_pml_base_module_t mca_pml;
mca_pml_base_module_t 的定义如下:
struct mca_pml_base_module_1_0_1_t { /* downcalls from MCA to PML */ mca_pml_base_module_add_procs_fn_t pml_add_procs; mca_pml_base_module_del_procs_fn_t pml_del_procs; mca_pml_base_module_enable_fn_t pml_enable; mca_pml_base_module_progress_fn_t pml_progress; /* downcalls from MPI to PML */ mca_pml_base_module_add_comm_fn_t pml_add_comm; mca_pml_base_module_del_comm_fn_t pml_del_comm; mca_pml_base_module_irecv_init_fn_t pml_irecv_init; mca_pml_base_module_irecv_fn_t pml_irecv; mca_pml_base_module_recv_fn_t pml_recv; mca_pml_base_module_isend_init_fn_t pml_isend_init; mca_pml_base_module_isend_fn_t pml_isend; mca_pml_base_module_send_fn_t pml_send; mca_pml_base_module_iprobe_fn_t pml_iprobe; mca_pml_base_module_probe_fn_t pml_probe; mca_pml_base_module_start_fn_t pml_start; mca_pml_base_module_improbe_fn_t pml_improbe; mca_pml_base_module_mprobe_fn_t pml_mprobe; mca_pml_base_module_imrecv_fn_t pml_imrecv; mca_pml_base_module_mrecv_fn_t pml_mrecv; /* diagnostics */ mca_pml_base_module_dump_fn_t pml_dump; /* FT Event */ mca_pml_base_module_ft_event_fn_t pml_ft_event; /* maximum constant sizes */ uint32_t pml_max_contextid; int pml_max_tag; int pml_flags; }; typedef struct mca_pml_base_module_1_0_1_t mca_pml_base_module_1_0_1_t; typedef mca_pml_base_module_1_0_1_t mca_pml_base_module_t;
哦!!!这下明白了吧!!!我们所有的进行的 send , recv 等点对点 (PML) 的通信函数都封装在了这个结构体 的 函数指针成员里。
为什么要这么做呢?
——之前我们说过它支持不同的通信协议,在用户没有特定输入的时候,默认选择最高优先级的通信协议。如果每个通信协议都对应一套函数,那不是很麻烦???
为了让这个设计简单,可维护,用一个 base 封装起常见的操作,改变函数指针即可以改变使用的协议啦!!
那么下一节,我们就得看看,这个导出的 mca_pml_base_module_t 变量 mca_pm 的 函数指针在哪里初始化?——也就是,我们要看看它如何选择通信协议的!