# GRPC++
## 简介
在大规模训练场景下，用户使用大量worker和ps，导致训练任务通信，数据拷贝等带来很大的开销。原生Tensorflow使用GRPC作为通信协议，在大规模场景下难以满足用户需求的训练性能。

针对上述问题，DeepRec提供了GRPC++支持更大规模的训练任务。通过Sharing Nothing架构、BusyPolling机制、用户态零拷贝、Send/Recv融合等多种优化实现，极大的降低了E2E的通信延时，数倍的提高了Server的吞吐能力，从而可以在DeepRec上支持更大的训练规模和更优的训练性能，在一些典型的业务场景上相比较原生的Tensorflow大幅提升了性能。

## 接口介绍
为了方便用户使用，开启GRPC++功能和使用GRPC一样简单，只需要配置`Protocol`字段即可。
在一些场景下，特别是Send/Recv算子特别多时，将Send/Recv算子fuse起来提升性能，具体的是通过在`config`中配置`tensor_fuse`字段来启动此功能，**默认不开启此功能**。

_注：在grpc协议下也能使用tensor_fuse功能。_

### Configure GRPC++
GRPC++使用了seastar做为底层的通信库，同时保留了GRPC的接口连接（用于MasterSession），这样需要为seastar配置一组ports。使用GRPC++需要在执行目录下配置.endpoint_map文件，格式如下：

```
127.0.0.1:3333=127.0.0.1:5555
127.0.0.1:4444=127.0.0.1:6666
```
其中worker0的GRPC ip/port为127.0.0.1:3333，那么配置的对应节点的seastar port为5555的配置方法为127.0.0.1:3333=127.0.0.1:5555
其中ps0的GRPC ip/port为127.0.0.1:4444，那么配置的对应节点的seastar port为6666的配置方法为127.0.0.1:3333=127.0.0.1:6666

对应的TF_CONFIG的配置中仍然使用的是GRPC的ip/port进行描述。

### MonitoredTrainingSession训练
```python
cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})

server = tf.train.Server(cluster,
                         job_name=FLAGS.job_name,
                         task_index=FLAGS.task_index,
                         protocol="grpc++") # 配置 protocol 
...

with tf.train.MonitoredTrainingSession(
    master=target,
    config=tf.ConfigProto(tensor_fuse=True, # 开启send/recv tensor融合功能
                          ...),
    ) as mon_sess:
  ...
```
### Estimator训练
Estimator中使用GRPC++，需要通过`RunConfig`来配置`protocol="grpc++"`：
```python
session_config = tf.ConfigProto(
    tensor_fuse=True, # 开启send/recv tensor融合功能
    inter_op_parallelism_threads=16,
    intra_op_parallelism_threads=16)

run_config = tf.estimator.RunConfig(model_dir=model_dir, 
                                    save_summary_steps=train_save_summary_steps,
                                    protocol="grpc++", # 配置 protocol
                                    session_config=session_config) # 配置config

...

classifier = tf.estimator.Estimator(
    model_fn=model_fn,
    params={...},
    config=run_config) # 配置 run_config
```
_注意: PS/Worker模式下使用estimator，一定不要使用ParameterServerStrategy。会导致这里的RunConfig的protocol不生效。_
## 最佳实践
GRPC++中除了上述的配置参数之外，还提供了一些环境变量来供用户对性能进行调优。
```python
os.environ['WORKER_ENABLE_POLLING'] = "False"
os.environ['PS_ENABLE_POLLING'] = "False"
```
对于第一组参数：_表示是否需要对通信线程进行polling。_

- `"WORKER_ENABLE_POLLING"`一般都配置成`"False"`。
- `"PS_ENABLE_POLLING"`在混部集群下配置为`"False"`，如果是独立集群或者CPU隔离做的比较好，可以设置为`"True"`。

```python
os.environ['NETWORK_PS_CORE_NUMBER'] = "8"
os.environ['NETWORK_WORKER_CORE_NUMBER'] = "2"
```
对于第二组参数：_表示分配多少个通信线程。为何此处环境变量名是XX_CORE_NUMBER，因为如果通信线程使用polling，那么需要完整占用一个core。_

需要结合任务实际分配的CPU core数量以及是否需要开启上述polling功能来确定。

目前NETWORK_PS_CORE_NUMBER的默认值是Min(8, connections), 取最小连接数和8中较小的一个，默认连接数不是最佳的配置，建议调整该参数。
目前NETWORK_WORKER_CORE_NUMBER的默认值是Min(2, connections), 取最小连接数和2中较小的一个，默认连接数不是最佳的配置，建议调整该参数。

- 对于worker，一般取2-4。因为PS的数量一般是很少的，总的连接数不会太多，2-4个线程足够使用。
- 对于PS，对于比较大的规模(100-几百数量级别)，一般取8-10足够(假设分配24个CPU core)，具体也需要看模型的计算复杂度以及分配的CPU Core数做相应的微调。对于较小或者较大的规模，相应的数量可以减少或增加。


```python
os.environ["WORKER_DISABLE_PIN_CORES"] = "True"
os.environ["PS_DISABLE_PIN_CORES"] = "True"
```
对于第三组参数：_表示通信线程是否需要绑核。_

DeepRec默认不绑核(此处仅针对seastar的通信线程)，用户在独占机器下可以尝试开启此功能。
