Kaldi中的nnet3是默认使用GPU运行的,至于多卡GPU的问题,还是先要对number of jobs(nj)的概念有所了解。假如用的是steps/nnet3/train_raw_dnn.py去训练网络图,这时候会需要设置num-jobs-initial和num-jobs-final,这两个分别表示网络训练初期的进程数与末期的进程数,而这个nj会根据epoch和数据量自动在某些特定的iteration去递增,从initial渐变增加到final。
另外,我个人习惯是把显卡的compute mode设为default,而不是exclusive;可能会不少人觉得exclusive后,会显得多卡运行,因为每张卡都有进程nj在跑了,但如果在一张12GB的卡上跑一个只有2kMB的进程,那其实这张卡还很空,利用率也太低了。如果显卡是设置为default,那就会充分使用一张卡的所有显存~但这时候又会出现另一个问题。
假如我们设置了1到10个nj,那在训练时,敲nvidia-smi查看进程占用显存的情况时,一个nj占用2k+MB,有5个nj同在一张卡上正在运行,那对于这张12GB的卡来说是接近占满了;如果后面再多一个nj,那程序就OOM了,这个时候去想多卡问题就显得很有必要(如果只有一张卡的话,那就把final nj设小)。
在运行kaldi程序前,不少人会发现--cmd这个option,它可以写run.pl进行单机执行,也可以写queue.pl去集群;其实这个queue.pl也是能实现单机多卡,它会调用gridengin去调配显卡资源,不过实现的操作比较复杂,我不是很推荐用gridengin来做这事。我想介绍的是另一种方式来做到多GPU跑nnet3,那是利用队列和compute_pr。
每个nj运行的机制是这样子:先执行compute_pr查询当前机子每张卡的空闲率,选用空闲率最大的那张卡,然后逐步去占用它,直至这个nj占够了它需要的空间;如果有多个nj,那这些nj就会同时去查询空闲率,因为它们是同时查询,所以通常会出现一起判定用一张卡的结果。所以能做的就是,不让这些nj同时查询,在每次查询中插入等待时间来错开nj的查询;通过错开查询,每次迭代开始的nj就成队列形式,逐个逐个去查询去占用。当等待时间越大,第一个nj就已经完成查询与占用,此时每张卡的空闲率情况就发生了改变,那第二个nj查询时,就不会直接跟上第一个nj而是用另一张更空的卡……如此类推,所以nj就成功错开地占用显卡。
在代码上实现插入等待时间,是在steps/libs/nnet3/train/frame_level_objf/common.py的train_new_model()中thread下169行加入time.sleep(20),这里的意思是每当把一个thread push到threads队列后,就sleep 20秒才执行下一个nj的查询与占用:
至于LSTM是不是默认多卡训练,这其实没关系的,是否多卡跟网络类型无关
补充:今年2019年更新的kaldi会有一种对半调用显存的机制,那是因为nnet3-train有一个新参数cuda-memory-proportion=0.5;这表示每个进程调用显存时,会直接把该显卡的一半显存先占用了(无论是default还是独占模型);如果单机少卡的情况下想开多进程跑网络,每个nj直接占用一半显存显然是不合理的。
如果仔细看这个参数的底层代码,会发现当初始设置是较少的比例时,若当前nj实际需要的显存大于分配显存,就会触发按需分配,重新分配刚好这个nj能运行的显存。例如,cuda-memory-proportion=0.1,代码初始给nj只分配了1G显存资源,若1G资源也能运行该nj,则就是以1G运行;若1G资源不足以运行该nj(它实际需要2.5G),那程序会再分配2.5G来运行它;这就是按需分配。
补充2:如果是ASR的朋友,上述要修改的脚本则是kaldi/egs/wsj/s5/steps/libs/nnet3/train/chain_objf/acoustic_model.py当中nnet3-chain-train的那部分