本文简单介绍Fluent UDF串行代码并行化处理的问题。
注:以下内容来自Fluent UDF文档。
1 介绍
Fluent求解器包含三种类型的执行器:Cortex、host以及node。当Fluent运行时,启动1个Cortex实例,之后启动1个host及n个nodes,因此总共有有n+2个运行进程。因此,在并行计算(串行计算也推荐)时,用户需确保UDF成功地在host及node节点上运行。首先,可能需要用户准备两个不同的UDF版本:一个用于host,另一个用于node。不过最好的做法是编写一个UDF,使其编译后可以在不同的版本上运行,这个过程称之为串行UDF的并行化处理。
用户可以向UDF中添加特殊的宏和编译器指令来实现这一过程。
编译器指令(例如#if RP_NODE、RP_HOST)及其否定形式,能够指定编译器只编译UDF中应用于特定处理器的代码,而忽略其余部分,关于编译器指令的使用,我们在后面再详细描述。
如果串行UDF执行一个依赖于其他计算节点(或主机)发送或接收数据的操作,或者使用Fluent 18.2之后版本引入的宏类型,那么该串行UDF必须是能够并行的。还有一些操作必须将串行代码并行化:
-
文件读写 -
Global Reductions -
全局求和 -
全局求最小及最大值 -
全局求逻辑值 -
一些包含网格及网格面的循环 -
在console窗口显示消息 -
向Host或Node节点打印信息
当串行代码实现并行化修改后,即可采用与串行代码相同的方式进行编译及挂载。
2 DPM模型UDF的并行化
DPM模型可以使用以下下两种并行方式:
-
共享内存(Shared Memory) -
消息传递(Message Passing)
当用户使用DPM相关的UDF宏时,其必须以上面该两种并行方式中的其中一种运行。由于DPM模型所需的所有流体变量保存在被跟踪的颗粒的数据结构中,因此在并行Fluent中使用DPM UDF时无需特别注意。不过以下两种情况例外:
-
当使用DEFINE_DPM_OUTPUT宏输出颗粒信息时,不允许使用c函数frpintf,此时应当使用特定的函数par_fprintf及par_fprintf_head采用并行方式写入文件。每个计算节点将颗粒信息写入到一个单独的临时文件,之后由Fluent排序后输出到最终文件。特定的输出函数与c函数fprintf采用相同的参数列表,但在当Fluent需要对文件进行排序时,需要指定一个扩展的参数列表。 -
当存储颗粒信息时,并行模拟时需要使用特定的变量,这些变量可以通过宏TP_USER_REAL(tp, i) (tp是类型Tracked_Particle *)和PP_USER_REAL(p, i) (p是类型Particle *)访问。只有这些这样颗粒信息才能跨越分区边界,而其他局部或全局变量无法跨越分区边界。
需要注意,如果想要访问其他数据(如单元网格上的物理量),那么除了共享内存的并行方式外,用户可以访问所有流动和求解变量,但是如果采用了共享内存的方式,则只能访问宏SV_DPM_LIST和SV_DPMS_LIST中定义的变量。这些宏在dpm.h文件中定义。
本节包含可以用来并行化串行UDF的宏。可以在引用的头文件(例如para.h)中找到这些宏的定义。
3 编译器指令
在将UDF转化为并行时,代码中的某些部分可能需要由Host节点完成,而另一部分则可能需要由Node节点完成。通过使用Fluent提供的编译器指令,可以分别指定代码哪些部分由Host或Node节点运行。用户为Host和Node节点编写一个UDF源文件,但是编译后可以生成不同的动态链接库版本。如用户可以将打印任务分配给Host节点,将任务计算整个区域网格的总体积分配给node节点。由于大多数操作系统是由host或node节点执行的,因此通常采用否定形式的编译器指令。
需要注意,Host节点的主要目的是解释来自Cortex的命令或数据,并将命令或数据传递给node-0节点。由于Host节点中不包含网格数据,因此需要格外小心不要在任何计算中Host节点,以防出现分母为零的情况。在这种情况下,需要将这些操作包裹在#if !RP_HOST
指令中,以指示编译器在执行与网格相关的计算时忽略Host节点。如想要利用UDF计算一个面Thread上的总面积,之后利用该总面积计算物理量的通量,如果不将Host排除在操作之外,则Host节点就是得到 的总面积为零,当UDF试图除以零计算通量时,将会出现浮点异常的错误提示。
代码示例:
#if !RP_HOST
avg_pres = total_pres_a / total_area;
#endif
上面代码指定了求商操作在node节点上完成。
当需要从没有数据的操作中排除node节点时,可以使用#if !RP_NODE指令。
下面是一个并行编译器指令的列表,以及它们的作用
/*************************/
/* Compiler Directives */
/***********************/
#if RP_HOST
/* 只在Host节点中处理*/
#endif
#if RP_NODE
/* 只在Node节点中处理*/
#endif
#if !RP_HOST
/* 只在Node节点中处理*/
#endif
#if !RP_NODE
/*只在Host节点中处理*/
#endif
下面UDF简单展示了编译器指令的使用。DEFINE_ADJUST宏中定义了一个名为where_am_i的函数。此函数查询以确定正在执行哪种类型的进程,然后在计算的节点上显示一条消息。
/*****************************************************
Simple UDF that uses compiler directives
*****************************************************/
#include "udf.h"
DEFINE_ADJUST(where_am_i, domain)
{
#if RP_HOST
Message("I am in the host processn");
#endif /* RP_HOST */
#if RP_NODE
Message("I am in the node process with ID %dn",myid);
#endif
}
这种不同类型处理器之间的简单功能分配在实际情况下是有用的。例如,用户可能希望在运行特定计算时(通过使用RP_NODE或!RP_HOST)在计算节点上显示一条消息。或者用户也可以选择指定host进程来显示消息(通过使用RP_HOST或!RP_NODE)。通常,用户希望主机进程只写一次消息。或者用户可能希望从所有nodes收集数据,并从host打印一次总数。要执行这种类型的操作,UDF需要在进程之间进行某种形式的通信。最常见的通信模式是host和node进程之间的通信。
4 Host与Node节点通信
Fluent提供了两个宏用于Host与Node节点之间的数据通信:host_to_node_type_num及node_to_host_type_num。
4.1 Host-to-Node数据传递
从Host节点向所有node节点发送数据,可以使用宏host_to_node_type_num。该宏的表达形式为:
host_to_node_type_num(val_1, val_2,...,val_num);
其中num是将在参数列表中传递的变量的数量,type是将传递的变量的数据类型。可以传递的变量的最大数量是7。数组和字符串也可以一次一个地从主机传递到节点,如下面的示例所示。
/* integer and real variables passed from host to nodes */
host_to_node_int_1(count);
host_to_node_real_7(len1, len2, width1, width2, breadth1, breadth2, vol);
/* string and array variables passed from host to nodes */
char wall_name[]="wall-17";
int thread_ids[10] = {1,29,5,32,18,2,55,21,72,14};
host_to_node_string(wall_name,8); /* remember terminating NUL character */
host_to_node_int(thread_ids,10);
注意,这些host_to_node通信宏不需要受并行udf的编译器指令保护,因为所有这些宏都会自动执行以下操作:
-
如果编译为Host版本,则发送数据 -
若编译为node版本,则接收数据
这组宏最常见的用途是将参数或边界条件从Host传递给Node节点。
4.2 Node-to-Host数据传递
从node-0节点向Host节点发送数据,可以使用宏:
node_to_host_type_num(val_1,val_2,...,val_num);
其中num是将在参数列表中传递的变量的数量,type是将传递的变量的数据类型。可以传递最多7个变量。如果想要传递更多的变量,可以使用数组。数组和字符串可以一次一个地从主机传递到节点,如下面的示例所示。
/* integer and real variables passed from compute node-0 to host */
node_to_host_int_1(count);
node_to_host_real_7(len1, len2, width1, width2, breadth1, breadth2, vol);
/* string and array variables passed from compute node-0 to host */
char *string;
int string_length;
real vel[ND_ND];
node_to_host_string(string,string_length);
node_to_host_real(vel,ND_ND);
host_to_node宏是host节点将数据传递给所有的node节点(通过node-0节点间接传递),而node_to_host宏只是node-0节点向host节点传递数据。
node_to_host宏不需要编译器指令(例如#if RP_NODE)的保护,因为它们会自动执行以下操作
-
如果节点是node-0,并且将UDF编译为node版本,则发送数据 -
如果UDF编译为node版本,但节点不是-node0,则什么事情都不做 -
如果UDF编译为host版本,则接收数据
这组宏最常见的用法是将node-0结果传递给host。
5 逻辑判断
在并行Fluent中有许多可以扩展为逻辑测试的宏。这些逻辑宏称为判断式,由后缀P表示,可以用作UDF中的测试条件。如果满足括号中的条件,以下判断式将返回TRUE。
# define MULTIPLE_COMPUTE_NODE_P (compute_node_count > 1)
# define ONE_COMPUTE_NODE_P (compute_node_count == 1)
# define ZERO_COMPUTE_NODE_P (compute_node_count == 0)
有许多判断式允许使用计算节点ID来测试UDF中node节点标识。计算节点的ID存储为全局整数变量myid。下面列出的每个宏都可以用来测试进程myid的某些条件。例如,判断式I_AM_NODE_ZERO_P将myid的值与node-0的ID进行比较,并在两者相同时返回TRUE。另一方面,I_AM_NODE_SAME_P(n)比较在n中传递的计算节点ID和myid。当两个id相同时,函数返回TRUE。节点ID判断式通常用于udf中的条件-if语句。
/* predicate definitions from para.h header file */
# define I_AM_NODE_HOST_P (myid == host)
# define I_AM_NODE_ZERO_P (myid == node_zero)
# define I_AM_NODE_ONE_P (myid == node_one)
# define I_AM_NODE_LAST_P (myid == node_last)
# define I_AM_NODE_SAME_P(n) (myid == (n))
# define I_AM_NODE_LESS_P(n) (myid < (n))
# define I_AM_NODE_MORE_P(n) (myid > (n))
在一个分区网格中,一个面可能同时出现在一个或两个分区中,为了使求和操作不重复计算它,它只被正式分配给一个分区。上面的判断式与相邻网格的分区ID一起使用,以确定它是否属于当前分区。使用的约定是将编号较小的计算节点指定为该面的principal计算节点。如果面位于其principal计算节点上,则PRINCIPAL_FACE_P返回TRUE。当希望对面执行全局且其中一些面是分区边界面时,可以使用该宏作为测试条件。下面是来自para.h的PRINCIPAL_FACE_P的定义。
/* predicate definitions from para.h header file */
# define PRINCIPAL_FACE_P(f,t) (!TWO_CELL_FACE_P(f,t) ||
PRINCIPAL_TWO_CELL_FACE_P(f,t))
# define PRINCIPAL_TWO_CELL_FACE_P(f,t)
(!(I_AM_NODE_MORE_P(C_PART(F_C0(f,t),THREAD_T0(t))) ||
I_AM_NODE_MORE_P(C_PART(F_C1(f,t),THREAD_T1(t)))))
6 全局约简宏
全局约简(global reduction)操作是从所有计算节点收集数据,并将数据约简为单个值或数组的操作。这些操作包括全局求和、全局最大值和最小值以及全局逻辑判断等。这些宏以前缀PRF_G开头,在头文件prf.h中定义。全局求和宏用后缀SUM表示、全局最大值用HIGH表示,全局最小值用LOW表示。后缀AND和OR标识全局逻辑。
变量数据类型在宏名称中标识,其中R表示实际数据类型,I表示整数,L表示逻辑。例如,宏PRF_GISUM查找计算节点上整数的总和。
每个全局约简宏都有两个不同的版本:一个采用单个变量参数,另一个采用变量数组。
-
宏名称中带有后缀1的宏接受一个参数,并返回单个值作为全局约简结果。例如,宏PRF_GIHIGH1(x)接受一个参数x并在所有计算节点中计算变量x的最大值,然后返回该值。,如下面的示例所示。
{
int y;
int x = myid;
y = PRF_GIHIGH1(x);
}
-
没有1后缀的宏计算全局约简变量数组。这些宏有三个参数:x、N和iwork,其中x是一个数组,N是数组中元素数量,iwork是一个与临时存储所需的x类型和大小相同的数组。这种类型的宏被传递给一个数组x,数组x的元素在从函数返回后被新的结果填充。例如,宏PRF_GIHIGH(x,N,iwork)计算x数组中每个元素在所有计算节点上的最大值,使用数组iwork作为临时存储,并通过将每个元素替换为结果的全局最大值来修改数组x。该函数不返回值。
{
real x[N], iwork[N];
PRF_GRHIGH(x,N,iwork);
}
6.1 全局求和
可用于计算变量的全局和的宏由后缀SUM标识。宏PRF_GISUM1及PRF_GISUM分别计算整数变量和整数变量数组的全局和。
PRF_GRSUM1(x)跨所有计算节点计算实变量x的和。运行单精度版本的Fluent时,全局和为浮点型,运行双精度版本时,全局和为双精度型。另外,PRF_GRSUM(x,N,iwork)在双精度时返回浮点数组,单精度返回double数组。
宏形式 | 宏描述 |
---|---|
PRF_GISUM1(x) | 返回所有计算节点上整型变量x的和 |
PRF_GISUM(x,N,iwork) | 设置数组x存储所有计算节点上的和 |
PRF_GRSUM1(x) | 返回所有计算节点上的变量x的和,单精度返回float,双精度返回double |
PRF_GRSUM(x,N,iwork) | 设置x为包含所有计算节点上变量的和的数组,单精度返回float数组,双精度返回double数组 |
注:数组调用为传址调用。
6.2 全局最大最小值
与全局求和类似,后缀为HIGH及LO的宏用于计算全局的最大值与最小值。
宏形式 | 宏描述 |
---|---|
PRF_GHIGH1(x) | 返回所有计算节点上整型变量x的最大值 |
PRF_GHIGH(x,N,iwork) | 设置x为包含所有计算节点上最大值的数组 |
PRF_GRHIGH1(x) | 返回所有计算节点上的变量x的最大值,单精度返回float,双精度返回double |
PRF_GRHIGH(x,N,iwork) | 设置x为包含所有计算节点上变量最大值的数组,单精度返回float数组,双精度返回double数组 |
PRF_GILOW1(x) | 设置x为包含所有计算节点上最小值的数组 |
PRF_GILOW(x,N,iwork) | 设置x为包含所有计算节点上最小值的数组 |
PRF_GRLOW1(x) | 返回所有计算节点上的变量x的最小值,单精度返回float,双精度返回double |
PRF_GRLOW(x,N,iwork) | 设置x为包含所有计算节点上变量最小值的数组,单精度返回float数组,双精度返回double数组 |
6.3 全局逻辑值
后缀AND及OR的宏可用于计算全局逻辑与及逻辑或的值。宏PRF_GLOR1(x)可以跨所有计算节点计算变量x的全局逻辑或。PRF_GLOR(x,N,iwork)计算变量数组x的全局逻辑或。如果计算节点上的任何对应元素为TRUE,则将x的元素设置为TRUE。
宏形式 | 宏描述 |
---|---|
PRF_GLOR1(x) | 任何计算节点值为TRUE则返回TRUE |
PRF_GLOR1(x,N,work) | 任何元素为TRUE则返回TRUE |
PRF_GLAND1(x) | 所有计算节点x值为TRUE则返回TRUE |
PRF_GLAND(x) | 所有变量数组元素为TRUE则返回TRUE |
6.4 全局同步
如果希望在执行下一个操作之前全局同步计算节点,可以使用PRF_GSYNC()。当在UDF中插入PRF_GSYNC宏时,在源代码中的上述命令在所有计算节点上完成之前,不会执行任何其他命令。在调试函数时,同步可能也很有用。
本篇文章来源于微信公众号: CFD之道
评论前必须登录!
注册