5.10. 使用 Manual Sharding

PopRT Manual Sharding 支持通过用户提供的切分点将模型划分成不同的子图, 实现模型并行和流水线并行.

5.10.1. Sharding / 模型并行

PopRT 支持根据用户提供的切分点将 ONNX graph 切分到不同的设备实现模型并行, 适用于超出单个设备内存限制, 需要占用多个设备的大模型.

参考: Sharding 原理说明

Note

使用模型并行, 需要对 PopRT backend options 进行如下设置:

  • options.virtual_graph_mode = “manual”

  • options.num_ipus = 设备数量

5.10.2. Pipelining / 流水线并行

PopRT 支持根据用户的提供的切分点将 ONNX graph 切分成不同的流水线阶段, 实现流水线并行, 提高 Throughput.

参考: Pipelining 原理说明

Note

使用流水线并行, 需要基于模型并行并对 PopRT backend options 进行如下设置:

  • options.enable_pipelining = True

  • options.batches_per_step = 流水线阶段数量的整数倍

5.10.3. Manual Sharding 流程

PopRT Manual Sharding 基于 ONNX node 对 ONNX graph 进行切分, 切分点可以是任意的 ONNX node.

  1. ONNX graph 中 nodes 是按拓扑排序的顺序排列的. PopRT Manual Sharding 首先对用户设置的切分点进行拓扑排序.

  2. 遍历切分点, 以切分点为起点向输入方向遍历 ONNX graph, 将遍历到的所有 ONNX node 放入一个子图中, 如果遇到 node 没有输入 node 或 node 已经设置了切分信息则停止该分支的遍历.

  3. 遍历完整后将得到子图, 以 ONNX attribute 的方式对子图设置切分信息:

    __ipu_number 指定模型并行中每个子图对应的设备序号

    __pipeline_stage 指定流水线并行中每个子图对应的流水线阶段.

Note

  • 通常不同的切分点对应不同的设备序号和流水线阶段, 但同一个设备上可以有多个子图和多个流水线阶段.

  • 根据切分点设置切分信息后, 剩余没有设置切分信息的 node 将被自动设置:

    __ipu_number 将被设置为当前已设置的最大设备序号 + 1.

    __pipeline_stage 将被设置为当前已设置的最大流水线阶段 + 1.

5.10.4. 配置 Manual Sharding

Manual Sharding 有两种配置方法, 一种是通过 PopRT CLI, 另一种是通过 poprt.converter.Sharder API.

通过 PopRT CLI 配置 Manual Sharding

  • 通过 yaml 文件指定切分点名称, 设备序号和流水线阶段:

Listing 5.17 shard.yaml
 1-
 2  node: resnetv17_stage1__plus0
 3  device: 0
 4  stage: 0
 5-
 6  node: resnetv17_stage4_batchnorm2_fwd
 7  device: 1
 8  stage: 1
 9-
10  node: resnetv17_stage4__plus0
11  device: 2
12  stage: 2

Download shard.yaml

  • 通过 PopRT CLI 中通过 --manual_sharding_config 来配置切分信息:

poprt \
    --input_model model.onnx \
    --manual_sharding_config shard.yaml
  • 通过 PopRT CLI 中 --only_manual_sharding 来确定是否仅对 input_model 进行 Manual Sharding, 默认不设置.

    不设置 --only_manual_sharding 表示对 input_model 进行 Convert 优化后再进行 Manual Sharding.

    设置 --only_manual_sharding 表示对 input_model 仅进行 Manual Sharding, 仅支持 --input_model, --output_model, --output_dir--manual_sharding_config, 其它参数无效.

poprt \
    --input_model model.onnx \
    --manual_sharding_config shard.yaml \
    --only_manual_sharding

通过 poprt.converter.Sharder API 配置 Manual Sharding

sharding_info = {
    "resnetv17_stage1__plus0": 0,
    "resnetv17_stage4_batchnorm2_fwd": 1,
    "resnetv17_stage4__plus0: 2,
}
pipelining_info = {
    "resnetv17_stage1__plus0": 0,
    "resnetv17_stage4_batchnorm2_fwd": 1,
    "resnetv17_stage4__plus0: 2,
}

sharded_model = poprt.converter.Sharder(
                            sharding_info=sharding_info,
                            pipelining_info=pipelining_info
                        ).run(converted_model)

Note

  • 设置 --only_manual_sharding 的 CLI 或 使用 poprt.converter.Sharder API 需要保证 ONNX graph 中每一个 node 都有 unique name .

  • 不设置 --only_manual_sharding 的 CLI 无需保证 ONNX graph 中每一个 node 都有 unique name , Convert 优化过程会保证每一个 node 都有 unique name .

5.10.5. 示例

下面是一个简单的 Manual Sharding 的 example:

ResNet50 为例.

Listing 5.18 shard.py
 1# Copyright (c) 2023 Graphcore Ltd. All rights reserved.
 2
 3import datetime
 4
 5import numpy as np
 6import onnx
 7import requests
 8
 9from poprt import runtime
10from poprt.compiler import Compiler, CompilerOptions
11from poprt.converter import Sharder
12
13
14def load_model():
15    # Download model
16    url = 'https://github.com/onnx/models/raw/main/vision/classification/resnet/model/resnet50-v1-7.onnx'
17    response = requests.get(url)
18    if response.status_code == 200:
19        model = onnx.load_model_from_string(response.content)
20    else:
21        raise Exception(
22            f"Failed to download model with status_code {response.status_code}"
23        )
24    return model
25
26
27def manual_sharding(model):
28    # Fix the batch size to 1
29    model.graph.input[0].type.tensor_type.shape.dim[0].dim_value = 1
30
31    # Sharding and pipelining info
32    sharding_info = {
33        "resnetv17_stage1__plus0": 0,
34        "resnetv17_stage4_batchnorm2_fwd": 1,
35        "resnetv17_stage4__plus0": 2,
36    }
37    pipelining_info = {
38        "resnetv17_stage1__plus0": 0,
39        "resnetv17_stage4_batchnorm2_fwd": 1,
40        "resnetv17_stage4__plus0": 2,
41    }
42    model = Sharder(sharding_info=sharding_info, pipelining_info=pipelining_info).run(
43        model
44    )
45
46    return model
47
48
49def compile(model):
50    # Compile the model with backend options
51    model_bytes = model.SerializeToString()
52    outputs = [o.name for o in model.graph.output]
53
54    options = CompilerOptions()
55    options.ipu_version = runtime.DeviceManager().ipu_hardware_version()
56    # Sharding into 4 IPUs
57    options.num_ipus = 4
58    # Enable Sharding and Pipelining
59    options.enable_pipelining = True
60    options.virtual_graph_mode = "manual"
61    options.batches_per_step = 16
62
63    executable = Compiler.compile(model_bytes, outputs, options)
64    runner_config = runtime.RuntimeConfig()
65    runner_config.timeout_ns = datetime.timedelta(microseconds=0)
66    runner = runtime.Runner(executable, runner_config)
67    return runner
68
69
70def run(runner):
71    inputs_info = runner.get_execute_inputs()
72    outputs_info = runner.get_execute_outputs()
73
74    inputs = {}
75    for i in inputs_info:
76        inputs[i.name] = np.ones(i.shape, dtype=i.numpy_data_type())
77
78    outputs = {}
79    for o in outputs_info:
80        outputs[o.name] = np.zeros(o.shape, dtype=o.numpy_data_type())
81
82    runner.execute(inputs, outputs)
83
84
85if __name__ == '__main__':
86    model = load_model()
87    model = manual_sharding(model)
88    runner = compile(model)
89    run(runner)

Download shard.py