昇思MindSpore进阶教程--单节点数据缓存(下)

大家好,我是刘明,明志科技创始人,华为昇思MindSpore布道师。
技术上主攻前端开发、鸿蒙开发和AI算法研究。
努力为大家带来持续的技术分享,如果你也喜欢我的文章,就点个关注吧

缓存加速

为了使较大的数据集在多台服务器之间共享,缓解单台服务器的磁盘空间需求,用户通常可以选择使用NFS(Network File System)即网络文件系统来存储数据集,如华为云-NFS存储服务器。

然而,对于NFS数据集的访问通常开销较大,导致使用NFS数据集进行的训练用时较长。

为了提高NFS数据集的训练性能,我们可以选择使用缓存服务,将数据集以Tensor的形式缓存在内存中。

经过缓存后,后序的epoch就可以直接从内存中读取数据,避免了访问远程网络存储的开销。

需要注意的是,在训练过程的数据处理流程中,数据集经加载后通常还需要进行一些带有随机性的增强操作,如RandomCropDecodeResize,若将缓存添加到该具有随机性的操作之后,将会导致第一次的增强操作结果被缓存下来,后序从缓存服务器中读取的结果均为第一次已缓存的数据,导致数据的随机性丢失,影响训练网络的精度。

因此我们可以选择直接在数据集读取操作之后添加缓存。本节将采用这种方法,以MobileNetV2网络为样本,进行示例。

完整示例代码请参考ModelZoo的MobileNetV2。

  1. 创建管理缓存的Shell脚本cache_util.sh:
bootup_cache_server()
{
  echo "Booting up cache server..."
  result=$(cache_admin --start 2>&1)
  echo "${result}"
}

generate_cache_session()
{
  result=$(cache_admin -g | awk 'END {print $NF}')
  echo "${result}"
}

  1. 在启动NFS数据集训练的Shell脚本run_train_nfs_cache.sh中,为使用位于NFS上的数据集训练的场景开启缓存服务器并生成一个缓存会话保存在Shell变量CACHE_SESSION_ID中:
CURPATH="${dirname "$0"}"
source ${CURPATH}/cache_util.sh

bootup_cache_server
CACHE_SESSION_ID=$(generate_cache_session)

  1. 在启动Python训练时将CACHE_SESSION_ID以及其他参数传入:
python train.py \
--platform=$1 \
--dataset_path=$5 \
--pretrain_ckpt=$PRETRAINED_CKPT \
--freeze_layer=$FREEZE_LAYER \
--filter_head=$FILTER_HEAD \
--enable_cache=True \
--cache_session_id=$CACHE_SESSION_ID \
&> log$i.log &

  1. 在Python的参数解析脚本args.py的train_parse_args()函数中,通过以下代码接收传入的cache_session_id:
import argparse

def train_parse_args():

    train_parser.add_argument('--enable_cache',
        type=ast.literal_eval,
        default=False,
        help='Caching the dataset in memory to speedup dataset processing, default is False.')
    train_parser.add_argument('--cache_session_id',
         type=str,
         default="",
         help='The session id for cache service.')
train_args = train_parser.parse_args()

并在Python的训练脚本train.py中调用train_parse_args()函数解析传入的cache_session_id等参数,并在定义数据集dataset时将其作为参数传入。

from src.args import train_parse_args
args_opt = train_parse_args()

dataset = create_dataset(
    dataset_path=args_opt.dataset_path,
    do_train=True,
    config=config,
    enable_cache=args_opt.enable_cache,
    cache_session_id=args_opt.cache_session_id)

  1. 在定义数据处理流程的Python脚本dataset.py中,根据传入的enable_cache以及cache_session_id参数,创建一个DatasetCache的实例并将其插入至ImageFolderDataset之后:
def create_dataset(dataset_path, do_train, config, repeat_num=1, enable_cache=False, cache_session_id=None):

    if enable_cache:
        nfs_dataset_cache = ds.DatasetCache(session_id=int(cache_session_id), size=0)
    else:
        nfs_dataset_cache = None

    if config.platform == "Ascend":
        rank_size = int(os.getenv("RANK_SIZE", '1'))
        rank_id = int(os.getenv("RANK_ID", '0'))
        if rank_size == 1:
            data_set = ds.ImageFolderDataset(dataset_path, num_parallel_workers=8, shuffle=True, cache=nfs_dataset_cache)
        else:
            data_set = ds.ImageFolderDataset(dataset_path, num_parallel_workers=8, shuffle=True, num_shards=rank_size, shard_id=rank_id, cache=nfs_dataset_cache)

  1. 运行run_train_nfs_cache.sh,得到以下结果:
epoch: [  0/ 200], step:[ 2134/ 2135], loss:[4.682/4.682], time:[3364893.166], lr:[0.780]
epoch time: 3384387.999, per step time: 1585.193, avg loss: 4.682
epoch: [  1/ 200], step:[ 2134/ 2135], loss:[3.750/3.750], time:[430495.242], lr:[0.724]
epoch time: 431005.885, per step time: 201.876, avg loss: 4.286
epoch: [  2/ 200], step:[ 2134/ 2135], loss:[3.922/3.922], time:[420104.849], lr:[0.635]
epoch time: 420669.174, per step time: 197.035, avg loss: 3.534
epoch: [  3/ 200], step:[ 2134/ 2135], loss:[3.581/3.581], time:[420825.587], lr:[0.524]
epoch time: 421494.842, per step time: 197.421, avg loss: 3.417


下表展示了GPU服务器上使用缓存与不使用缓存的平均每个epoch时间对比:

| 4p, MobileNetV2, imagenet2012            | without cache | with cache |
| ---------------------------------------- | ------------- | ---------- |
| first epoch time                         | 1649s         | 3384s      |
| average epoch time (exclude first epoch) | 458s          | 421s       |

可以看到使用缓存后,相比于不使用缓存的情况第一个epoch的完成时间增加了较多,这主要是由于缓存数据写入至缓存服务器的开销导致的。但是,在缓存数据写入之后随后的每个epoch都可以获得较大的性能提升。因此,训练的总epoch数目越多,使用缓存的收益将越明显。

以运行200个epoch为例,使用缓存可以使端到端的训练总用时从92791秒降低至87163秒,共计节省约5628秒。

  1. 使用完毕后,可以选择关闭缓存服务器:
$ cache_admin --stop
Cache server on port 50052 has been stopped successfully.

缓存性能调优

使用缓存服务能够在一些场景下获得显著的性能提升,例如:

  • 缓存经过数据增强处理后的数据,尤其是当数据预处理管道中包含decode等高复杂度操作时。在该场景下,用户不需要在每个epoch重复执行数据增强操作,可节省较多时间。

  • 在简单网络的训练和推理过程中使用缓存服务。相比于复杂网络,简单网络的训练耗时占比更小,因此在该场景下应用缓存,能获得更显著的时间性能提升。

然而,在以下场景中使用缓存可能不会获得明显的性能收益,例如:

  • 系统内存不足、缓存未命中等因素将导致缓存服务在时间性能上提升不明显。因此,可在使用缓存前检查可用系统内存是否充足,选择一个适当的缓存大小。

  • 过多缓存溢出会导致时间性能变差。因此,在使用可随机访问的数据集(如ImageFolderDataset)进行数据加载的场景,尽量不要允许缓存溢出至磁盘。

  • 在Bert等NLP类网络中使用缓存,通常不会取得性能提升。因为在NLP场景下通常不会使用到decode等高复杂度的数据增强操作。

  • 使用non-mappable数据集(如TFRecordDataset)的pipeline在第一个epoch的时间开销较大。根据当前的缓存机制,non-mappable数据集需要在第一个epoch训练开始前将所有数据写入缓存服务器中,因此这使得第一个epoch时间较长。

缓存限制

  • 当前GeneratorDataset、PaddedDataset和NumpySlicesDataset等数据集类不支持缓存。其中,GeneratorDataset、PaddedDataset和NumpySlicesDataset属于GeneratorOp,在不支持的报错信息中会呈现“There is currently no support for GeneratorOp under cache”。

  • 经过batch、concat、filter、repeat、skip、split、take和zip处理后的数据不支持缓存。

  • 经过随机数据增强操作(如RandomCrop)后的数据不支持缓存。

  • 不支持在同个数据管道的不同位置嵌套使用同一个缓存实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明志刘明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值