如果没有root访问权限,则在与参考BLAS链接时使用调优的BLAS运行R.

李哲源*_*李哲源 4 linux r shared-libraries ld blas

任何人都可以告诉我为什么我不能通过以下方式成功测试R中OpenBLAS的dgemm性能(在GFLOP 中)?

  1. 链接R与"参考BLAS" libblas.so
  2. mmperf.c使用OpenBLAS库编译我的C程序libopenblas.so
  3. 将生成的共享库加载mmperf.so到R中,调用R包装函数mmperfdgemm在GFLOP中报告性能.

第1点看起来很奇怪,但我别无选择,因为我在我想测试的机器上没有root访问权限,所以实际链接到OpenBLAS是不可能的.通过"未成功"我的意思是我的程序最终报告dgemm以供参考BLAS,而不是OpenBLAS性能.我希望有人可以向我解释一下:

  1. 为什么我的方式不起作用;
  2. 它是否有可能使它工作(这很重要,因为如果不可能,我必须编写一个C main函数并在C程序中完成我的工作.)

我已经调查了这个问题两天,在这里我将包括各种系统输出,以帮助您进行诊断.为了使事情可以重现,我还将包括代码,makefile以及shell命令.

第1部分:测试前的系统环境

调用R有两种方法,使用RRscript.调用它们时加载的内容有一些差异:

~/Desktop/dgemm$ readelf -d $(R RHOME)/bin/exec/R | grep "NEEDED"
0x00000001 (NEEDED)         Shared library: [libR.so]
0x00000001 (NEEDED)         Shared library: [libpthread.so.0]
0x00000001 (NEEDED)         Shared library: [libc.so.6]

~/Desktop/dgemm$ readelf -d $(R RHOME)/bin/Rscript | grep "NEEDED"
0x00000001 (NEEDED)         Shared library: [libc.so.6]
Run Code Online (Sandbox Code Playgroud)

这里我们需要选择Rscript,因为R加载libR.so会自动加载参考BLAS libblas.so.3:

~/Desktop/dgemm$ readelf -d $(R RHOME)/lib/libR.so | grep blas
0x00000001 (NEEDED)         Shared library: [libblas.so.3]

~/Desktop/dgemm$ ls -l /etc/alternatives/libblas.so.3
... 31 May /etc/alternatives/libblas.so.3 -> /usr/lib/libblas/libblas.so.3.0

~/Desktop/dgemm$ readelf -d /usr/lib/libblas/libblas.so.3 | grep SONAME
0x0000000e (SONAME)         Library soname: [libblas.so.3]
Run Code Online (Sandbox Code Playgroud)

相比之下,Rscript给人一种更清洁的环境.

第2部分:OpenBLAS

OpenBLAS下载源文件和简单make命令后,libopenblas-<arch>-<release>.so-<version>可以生成表单的共享库.请注意,我们没有root权限来安装它; 相反,我们将此库复制到我们的工作目录中~/Desktop/dgemm,并将其重命名为libopenblas.so.与此同时,我们必须使用名称制作另一个副本libopenblas.so.0,因为这是运行时加载器将寻求的SONAME:

~/Desktop/dgemm$ readelf -d libopenblas.so | grep "RPATH\|SONAME"
0x0000000e (SONAME)         Library soname: [libopenblas.so.0]
Run Code Online (Sandbox Code Playgroud)

请注意,RPATH未给出该属性,这意味着要放入此库/usr/lib,我们应该调用ldconfig它来添加它ld.so.cache.但是我们再次没有root权限来执行此操作.事实上,如果可以做到这一点,那么所有的困难都消失了.然后我们可以update-alternatives --config libblas.so.3用来有效地将R链接到OpenBLAS.

第3部分:C代码,Makefile和R代码

这是一个C脚本mmperf.c计算GFLOPs乘以2平方矩阵的大小N:

#include <R.h>
#include <Rmath.h>
#include <Rinternals.h>
#include <R_ext/BLAS.h>
#include <sys/time.h>

/* standard C subroutine */
double mmperf (int n) {
  /* local vars */
  int n2 = n * n, tmp; double *A, *C, one = 1.0;
  struct timeval t1, t2; double elapsedTime, GFLOPs;
  /* simulate N-by-N matrix A */
  A = (double *)calloc(n2, sizeof(double));
  GetRNGstate();
  tmp = 0; while (tmp < n2) {A[tmp] = runif(0.0, 1.0); tmp++;}
  PutRNGstate();
  /* generate N-by-N zero matrix C */
  C = (double *)calloc(n2, sizeof(double));
  /* time 'dgemm.f' for C <- A * A + C */
  gettimeofday(&t1, NULL);
  F77_CALL(dgemm) ("N", "N", &n, &n, &n, &one, A, &n, A, &n, &one, C, &n);
  gettimeofday(&t2, NULL);
  /* free memory */
  free(A); free(C);
  /* compute and return elapsedTime in microseconds (usec or 1e-6 sec) */
  elapsedTime = (double)(t2.tv_sec - t1.tv_sec) * 1e+6;
  elapsedTime += (double)(t2.tv_usec - t1.tv_usec);
  /* convert microseconds to nanoseconds (1e-9 sec) */
  elapsedTime *= 1e+3;
  /* compute and return GFLOPs */
  GFLOPs = 2.0 * (double)n2 * (double)n / elapsedTime;
  return GFLOPs;
  }

/* R wrapper */
SEXP R_mmperf (SEXP n) {
  double GFLOPs = mmperf(asInteger(n));
  return ScalarReal(GFLOPs);
  }
Run Code Online (Sandbox Code Playgroud)

这是一个简单的R脚本,mmperf.R用于报告案例的GFLOPN = 2000

mmperf <- function (n) {
  dyn.load("mmperf.so")
  GFLOPs <- .Call("R_mmperf", n)
  dyn.unload("mmperf.so")
  return(GFLOPs)
  }

GFLOPs <- round(mmperf(2000), 2)
cat(paste("GFLOPs =",GFLOPs, "\n"))
Run Code Online (Sandbox Code Playgroud)

最后有一个简单的makefile来生成共享库mmperf.so:

mmperf.so: mmperf.o
    gcc -shared -L$(shell pwd) -Wl,-rpath=$(shell pwd) -o mmperf.so mmperf.o -lopenblas

mmperf.o: mmperf.c
    gcc -fpic -O2 -I$(shell Rscript --default-packages=base --vanilla -e 'cat(R.home("include"))') -c mmperf.c
Run Code Online (Sandbox Code Playgroud)

将所有这些文件放在工作目录下~/Desktop/dgemm,然后编译它:

~/Desktop/dgemm$ make
~/Desktop/dgemm$ readelf -d mmperf.so | grep "NEEDED\|RPATH\|SONAME"
0x00000001 (NEEDED)            Shared library: [libopenblas.so.0]
0x00000001 (NEEDED)            Shared library: [libc.so.6]
0x0000000f (RPATH)             Library rpath: [/home/zheyuan/Desktop/dgemm]
Run Code Online (Sandbox Code Playgroud)

输出向我们保证OpenBLAS已正确链接,并且正确设置了运行时加载路径.

第4部分:在R中测试OpenBLAS

让我们做

~/Desktop/dgemm$ Rscript --default-packages=base --vanilla mmperf.R
Run Code Online (Sandbox Code Playgroud)

请注意,我们的脚本只需要baseR中的包,--vanilla用于忽略R启动时的所有用户设置.在我的笔记本电脑上,我的程序返回

GFLOPs = 1.11
Run Code Online (Sandbox Code Playgroud)

哎呀!这确实是参考BLAS性能而不是OpenBLAS(约为8-9 GFLOP).

第5部分:为什么?

说实话,我不知道为什么会这样.每个步骤似乎都正常工作.调用R时是否会发生微妙的事情?例如,出于某种原因,某些时候参考BLAS会覆盖OpenBLAS库的任何可能性吗?任何解释和解决方案?谢谢!

Emp*_*ian 5

为什么我的方式不起作用

首先,UNIX上的共享库旨在模仿归档库的工作方式(归档库首先存在).特别是这意味着如果你有libfoo.solibbar.so两个都定义了符号foo,那么首先加载的是获胜的库:foo从程序中任何地方(包括from libbar.so)的所有引用都将绑定到libfoo.sos的定义foo.

这种模仿,如果你链接您对程序会发生什么libfoo.alibbar.a,其中两个档案库中定义的相同符号foo.在归档更多信息链接在这里.

从上面应该清楚,如果libblas.so.3libopenblas.so.0定义相同的符号集(他们这样做),并且如果libblas.so.3首先加载到进程中,那么libopenblas.so.0永远不会调用例程.

其次,你已经正确地决定,因为R直接链接libR.so,并且因为libR.so直接链接libblas.so.3,所以保证libopenblas.so.0将失去战斗.

但是,您错误地决定Rscript比较好,但它不是:Rscript是一个很小的二进制文件(11K我的系统上,比较2.4MB的libR.so),它也几乎全部是execR.在strace输出中看到这一点很简单:

strace -e trace=execve /usr/bin/Rscript --default-packages=base --vanilla /dev/null
execve("/usr/bin/Rscript", ["/usr/bin/Rscript", "--default-packages=base", "--vanilla", "/dev/null"], [/* 42 vars */]) = 0
execve("/usr/lib/R/bin/R", ["/usr/lib/R/bin/R", "--slave", "--no-restore", "--vanilla", "--file=/dev/null", "--args"], [/* 43 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=89625, si_status=0, si_utime=0, si_stime=0} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=89626, si_status=0, si_utime=0, si_stime=0} ---
execve("/usr/lib/R/bin/exec/R", ["/usr/lib/R/bin/exec/R", "--slave", "--no-restore", "--vanilla", "--file=/dev/null", "--args"], [/* 51 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=89630, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
Run Code Online (Sandbox Code Playgroud)

这意味着在你的脚本开始执行的时候,libblas.so.3已经被加载,libopenblas.so.0将被加载的依赖mmperf.so不会真正被用于任何东西.

它是否有可能使其发挥作用

大概.我可以想到两种可能的解决方案:

  1. 假装libopenblas.so.0实际上是libblas.so.3
  2. 重建整个Rlibopenblas.so.

对于#1,您需要ln -s libopenblas.so.0 libblas.so.3,然后libblas.so.3通过LD_LIBRARY_PATH适当设置确保在系统之前找到您的副本.

这似乎对我有用:

mkdir /tmp/libblas
# pretend that libc.so.6 is really libblas.so.3
cp /lib/x86_64-linux-gnu/libc.so.6 /tmp/libblas/libblas.so.3
LD_LIBRARY_PATH=/tmp/libblas /usr/bin/Rscript /dev/null
Error in dyn.load(file, DLLpath = DLLpath, ...) :
  unable to load shared object '/usr/lib/R/library/stats/libs/stats.so':
  /usr/lib/liblapack.so.3: undefined symbol: cgemv_
During startup - Warning message:
package ‘stats’ in options("defaultPackages") was not found
Run Code Online (Sandbox Code Playgroud)

注意我是如何得到一个错误的(我的"假装" libblas.so.3没有定义它所期望的符号,因为它确实是一个副本libc.so.6).

您还可以通过libblas.so.3这种方式确认加载的版本:

LD_DEBUG=libs LD_LIBRARY_PATH=/tmp/libblas /usr/bin/Rscript /dev/null |& grep 'libblas\.so\.3'
     91533: find library=libblas.so.3 [0]; searching
     91533:   trying file=/usr/lib/R/lib/libblas.so.3
     91533:   trying file=/usr/lib/x86_64-linux-gnu/libblas.so.3
     91533:   trying file=/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server/libblas.so.3
     91533:   trying file=/tmp/libblas/libblas.so.3
     91533: calling init: /tmp/libblas/libblas.so.3
Run Code Online (Sandbox Code Playgroud)

对于#2,你说:

我在要测试的机器上没有root访问权限,因此无法实际链接到OpenBLAS.

但这似乎是一个虚假的论点:如果你可以建立libopenblas,当然你也可以建立自己的版本R.

更新:

你在开头提到libblas.so.3和libopenblas.so.0定义了相同的符号,这是什么意思?他们有不同的SONAME,是不是不足以通过系统区分它们?

符号和SONAME彼此无关.

您可以从输出中看到的符号readelf -Ws libblas.so.3readelf -Ws libopenblas.so.0.涉及到的符号BLAS,比如cgemv_,将出现在这两个库.

SONAME 可能来自Windows的困惑.DLLWindows上的s设计完全不同.特别是,当FOO.DLL进口象征barBAR.DLL,双方(符号的名称bar),并DLL从该符号是进口的(BAR.DLL)被记录在FOO.DLL的进口表.

这使得它轻松拥有R进口cgemv_BLAS.DLL,而MMPERF.DLL进口的相同符号的OPENBLAS.DLL.

但是,这使得库的插入很难,并且与归档库的工作方式完全不同(即使在Windows上).

关于哪种设计总体上更好的意见不同,但两种系统都不可能改变其模型.

UNIX可以通过多种方式模拟Windows样式的符号绑定:请参阅RTLD_DEEPBINDdlopen 手册页.要注意:这些都充满了危险,可能会使UNIX专家感到困惑,没有被广泛使用,并且可能存在实施错误.

更新2:

你的意思是我编译R并将其安装在我的主目录下?

是.

然后,当我想调用它时,我应该明确地给出我的可执行程序版本的路径,否则可能会调用系统上的那个?或者,我可以将此路径放在环境变量$ PATH的第一个位置来欺骗系统吗?

无论哪种方式都有效.