2.1 如何运行
官方给出的 Hovorod 运行范例之一如下:

horovodrun -np 2 -H localhost:4 --gloo python /horovod/examples/tensorflow2/tensorflow2_mnist.py
这里 -np 指的是进程的数量,localhost:4表示localhost节点上4个GPU。

注意,如果虚拟机只有一个核。想要强行地达到并行的效果,可以使用 -np参数,它会自动帮你把一个核心切成多份处理器,每一个分布式处理就是一个slot。

因此,我们可以从 horovodrun 这个命令入手看看。

2.2 horovodrun
入口文件可以从 setup.py 看到,其就被映射成 horovod.runner.launch:run_commandline。

entry_points={
 ‘console_scripts’: [
 ‘horovodrun = horovod.runner.launch:run_commandline’
 ]
 }


所以我们看看 run_commandline

2.3 run_commandline
该命令位于:horovod-master/horovod/runner/launch.py,我们摘录重要部分。

def run_commandline():
 args = parse_args()
 _run(args)


于是进入到 _run 函数。可以看到,Horovod 会依据是否是弹性训练来选择不同的路径。我们在此系列中,会首先分析 非弹性训练 _run_static。

def _run(args):
 # if hosts are not specified, either parse from hostfile, or default as
 # localhost
 if not args.hosts and not args.host_discovery_script:
 if args.hostfile:
 args.hosts = hosts.parse_host_files(args.hostfile)
 else:
 # Set hosts to localhost if not specified
 args.hosts = ‘localhost:{np}’.format(np=args.np)
# Convert nics into set
args.nics = set(args.nics.split(',')) if args.nics else None

if _is_elastic(args):
    return _run_elastic(args)
else:
    return _run_static(args) # 我们先看这里

2.4 非弹性训练 _run_static
在 _run_static 之中做了如下操作:

首先解析各种参数,得到 settings;
会调用 driver_service.get_common_interfaces 获取网卡以及其他host的信息,依据这些信息会进行slot分配,这部分很复杂,具体我们会有专文讲解(下一篇)。
这里有一个问题:为什么要得到 host, slot, rank 之间的关系信息?由于工程上的考虑,底层 C++ 世界中对于 rank 的角色做了区分:rank 0 是 master,rank n 是 worker,所以这些信息需要决定并且传递给 C++世界;
会根据是否在参数中传递运行函数来决定采取何种路径,一般默认没有运行参数,所以会执行_launch_job 来启动训练 job;
具体代码如下:

def _run_static(args):

settings = hvd_settings.Settings(verbose=2 if args.verbose else 0,
                                 ssh_port=args.ssh_port,
                                 ssh_identity_file=args.ssh_identity_file,
                                 extra_mpi_args=args.mpi_args,
                                 tcp_flag=args.tcp_flag,
                                 binding_args=args.binding_args,
                                 key=secret.make_secret_key(),
                                 start_timeout=tmout,
                                 num_proc=args.np,
                                 hosts=args.hosts,
                                 output_filename=args.output_filename,
                                 run_func_mode=args.run_func is not None,
                                 nics=args.nics,...)
  # 首先解析各种参数,得到 settings
fn_cache = None
if not args.disable_cache:
    params = ''
    if args.np:
        params += str(args.np) + ' '
    if args.hosts:
        params += str(args.hosts) + ' '
    if args.ssh_port:
        params += str(args.ssh_port)
    if args.ssh_identity_file:
        params += args.ssh_identity_file
    parameters_hash = hashlib.md5(params.encode('utf-8')).hexdigest()
    fn_cache = cache.Cache(CACHE_FOLDER, CACHE_STALENESS_THRESHOLD_MINUTES,
                           parameters_hash)

# 获取网卡以及其他host的信息,依据这些信息会进行slot分配
all_host_names, _ = hosts.parse_hosts_and_slots(args.hosts)
remote_host_names = network.filter_local_addresses(all_host_names)

nics = driver_service.get_common_interfaces(settings, all_host_names,
                                            remote_host_names, fn_cache)

if args.run_func:
    # get the driver IPv4 address
    driver_ip = network.get_driver_ip(nics)
    run_func_server = KVStoreServer(verbose=settings.verbose) # 启动内部KV服务器
    run_func_server_port = run_func_server.start_server()
    put_data_into_kvstore(driver_ip, run_func_server_port,
                          'runfunc', 'func', args.run_func) # 把'func', args.run_func存储成KV

    command = [sys.executable, '-m', 'horovod.runner.run_task', str(driver_ip), str(run_func_server_port)]

    try:
        _launch_job(args, settings, nics, command)
        results = [None] * args.np
        for i in range(args.np):
            results[i] = read_data_from_kvstore(driver_ip, run_func_server_port,'runfunc_result', str(i))
        return results
    finally:
        run_func_server.shutdown_server()
else:
    command = args.command
    _launch_job(args, settings, nics, command) # 我们重点讲解这里
    return None

目前逻辑如下:

+-----------+
          |horovodrun |
          +-----+-----+
                |
                |
                v
       +--------+--------+
       | run_commandline |
       +----+------+-----+
            |      |
  +---------+      +--------+
  |                         |
  |                         |
  v                         v
±----±-------+ ±—±-------+
 | _run_elastic | | _run_static |
 | | | |
 ±-------------+ ±------------+


至此,我们已经分析完成 horovod 的入口,下面会分析具体如何启动 Job。

0x03 运行训练 Job
3.1 _launch_job
_launch_job 会根据配置或者安装情况来进行具体调用。我们看到有三种可能:gloo, mpi, js。

jsrun的资料很难找,所以我们重点看看 gloo, mpi 这两种。

def _launch_job(args, settings, nics, command):
 env = os.environ.copy()
 config_parser.set_env_from_args(env, args)
def gloo_run_fn():
    driver_ip = network.get_driver_ip(nics)
    gloo_run(settings, nics, env, driver_ip, command)

def mpi_run_fn():
    mpi_run(settings, nics, env, command)

def js_run_fn():
    js_run(settings, nics, env, command)

run_controller(args.use_gloo, gloo_run_fn,
               args.use_mpi, mpi_run_fn,
               args.use_jsrun, js_run_fn,
               args.verbose)

3.2 run_controller
run_controller 依然是一个中介函数,具体导入 gloo 或者 mpi。

def run_controller(use_gloo, gloo_run, use_mpi, mpi_run, use_jsrun, js_run, verbosity):
 if use_gloo:
 gloo_run()
 elif use_mpi:
 mpi_run()
 elif use_jsrun:
 js_run()
 else:
 if mpi_built(verbose=verbose):
 if lsf.LSFUtils.using_lsf() and is_jsrun_installed():
 js_run()
 else:
 mpi_run()
 elif gloo_built(verbose=verbose):
 gloo_run()
 USB Microphone https://www.soft-voice.com/
 Wooden Speakers https://www.zeshuiplatform.com/