使用 Intel® 架构构建和探索 Prolog





0/5 (0投票)
本文旨在探讨当英特尔解决方案支持常用的人工智能函数式和逻辑编程语言时会发生什么。
引言
互联网上有很多关于机器学习和人工智能(AI)基本相同的说法,但这是一种误解。机器学习和知识推理都关注同一件事:构建智能软件。然而,机器学习是一种基于算法的人工智能方法,随着时间的推移和更多数据的暴露,其性能会不断提高,而知识推理则是一种基于符号逻辑的姊妹方法。
知识推理的策略通常是通过使用函数式和逻辑编程语言(如 Lisp*、Prolog* 和 ML*)来实现的,因为它们能够进行符号操作。这种操作通常与专家系统相关,其中人类提供高层规则,用于模拟知识,避免低级语言细节。这种关注点被称为“以心为中心”。通常需要某种(后向或前向)逻辑推理。
而机器学习则与系统的低级数学表示和一套训练数据相关联,这些数据可以引导系统提高性能。由于没有高层建模,这个过程被称为“以脑为中心”。任何能够方便地在命令式范例上编写向量代数和数值微积分的语言都可以。例如,有许多用 Python* 编写的机器学习系统,仅仅因为数学支持可以作为该编程语言的库使用。
本文旨在探讨当英特尔解决方案支持常用的人工智能函数式和逻辑编程语言时会发生什么。尽管机器学习系统在过去二十年中取得了成功,但传统人工智能的地位并未消失或减弱,特别是在需要解释计算机程序为何如此行为的系统中。因此,认为下一代学习系统将在没有高层描述的情况下开发是不现实的,因此预计某些问题将需要符号解决方案。Prolog 和类似的编程语言是解决这些问题的宝贵工具。
正如下面将详细介绍的,本文提出使用英特尔® C++ 编译器和库重新编译 Prolog 解释器,以评估它们对基于逻辑的人工智能的贡献。使用的两个主要产品是英特尔® Parallel Studio XE Cluster Edition 和 SWI-Prolog 解释器。还介绍了一个针对经典 AI 问题的实验。
在英特尔®架构上构建 Prolog
1. 以下描述使用配备了以下组件的系统:Intel® Core™ i7 4500U@1.8 GHz 处理器,64 位,Ubuntu 16.04 LTS 操作系统,8GB RAM,以及开启了超线程,每个核心 2 个线程(您可以通过输入 sudo dmidecode -t processor | grep -E '(Core Count|Thread Count)'
来检查)。不同的操作系统可能需要进行微小的改动。
2. 准备环境。
在硬件上优化性能是一个迭代过程。图 1 展示了一个流程图,描述了各种英特尔工具如何在优化任务的各个阶段为您提供帮助。
安装英特尔工具最便捷的方式是 下载 并安装英特尔® Parallel Studio XE 2017。解压 .tgz 文件后,您将得到一个名为 parallel_studio_xe_2017update4_cluster_edition_online
(或类似版本)的文件夹。打开终端,然后选择图形安装。
<user>@<host>:~% cd parallel_studio_xe_2017update4_cluster_edition_online <user>@<host>:~/parallel_studio_xe_2017update4_cluster_edition_online% ./install_GUI.sh
虽然您可能更喜欢进行完整安装,但本文将选择自定义安装,并包含对许多开发者常用的组件。建议也安装这些组件,以便在后续项目中能够进一步使用这些性能库。
- 英特尔® Trace Analyzer and Collector
- 英特尔® Advisor
- 英特尔® C++ 编译器
- 适用于 C/C++ 的英特尔® 数学核心库 (Intel® MKL)
- 英特尔® 线程构建模块 (Intel® TBB)
- 英特尔® 数据分析加速库 (Intel® DAAL)
- 英特尔® MPI 库
安装非常直接,不需要太多评论。完成此任务后,您必须通过在终端输入以下命令来测试英特尔® C++ 编译器的可用性。
<user>@<host>:~% cd .. <user>@<host>:~% icc --version icc (ICC) 17.0.4 20170411
如果找不到 icc
命令,是因为运行编译器环境的环境变量没有设置。您必须通过运行带有指定目标架构参数的预定义脚本来设置它们。
<user>@<host>:~% source /opt/intel/compilers_and_libraries/linux/bin/compilervars.sh -arch intel64 -platform linux
如果您愿意,可以通过以下方式节省磁盘空间。
<user>@<host>:~% rm -r parallel_studio_xe_2017update4_cluster_edition_online
3. 构建 Prolog。
本文使用 SWI-Prolog 解释器2,该解释器受简化 BSD 许可证保护。SWI-Prolog 提供了一个全面的免费 Prolog 环境。它广泛应用于研究、教育以及商业应用。您必须 下载 .tar.gz 格式的源代码。在本文撰写时,可用的版本是 7.4.2。首先,解压下载的文件。
<user>@<host>:~% tar zxvf swipl-<version>.tar.gz
然后,创建一个目录用于安装 Prolog 解释器。
<user>@<host>:~% mkdir swipl_intel
之后,准备编辑构建变量。
<user>@<host>:~% cd swipl-<version> <user>@<host>:~/swipl-<version>% cp -p build.templ build <user>@<host>:~/swipl-<version>% <edit> build
在 build
文件中,找到 PREFIX
变量,它指示 SWI-Prolog 的安装位置。您必须将其设置为:
PREFIX=$HOME/swipl_intel
然后,有必要设置一些编译变量。必须更改 CC
变量,以指示将使用英特尔® C++ 编译器而不是其他编译器。COFLAGS
用于启用速度优化。编译器向量化在 –O2
级别启用。您可以选择更高的级别(–O3
),但建议使用的标志是通常推荐的优化级别。使用此选项,编译器会执行一些基本的循环优化、内在函数内联、文件内过程间优化以及大多数常见的编译器优化技术。–mkl=parallel
选项允许访问一组经过优化和多线程处理的数学函数,以充分利用最新英特尔® 酷睿™ 处理器。它必须与特定的英特尔® MKL 线程层一起使用,具体取决于提供的线程选项。在本文中,英特尔® TBB 就是这样一个选项,并通过选择 –tbb
标志来使用。最后,CMFLAGS
指示编译将创建 64 位可执行文件。
export CC="icc"
export COFLAGS="-O2 -mkl=parallel -tbb"
export CMFLAGS="-m64"
保存您的 build
文件并关闭它。
请注意,在撰写本文时,SWI-Prolog 尚不具备消息传递接口 (MPI) 功能3。此外,在检查其源代码时,未发现 OpenMP* 宏 (OMP
),因此 SWI-Prolog 可能也不支持 OpenMP*。
如果您计算机上已安装 SWI-Prolog 实例,您可能会对哪个解释器版本是用英特尔库编译的,哪个不是感到困惑。因此,通过在调用 SWI-Prolog 解释器时提示该功能,来表明您使用的是英特尔版本会很有用。因此,以下指令提供了一个定制的欢迎消息,用于运行解释器。
<user>@<host>:~/swipl-<version>% cd boot <user>@<host>:~/swipl-<version>/boot% <edit> messages.pl
搜索
prolog_message(welcome) -->
[ 'Welcome to SWI-Prolog (' ],
prolog_message(threads),
prolog_message(address_bits),
['version ' ],
prolog_message(version),
[ ')', nl ],
prolog_message(copyright),
[ nl ],
prolog_message(user_versions),
[ nl ],
prolog_message(documentaton),
[ nl, nl ].
并将 `@ Intel® architecture` 添加到其中,改为:
prolog_message(welcome) -->
[ 'Welcome to SWI-Prolog (' ],
prolog_message(threads),
prolog_message(address_bits),
['version ' ],
prolog_message(version),
[ ') @ Intel® architecture', nl ],
prolog_message(copyright),
[ nl ],
prolog_message(user_versions),
[ nl ],
prolog_message(documentaton),
[ nl, nl ].
保存您的 messages.pl
文件并关闭它。开始构建。
<user>@<host>:~/swipl-<version>/boot% cd .. <user>@<host>:~/swipl-<version>% ./build
编译会执行多项检查,并需要一段时间。不用担心,它确实非常冗长。最后,您将得到类似这样的输出:
make[1]: Leaving directory '~/swipl-<version>/src'
Warning: Found 9 issues.
No errors during package build
现在您可以通过输入以下命令来运行 SWI-Prolog 解释器:
<user>@<host>:~/swipl-<version>% cd ~/swipl_intel/lib/swipl-7.4.2/bin/x86_64-linux <user>@<host>:~/swipl_intel/lib/swipl-<version>/bin/x86_64-linux% ./swipl
欢迎使用 SWI-Prolog(多线程,64 位,版本 7.4.2) @ 英特尔® 架构 SWI-Prolog 绝对没有任何保修。这是免费软件。请运行 ?- license. 查看法律详情。有关在线帮助和背景信息,请访问 http://www.swi-prolog.org。有关内置帮助,请使用 ?- help(Topic). 或 ?- apropos(Word)。 1 ?-
要退出解释器,请输入 halt. .
现在您就可以使用由英特尔® 架构驱动的 Prolog 了。
您也可以通过以下方式节省磁盘空间。
<user>@<host>:~/swipl_intel/lib/swipl-<version>/bin/x86_64-linux% cd ~ <user>@<host>:~% rm -r swipl-<version>
探测实验
到目前为止,您的计算机上已经有了英特尔编译的 SWI-Prolog 版本。由于本实验旨在将这种组合与其他环境进行比较,因此需要一个使用不同编译器(如 gcc 5.4.0)编译的 SWI-Prolog 解释器。构建备用版本的过程与本文所述非常相似。
汉诺塔谜题4是一个经典的 AI 问题,我们使用它来探测 Prolog 解释器。以下代码是最优化的实现。
move(1,X,Y,_) :-
write('Move top disk from '),
write(X),
write(' to '),
write(Y),
nl.
move(N,X,Y,Z) :-
N>1,
M is N-1,
move(M,X,Z,Y),
move(1,X,Y,_),
move(M,Z,Y,X).
它会在塔盘之间移动圆盘并记录其移动过程。加载此实现并运行 3 个圆盘的实例问题(move(3,left,right,center)
)后,在 48 次推理后将获得以下输出。
Move top disk from left to right
Move top disk from left to center
Move top disk from right to center
Move top disk from left to right
Move top disk from center to left
Move top disk from center to right
Move top disk from left to right
true .
此测试旨在比较英特尔 SWI-Prolog 版本与 gcc 编译版本的性能。请注意,终端输出打印是一项缓慢的操作,因此不建议在基准测试中使用它,因为它会掩盖结果。因此,对程序进行了修改,以通过两个整数的虚拟求和提供更好的探测。
move(1,X,Y,_) :-
S is 1 + 2.
move(N,X,Y,Z) :-
N>1,
M is N-1,
move(M,X,Z,Y),
move(1,X,Y,_),
move(M,Z,Y,X).
回想一下,SWI-Prolog 源代码似乎不支持 OpenMP*。然而,大多数循环可以通过在循环前插入宏 #pragma omp parallel for
来进行多线程处理。因此,定位了 SWI-Prolog 推理过程中的耗时循环,并为其添加了 OpenMP 宏。使用 –openmp
选项编译了源代码,构建了第三个 Prolog 解释器版本,并使用了 8 个线程。如果读者希望构建此并行化版本的 Prolog,需要执行以下操作。
在 ~/swipl-/src/pl-main.c
的 pl-main.c
的头部部分添加 #include <omp.h>
;如果您愿意,可以在 main
方法中添加 omp_set_num_threads(8)
来指定 8 个 OpenMP 线程。请注意,本实验环境提供 4 个核心,并开启了超线程,每个核心 2 个线程,因此使用了 8 个线程;否则,请将其省略,OpenMP 将自动分配其能处理的最大线程数。
int main(int argc, char **argv){
omp_set_num_threads(8);
#if O_CTRLC
main_thread_id = GetCurrentThreadId();
SetConsoleCtrlHandler((PHANDLER_ROUTINE)consoleHandlerRoutine, TRUE);
#endif
#if O_ANSI_COLORS
PL_w32_wrap_ansi_console(); /* decode ANSI color sequences (ESC[...m) */
#endif
if ( !PL_initialise(argc, argv) )
PL_halt(1);
for(;;)
{ int status = PL_toplevel() ? 0 : 1;
PL_halt(status);
}
return 0;
}
在 ~/swipl-<version>/src/pl-prof.c 的 pl-prof.c 的头部部分添加 #include <omp.h>;在 methods activateProfiler, add_parent_ref, profResumeParent, freeProfileNode, freeProfileData(void) 的 for-loop 前面添加 #pragma omp parallel for。
int activateProfiler(prof_status active ARG_LD){
.......... < non relevant source code ommited > .......…
LD->profile.active = active;
#pragma omp parallel for
for(i=0; i<MAX_PROF_TYPES; i++)
{ if ( types[i] && types[i]->activate )
(*types[i]->activate)(active);
}
.......... < non relevant source code ommited > ..........
return TRUE;
}
static void add_parent_ref(node_sum *sum,
call_node *self,
void *handle, PL_prof_type_t *type,
int cycle)
{ prof_ref *r;
sum->calls += self->calls;
sum->redos += self->redos;
#pragma omp parallel for
for(r=sum->callers; r; r=r->next)
{ if ( r->handle == handle && r->cycle == cycle )
{ r->calls += self->calls;
r->redos += self->redos;
r->ticks += self->ticks;
r->sibling_ticks += self->sibling_ticks;
return;
}
}
r = allocHeapOrHalt(sizeof(*r));
r->calls = self->calls;
r->redos = self->redos;
r->ticks = self->ticks;
r->sibling_ticks = self->sibling_ticks;
r->handle = handle;
r->type = type;
r->cycle = cycle;
r->next = sum->callers;
sum->callers = r;
}
void profResumeParent(struct call_node *node ARG_LD)
{ call_node *n;
if ( node && node->magic != PROFNODE_MAGIC )
return;
LD->profile.accounting = TRUE;
#pragma omp parallel for
for(n=LD->profile.current; n && n != node; n=n->parent)
{ n->exits++;
}
LD->profile.accounting = FALSE;
LD->profile.current = node;
}
static void freeProfileNode(call_node *node ARG_LD)
{ call_node *n, *next;
assert(node->magic == PROFNODE_MAGIC);
#pragma omp parallel for
for(n=node->siblings; n; n=next)
{ next = n->next;
freeProfileNode(n PASS_LD);
}
node->magic = 0;
freeHeap(node, sizeof(*node));
LD->profile.nodes--;
}
static void freeProfileData(void)
{ GET_LD
call_node *n, *next;
n = LD->profile.roots;
LD->profile.roots = NULL;
LD->profile.current = NULL;
#pragma omp parallel for
for(; n; n=next)
{ next = n->next;
freeProfileNode(n PASS_LD);
}
assert(LD->profile.nodes == 0);
}
该测试使用 20 个圆盘的实例问题,在 3,145,724 次推理后完成。使用 Prolog 函数 time 测量时间。每个测试在循环中运行 300 次,并丢弃任何远高于其他结果的结果。图 2 显示了所有三种配置消耗的 CPU 时间。
以 gcc 编译的 Prolog 为基准,英特尔工具的提速比为 1.35。这是一个不错的结果,因为源代码根本没有改变,开发者没有探索并行性,也没有调用专门的方法,也就是说,所有的盲目工作都委托给了英特尔® C++ 编译器和库。当使用英特尔 4.0 版 OpenMP 实现时,相同的提速比提高到了 4.60x。
结论
本文刻意关注了基于逻辑的人工智能。它表明,使用英特尔开发工具解决 AI 问题的好处不仅限于机器学习。一个常见的 Prolog 分发版使用英特尔® C++ 编译器、英特尔® MKL 和英特尔 4.0 版 OpenMP 来编译。尽管 Prolog 推理机制的算法不易优化,但获得了显著的加速。因此,任何用该 Prolog 解释器实现的符号逻辑问题的解决方案,都将由一个增强的引擎驱动。
参考文献
1. Intel. Getting Started with Intel® Parallel Studio XE 2017 Cluster Edition for Linux*, Intel® Parallel Studio 2017 Documentation, 2017.
2. SWI-Prolog, 2017. http://www.swi-prolog.org/,访问日期:2017 年 6 月 18 日。
3. Swiprolog - Summary and Version Information, High Performance Computing, Division of Information Technology, University of Maryland, 2017. http://hpcc.umd.edu/hpcc/help/software/swiprolog.html,访问日期:2017 年 6 月 20 日。
4. A. Beck, M. N. Bleicher, D. W. Crowe, Excursions into Mathematics, A K Peters, 2000.
5. Russell, Stuart; Norvig, Peter. Artificial Intelligence: A Modern Approach, Prentice Hall Series in Artificial Intelligence, Pearson Education Inc., 2nd edition, 2003.