我正在做研究,我经常需要使用不同的输入(重复输入的每个组合)执行相同的程序并存储结果,以进行聚合。
我想通过在多台机器上并行执行这些实验来加快这一过程。但是,我想避免手动启动它们的麻烦。此外,我希望我的程序可以作为单个线程实现,并且只在其上添加并行化。
我使用Ubuntu计算机,所有计算机都可以在本地网络中访问。 我知道GNU Parallel可以解决这个问题,但我不熟悉它。有人可以帮助我为我的实验设置环境吗?
答案 0 :(得分:1)
请注意,这个答案已经从我的一个脚本改编而来,未经测试。如果您发现错误,欢迎您编辑答案。
首先,为了使流程完全批量化,我们需要一个非交互式SSH登录(GNU Parallel用于远程启动命令的那些)。
为此,首先生成一对RSA密钥(如果您还没有),其中包含:
ssh-keygen -t rsa
将生成一对私钥和公钥,默认存储在~/.ssh/id_rsa
和~/.ssh/id_rsa.pub
中。使用这些位置很重要,因为openssh会在这里寻找它们。虽然openssh命令允许您指定私钥文件(通过-i PRIVATE_KEY_FILE_PATH
传递),但GNU Parallel没有这样的选项。
接下来,我们需要复制我们将要使用的所有远程计算机上的公钥。对于群集中的每台计算机(我将称之为#34; worker"),在本地计算机上运行此命令:
ssh-copy-id -i ~/.ssh/id_rsa.pub WORKER_USER@WORKER_HOST
此步骤是交互式的,因为您需要通过用户ID和密码登录每个工作人员。
从此刻起,从您的客户端登录到每个工作人员都是非交互式的。接下来,让我们使用逗号分隔的工作列表设置bash变量。我们将使用GNU Parallel特殊语法进行设置,该语法允许指示每个worker使用多少CPU:
WORKERS_PARALLEL="2/user1@192.168.0.10,user2@192.168.0.20,4/user3@10.0.111.69"
在这里,我指定在192.168.0.10上我只想要2个并行进程,而在10.0.111.69我想要。至于192.168.0.20,因为我没有指定任何数字,GNU Parallel会计算出远程机器有多少CPU(实际上是CPU内核)并执行那么多并行进程。
由于我还需要一个openssh可以理解的格式的相同列表,我将创建一个没有CPU信息和空格而不是逗号的第二个变量。我自动执行此操作:
WORKERS=`echo $WORKERS_PARALLEL | sed 's/[0-9]*\///g' | sed 's/,/ /g'`
现在是时候设置我的代码了。我假设每个worker都配置为运行我的代码,所以我只需要复制代码。在工作者上,我通常在/ tmp文件夹中工作,所以下面假设。代码将通过SSH隧道复制并远程提取:
WORKING_DIR=/tmp/myexperiments
TAR_PATH=/tmp/code.tar.gz
# Clean from previous executions
parallel --nonall -S $WORKERS rm -rf $WORKING_DIR $TAR_PATH
# Copy the the tar.gz file on the worker
parallel scp LOCAL_TAR_PATH {}:/tmp ::: `echo $WORKERS`
# Create the working directory on the worker
parallel --nonall -S $WORKERS mkdir -p $WORKING_DIR
# Extract the tar file in the working directory
parallel --nonall -S $WORKERS tar --warning=no-timestamp -xzf $TAR_PATH -C $WORKING_DIR
请注意,同一台计算机上的多次执行将使用相同的工作目录。我假设在特定时间只运行一个版本的代码;如果不是这种情况,则需要修改命令以使用不同的工作目录。
我使用--warning=no-timestamp
指令来避免在机器时间超过工人时间时发出的恼人警告。
我们现在需要在本地机器中创建用于存储运行结果的目录,每个实验组一个(即具有相同参数的多次执行)。在这里,我使用两个虚拟参数alpha和beta:
GROUP_DIRS="results/alpha=1,beta=1 results/alpha=0.5,beta=1 results/alpha=0.2,beta=0.5"
N_GROUPS=3
parallel --header : mkdir -p {DIR} ::: DIR $GROUP_DIRS
请注意,这里使用parallel
不是必需的:使用循环会有效,但我发现它更具可读性。我还存储了组的数量,我们将在下一步中使用它们。
最后的准备步骤包括创建将在实验中使用的所有参数组合的列表,每个参数组合根据需要重复多次。每次重复都会附加一个增量编号,用于识别不同的运行。
ALPHAS="1.0 0.5 0.2"
BETAS="1.0 1.0 0.5"
REPETITIONS=1000
PARAMS_FILE=/tmp/params.txt
# Create header
echo REP GROUP_DIR ALPHA BETA > $PARAMS_FILE
# Populate
parallel \
--header : \
--xapply \
if [ ! -e {GROUP_DIR}"exp"{REP}".dat" ]';' then echo {REP} {GROUP_DIR} {ALPHA} {BETA} '>>' $PARAMS_FILE ';' fi \
::: REP $(for i in `seq $REPETITIONS`; do printf $i" %.0s" $(seq $N_GROUPS) ; done) \
::: GROUP_DIR $GROUP_DIRS \
::: ALPHA $ALPHAS \
::: BETA $BETAS
在这一步中,我还实现了一个控件:如果.dat文件已经存在,我会跳过这组参数。这是从实践中产生的:我经常中断GNU Parallel的执行,后来决定通过重新执行这些命令来恢复它。通过这种简单的控制,我避免运行超过必要的实验。
现在我们终于可以进行实验了。此示例中的算法生成我想要检索的参数--save-data
中指定的文件。我还想将stdout和stderr保存在文件中,以便进行调试。
cat $PARAMS_FILE | parallel \
--sshlogin $WORKERS_PARALLEL \
--workdir $WORKING_DIR \
--return {GROUP_DIR}"exp"{REP}".dat" \
--return {GROUP_DIR}"exp"{REP}".txt" \
--cleanup \
--xapply \
--header 1 \
--colsep " " \
mkdir -p {TEST_DIR} ';' \
./myExperiment \
--random-seed {REP} \
--alpha {ALPHA} \
--beta {BETA} \
--save-data {GROUP_DIR}"exp"{REP}".dat" \
'&>' {GROUP_DIR}"exp"{REP}".txt"
关于参数的一点解释。 --sshlogin
(可以缩写为-S
)传递Parallel将用于分配计算负载的worker列表。 --workdir
设置Parallel的工作目录,默认为~
。 --return
指令在执行完成后复制指定的文件。 --cleanup
删除复制的文件。 --xapply
告诉Parallel将参数解释为元组(而不是设置乘以笛卡尔积)。 --header 1
告诉Parallel,参数文件的第一行必须被解释为header(其条目将用作列的名称)。 --colsep
告诉Parallel,参数文件中的列是以空格分隔的。
警告:Ubuntu的并行版本已过时(2013年)。特别是,有一个错误阻止上面的代码正常运行,这已经在几天前修复。要获取最新的每月快照,请运行(不需要root权限):
(wget -O - pi.dk/3 || curl pi.dk/3/ || fetch -o - http://pi.dk/3) | bash
请注意,上面提到的错误的修复程序将仅包含在2015年9月22日的下一个快照中。如果您赶时间,则应手动安装吸烟最热的。
最后,清洁我们的工作环境是一个好习惯:
rm $PARAMS_FILE
parallel --nonall -S $WORKERS rm -rf $WORKING_DIR $TAR_PATH
如果您将其用于研究并发表论文,请记住引用Ole Tange的原创作品(参见parallel --bibtex
)。