亚马逊AWS官方博客

在 Amazon EKS 上使用 DeepSpeed 进行 Llama 2 分布式训练

机器学习模型,尤其是深度学习模型,近年来变得越来越复杂。在单台机器上训练这些模型,特别是大型语言模型,可能会非常低效,甚至由于单个设备计算资源和内存容量的限制而无法实现。这就是分布式训练发挥作用的地方。分布式训练是一种允许您将训练工作负载分散到多台机器或设备(如 GPU)上的技术。分布式训练有几种方法,包括数据并行、模型并行。目前,有许多分布式训练框架,如 Horovod、PyTorch 分布式数据并行(DDP)、TensorFlow 分布式策略、Megatron 和 DeepSpeed。这些框架处理工作节点之间的通信、梯度聚合和其他必要操作,使用户可以专注于模型架构和训练逻辑。在本文章中,我们将介绍如何使用 DeepSpeed 在亚马逊云科技的服务上进行 Llama 2 的分布式训练。

方案概览

在深入到方案之前我们需要了解一些知识:

  • DeepSpeed 是一个开源库,它能够高效地进行大型语言模型和其他深度学习模型的分布式训练。它提供了各种优化技术,例如 ZeRO(零冗余优化器),以减少内存消耗。
  • Kubeflow Training Operator 是一个 Kubernetes 原生项目,用于微调和针对机器学习模型做可扩展的分布式训练,这些模型可使用各种机器学习框架创建,如 PyTorch、TensorFlow、HuggingFace、JAX、DeepSpeed、XGBoost、PaddlePaddle 等。
  • Amazon EKS 是一个托管的 Kubernetes 服务,可在在亚马逊云上和本地数据中心中运行 Kubernetes。在云环境中,Amazon EKS 自动管理 Kubernetes 控制平面节点的可用性和可扩展性,这些节点负责调度容器、管理应用程序可用性、存储集群数据以及其他关键任务。
  • Amazon FSx for Lustre 提供完全托管的共享存储,具有流行的 Lustre 文件系统的可扩展性和性能。
  • Amazon Elastic Fabric Adapter (EFA) 是一种适用于 Amazon EC2 实例的网络接口,它使客户能够在 AWS 上大规模运行需要高水平节点间通信的应用程序。它定制的操作系统(OS)旁路硬件接口提升了实例间通信的性能,这对于扩展这些应用程序至关重要。EFA 支持远程直接内存访问(RDMA)协议。

安装步骤

开始训练之前有一些必要的安装步骤:

  1. 如果您没有 Amazon EKS 和 FSx for Lustre,请安装它们,这里有一个 Terraform 项目可以帮助您搭建环境,请注意的一点是如果您想自定义的 terraform-aws-modules/eks/aws 模块或者指定 EKS 版本。
    1. terraform-aws-modules/eks/aws 模块:在 data on EKS repo 的 ai-ml/jark-stack/terraform/eks.tf 中更新,请参考 Terraform 官方文档更新部署代码。
    2. EKS 版本:在 data on EKS repo 的 ai-ml/jark-stack/terraform/variables.tf 中更新,并且注意相关插件版本更新。
  2. 构建分布式训练的 Docker 镜像,包括 Cuda、DeepSpeed、EFA 插件、PyTorch 等组件。
  3. 安装 Kubeflow Training Operator,用于将分布式训练任务提交到 Amazon EKS。
  4. 准备使用 DeepSpeed 和 PyTorch 的 Llama 2 训练代码。
  5. 定义 AWS 训练 YAML 脚本并立即开始训练。

安装 Amazon EKS 和 FSx for Lustre

如果没有现成的环境,请按照 Amazon EKS 和 FSx for Lustre 的详细说明进行安装。我们还准备了一个 Terraform 项目,用于设置所需组件,包括 Amazon VPC、Amazon FSx for Lustre 和 Amazon EKS、EKS 插件(如 EFA 设备插件、NVIDIA 设备插件等)。这个 Terraform 项目基于 Data-on-EKS 项目开发。

Run the install script

使用提供的辅助脚本 install.sh 来运行 terraform init 和 apply 命令。默认情况下,该脚本会在 us-west-2 区域部署 EKS 集群。如需更改区域,请更新 variables.tf 文件。此时也可以更新任何其他输入变量或对 terraform 模板进行任何其他更改。

./install .sh

更新本地的 kubeconfig 文件以便能够访问新部署的 EKS 集群,请执行以下命令,如果您在 variables.tf 中更改了区域,请确保在上述命令中使用正确的区域。<集群名称>应替换为您的 EKS 集群的实际名称,这通常在 terraform 输出中可以找到。执行:

aws eks update-kubeconfig --name deepspeed-on-eks #or whatever you used for EKS cluster name

执行此命令后,kubectl 命令行工具将被配置为与您的 EKS 集群通信。您可以通过运行以下命令来验证连接:

kubectl get node

这应该会显示您的 EKS 集群中的节点列表:

同时,请检查集群中所有必要的 Pod 是否正常运行,特别是 nvidia-device-plugin 和 fsx-csi-controller。

安装 Kubeflow Training Operator

Training Operator 可以作为独立组件安装,运行以下命令来安装 Training Operator 控制平面的稳定版本:v1.8.1,如果您需要安装其他版本,只需将命令中的 v1.8.1 替换为您想要的版本号即可。

kubectl apply -k "github.com/kubeflow/training-operator.git/manifests/overlays/standalone?ref=v1.8.1"

这个命令将从 GitHub 仓库获取 Training Operator v1.8.1 的配置,并将其应用到您的 Kubernetes 集群中。安装完成后,让我们验证 Training Operator 是否已成功部署:

构建分布式训练的 Docker 镜像

构建用于分布式训练的 Docker 镜像涉及以下几个关键步骤:

  • 指定基础镜像:
    • 对于 LLM 训练,需要使用带有 CUDA 支持的 Python 基础镜像。在本仓库中,使用了 nvidia/cuda:11.8.0-devel-ubuntu22.04 镜像,该镜像提供了预装 NVIDIA CUDA 11.8.0 的 Ubuntu 22.04 环境。
    • 确保基础操作系统镜像与您的目标硬件兼容,并满足机器学习工作负载的要求。
  • 为您的 LLM 训练安装必要的插件,如 EFA 和 NCCL 插件,具体请参考 Get started with EFA and NCCL for ML workloads
    • 安装 Elastic Fabric Adapter(EFA)插件:能够在 AWS 上的 GPU 实例之间实现高性能通信。
    • 安装 NVIDIA 集合通信库(NCCL)插件:为深度学习工作负载优化了多 GPU 和多节点通信。
  • 机器学习库:
    • 安装所需的机器学习库,包括 DeepSpeed、PyTorch、Transformers、Accelerate、NumPy 和其他依赖项。
    • 特别注意这些库的版本,因为版本不匹配可能会导致兼容性问题。

当然,如果您已有一个指定了这些库正确版本的可用 Dockerfile,可以直接使用它来确保兼容性。以下是我的 Dockerfile,用于参考:

FROM nvidia/cuda:11.8.0-devel-ubuntu22.04

ARG EFA_INSTALLER_VERSION=1.29.1
ARG AWS_OFI_NCCL_VERSION=v1.7.4-aws
ARG NCCL_TESTS_VERSION=master
ARG NCCL_VERSION=2.18.6

RUN apt-get update -y
RUN apt-get remove -y —allow-change-held-packages \
libmlx5-1 ibverbs-utils libibverbs-dev libibverbs1 libnccl2 libnccl-dev

RUN rm -rf /opt/hpcx \
&& rm -rf /usr/local/mpi \
&& rm -f /etc/ld.so.conf.d/hpcx.conf \
&& ldconfig
ENV OPAL_PREFIX=

RUN DEBIAN_FRONTEND=noninteractive apt-get install -y —allow-unauthenticated \
git \
gcc \
vim \
kmod \
openssh-client \
openssh-server \
build-essential \
curl \
autoconf \
libtool \
gdb \
automake \
python3-distutils \
cmake \
apt-utils \
devscripts \
debhelper \
libsubunit-dev \
check \
pkg-config

RUN mkdir -p /var/run/sshd
RUN sed -i 's/[ #]\(.*StrictHostKeyChecking \).*/ \1no/g' /etc/ssh/ssh_config && \
echo " UserKnownHostsFile /dev/null" >> /etc/ssh/ssh_config && \
sed -i 's/#\(StrictModes \).*/\1no/g' /etc/ssh/sshd_config
ENV LD_LIBRARY_PATH /usr/local/cuda/extras/CUPTI/lib64:/opt/amazon/openmpi/lib:/opt/nccl/build/lib:/opt/amazon/efa/lib:/opt/aws-ofi-nccl/install/lib:/usr/local/lib:$LD_LIBRARY_PATH
ENV PATH /opt/amazon/openmpi/bin/:/opt/amazon/efa/bin:/usr/bin:/usr/local/bin:$PATH

RUN curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py \
&& python3 /tmp/get-pip.py \
&& pip3 install awscli pynvml


#################################################
## Install EFA installer
RUN cd $HOME \
&& curl -O https://efa-installer.amazonaws.com/aws-efa-installer-${EFA_INSTALLER_VERSION}.tar.gz \
&& tar -xf $HOME/aws-efa-installer-${EFA_INSTALLER_VERSION}.tar.gz \
&& cd aws-efa-installer \
&& ./efa_installer.sh -y -g -d —skip-kmod —skip-limit-conf —no-verify \
&& rm -rf $HOME/aws-efa-installer

###################################################
## Install NCCL
RUN echo "Installing NCCL_VERSION = v${NCCL_VERSION}-1"
RUN git clone https://github.com/NVIDIA/nccl -b v${NCCL_VERSION}-1 /opt/nccl \
&& cd /opt/nccl \
&& make -j $(nproc) src.build CUDA_HOME=/usr/local/cuda \
NVCC_GENCODE="-gencode=arch=compute_80,code=sm_80 -gencode=arch=compute_86,code=sm_86 -gencode=arch=compute_90,code=sm_90"

###################################################
## Install AWS-OFI-NCCL plugin
RUN apt-get install libtool autoconf cmake nasm unzip pigz parallel nfs-common build-essential hwloc libhwloc-dev libjemalloc2 libnuma-dev numactl libjemalloc-dev preload htop iftop liblapack-dev libgfortran5 ipcalc wget curl devscripts debhelper check libsubunit-dev fakeroot pkg-config dkms -y
RUN apt-get install python3-dev -y

RUN export OPAL_PREFIX="" \
&& git clone https://github.com/aws/aws-ofi-nccl.git /opt/aws-ofi-nccl \
&& cd /opt/aws-ofi-nccl \
&& git checkout ${AWS_OFI_NCCL_VERSION} \
&& ./autogen.sh \
&& ./configure —prefix=/opt/aws-ofi-nccl/install \
--with-libfabric=/opt/amazon/efa/ \
--with-cuda=/usr/local/cuda \
--with-mpi=/opt/amazon/openmpi/ \
--enable-platform-aws \
&& make -j $(nproc) && make install
###################################################
## Install fsdp

RUN mkdir -p /workspace/

WORKDIR /workspace


RUN pip3 install -U pip setuptools
RUN pip3 install fsspec==2023.1.0
RUN pip3 install huggingface_hub==0.17.0
RUN pip3 install huggingface
RUN pip3 install torch==1.13.0
RUN DS_BUILD_UTILS=1 DS_BUILD_FUSED_ADAM=1 pip3 install deepspeed==0.12.0
RUN pip3 install transformers==4.33.0
RUN pip3 install tqdm==4.66.1
RUN pip3 install peft==0.5.0
RUN pip install sentencepiece==0.1.99
RUN pip3 install tabulate
RUN pip3 install protobuf
RUN pip3 install python-etcd
RUN pip3 install datasets==3.0.1
RUN pip3 install numpy==1.26.4
RUN pip3 install accelerate==0.28.0

准备训练代码

训练代码使用 PyTorch,包含以下几个模块:

  • 参数解析:使用 argparse 解析命令行参数,允许用户指定各种超参数和训练目录
  • 对于分布式训练的 launcher/启动器的设置:代码使用 distributed.init_process_group 和 nccl 后端初始化分布式进程组,实现多 GPU 或多节点训练
  • 数据加载:使用 load_from_disk 从指定目录加载训练数据集,请指定您偏好的数据集
  • 加载模型和分词器
  • 训练参数配置和初始化 Transformer Trainer 对象
  • 模型训练和保存:执行模型训练并保存训练结果

具体代码如下:

from transformers import AutoModelForCausalLM, Trainer, TrainingArguments, AutoTokenizer
from transformers.models.llama.tokenization_llama import LlamaTokenizer
from datasets import load_from_disk,load_dataset
import random
import logging
import sys
import argparse
import os
import torch
import subprocess
import deepspeed
import torch.distributed as dist

if __name__ == "__main__":

    dist.init_process_group(backend='nccl')
    
    parser = argparse.ArgumentParser()

    # hyperparameters sent by the client are passed as command-line arguments to the script.
    parser.add_argument("--num_train_epochs", type=int, default=3)
    parser.add_argument("--per_device_train_batch_size", type=int, default=2)
    parser.add_argument("--per_device_eval_batch_size", type=int, default=4)
    parser.add_argument("--warmup_steps", type=int, default=100)
    #parser.add_argument("--eval_steps",type=int,default=5000)
    parser.add_argument("--learning_rate", type=str, default=2e-5)
    parser.add_argument("--evaluation_strategy",type=str,default="epoch")
    parser.add_argument("--gradient_accumulation_steps",type=int,default=4)
    parser.add_argument("--c",type=bool,default=False)
    #parser.add_argument("--logging_steps",type=int,default=5000)
    parser.add_argument("--save_steps",type=int,default=500)
    parser.add_argument("--save_strategy",type=str,default="steps")
    parser.add_argument("--save_total_limit",type=int,default=4)
    parser.add_argument("--model_max_length",type=int,default=512)
    parser.add_argument("--bf16",type=bool,default=True)
    #parser.add_argument("--deepspeed_config",type=str,default="/data/")
    # Data, model, and output directories
    parser.add_argument("--output_data_dir", type=str, default="/data/llmma/output_data")
    parser.add_argument("--output_dir", type=str, default="/data/data/llama2/output")
    parser.add_argument("--model_dir", type=str, default="/data/llmma/model")
    parser.add_argument("--n_gpus", type=str, default=8)
    parser.add_argument("--training_dir", type=str, default="/data/llmma/training")
    parser.add_argument("--test_dir", type=str, default="/data/llmma/test")
    
    parser = deepspeed.add_config_arguments(parser)

    args, _ = parser.parse_known_args()

    # Set up logging
    logger = logging.getLogger(__name__)

    logging.basicConfig(
        level=logging.getLevelName("INFO"),
        handlers=[logging.StreamHandler(sys.stdout)],
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )
    rank = dist.get_rank()

    world_size = dist.get_world_size()
    print(f"Rank: {rank}, World_size:{world_size}")    
    # load datasets
    train_dataset = load_from_disk(args.training_dir)
    #test_dataset = load_dataset(args.test_dir)

    logger.info(f" loaded train_dataset length is: {len(train_dataset)}")
    #logger.info(f" loaded test_dataset length is: {len(test_dataset)}")

    model_name_or_path = "meta-llama/Llama-2-7b-hf"

        
    model = AutoModelForCausalLM.from_pretrained(model_name_or_path,use_cache=False)
    
    tokenizer = LlamaTokenizer.from_pretrained(model_name_or_path, model_max_length=args.model_max_length)
    # Print additional special tokens
    print("BOS Token:", tokenizer.bos_token)
    print("EOS Token:", tokenizer.eos_token)
    print("Mask Token:", tokenizer.mask_token)
    print("Pad Token:", tokenizer.pad_token)
    print("Unknown Token:", tokenizer.unk_token) 


    # define training args
    training_args = TrainingArguments(
        output_dir = f"{args.output_dir}/checkpoint",
        num_train_epochs=args.num_train_epochs,
        per_device_train_batch_size=args.per_device_train_batch_size,
        per_device_eval_batch_size=args.per_device_eval_batch_size,
        warmup_steps=args.warmup_steps,
        evaluation_strategy="no",         #just for test
        logging_dir=f"{args.output_dir}/logs",
        logging_steps = 10,
        gradient_checkpointing=True,
        learning_rate=float(args.learning_rate),
        deepspeed=args.deepspeed_config,
        #save_steps = args.save_steps,
        save_strategy = "epoch",          #just for test
        save_total_limit = args.save_total_limit,
        remove_unused_columns=False,
        save_on_each_node = True,
        gradient_accumulation_steps = args.gradient_accumulation_steps,
        fp16=True,  
        bf16=False,  # Use BF16 if available
    )
  
    # create Trainer instance
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
    #    eval_dataset=test_dataset,
#        tokenizer=tokenizer
    )

    # train model
    trainer.train()

 
    print("------saving model!-----")
    save_model_dir = f"{args.output_dir}/model"
    tokenizer.save_pretrained(save_model_dir)
    trainer.save_model(save_model_dir)
    print("------model is saved!-----")

自定义训练 YAML 和开始训练

在本篇 Blog 中,我们使用每节点 4 个GPU,一共 2 个节点。请替换以下内容:

  • 将镜像 URL 替换为 REPLACE_WITH_YOUR_TRAINING_IMAGE_REPO_LINK
    containers:
         - name: pytorch
         image: <REPLACE_WITH_YOUR_TRAINING_IMAGE_REPO_LINK>
  • 替换为您的 Hugging Face token,因为 Llama 需要访问权限,当然如有需要,可以更改训练数据、测试数据目录和 DeepSpeed 配置的相关内容。
    huggingface-cli login --token <REPLACE_WITH_YOUR_HUGGINGFACE_TOKEN> && 
    torchrun 
    --nproc_per_node 4 
    --nnodes 2 /data/data/llama2/train.py 
    --per_device_eval_batch_size 1 
    --per_device_train_batch_size 1 
    --model_max_length 2048 
    --distributed-backend nccl 
    --learning_rate 0.00001 
    --training_dir /data/data/llama2/train 
    --test_dir /data/data/llama2/test 
    --deepspeed 
    --deepspeed_config /data/data/llama2/ds_z3_fp16.json 
    --num_train_epochs 1"
  • 具体 YAML 文件如下,其他的主要的内容包括:
    • Master 和 Worker 都需要指定 GPU 数量
    • FI_EFA_USE_DEVICE_RDMA 的参数值指定为 1,用于开启 RDMA
    • command 中为提交的训练任务命令
      apiVersion: "kubeflow.org/v1"
      kind: PyTorchJob
      metadata:
        name: llama2
      spec:
        pytorchReplicaSpecs:
          Master:
            replicas: 1
            restartPolicy: OnFailure
            template:
              spec:
                volumes:
                  - name: shmem
                    hostPath:
                      path: /dev/shm
                  - name: persistent-storage
                    persistentVolumeClaim:
                      claimName: fsx-static-pvc
                containers:
                  - name: pytorch
                    image: <REPLACE_WITH_YOUR_TRAINING_IMAGE_REPO_LINK>
                    imagePullPolicy: Always
                    resources:
                      requests:
                        nvidia.com/gpu: 4
                          #vpc.amazonaws.com/efa: 1
                      limits:
                        nvidia.com/gpu: 4
                          #vpc.amazonaws.com/efa: 1
                    env:
                      - name: LOGLEVEL
                        value: DEBUG
                      - name: NCCL_DEBUG
                        value: INFO
                      - name: TORCH_NCCL_ASYNC_ERROR_HANDLING
                        value: '1'
                      - name: OMP_NUM_THREADS
                        value: '1'
                      - name: FI_PROVIDER
                        value: 'efa'
                      - name: NCCL_PROTO
                        value: 'simple'
                      - name: FI_EFA_USE_DEVICE_RDMA
                        value: '1'
                      - name : NCCL_IGNORE_DISABLED_P2P
                        value: '1'
                    command: ["/bin/sh", "-c", "huggingface-cli login --token <REPLACE_WITH_YOUR_HUGGINGFACE_TOKEN> && torchrun --nproc_per_node 4 --nnodes 2 /data/data/llama2/train.py --per_device_eval_batch_size 1 --per_device_train_batch_size 1 --model_max_length 2048 --distributed-backend nccl --learning_rate 0.00001 --training_dir /data/data/llama2/train --test_dir /data/data/llama2/test --deepspeed --deepspeed_config /data/data/llama2/ds_z3_fp16.json --num_train_epochs 1"]
                    volumeMounts:
                      - name: shmem
                        mountPath: /dev/shm
                      - name: persistent-storage
                        mountPath: /data
          Worker:
            replicas: 1
            restartPolicy: OnFailure
            template:
              spec:
                volumes:
                  - name: shmem
                    hostPath:
                      path: /dev/shm
                  - name: persistent-storage
                    persistentVolumeClaim:
                      claimName: fsx-static-pvc
                containers:
                  - name: pytorch
                    image: <REPLACE_WITH_YOUR_TRAINING_IMAGE_REPO_LINK>
                    imagePullPolicy: Always
                    resources:
                      requests:
                        nvidia.com/gpu: 4
                          #vpc.amazonaws.com/efa: 1
                      limits:
                        nvidia.com/gpu: 4
                          #vpc.amazonaws.com/efa: 1
                    env:
                      - name: LOGLEVEL
                        value: DEBUG
                      - name: NCCL_DEBUG
                        value: INFO
                      - name: TORCH_NCCL_ASYNC_ERROR_HANDLING
                        value: '1'
                      - name: OMP_NUM_THREADS
                        value: '1'
                      - name: FI_PROVIDER
                        value: 'efa'
                      - name: NCCL_PROTO
                        value: 'simple'
                      - name: FI_EFA_USE_DEVICE_RDMA
                        value: '1'
                      - name : NCCL_IGNORE_DISABLED_P2P
                        value: '1'
                    command: ["/bin/sh", "-c", "huggingface-cli login --token <REPLACE_WITH_YOUR_HUGGINGFACE_TOKEN> && torchrun --nproc_per_node 4 --nnodes 2 /data/data/llama2/train.py --per_device_eval_batch_size 1 --per_device_train_batch_size 1 --model_max_length 2048 --distributed-backend nccl --learning_rate 0.00001 --training_dir /data/data/llama2/train --test_dir /data/data/llama2/test --deepspeed --deepspeed_config /data/data/llama2/ds_z3_fp16.json --num_train_epochs 1"]
                    volumeMounts:
                      - name: shmem
                        mountPath: /dev/shm
                      - name: persistent-storage
                        mountPath: /data

通过 kubectl apply -f deepspeed-llama2-job.yaml 执行上述的 YAML 文件后,训练任务成功提交到 Kubernetes 集群。系统会自动调度并在指定的 GPU 节点上启动训练容器。在初始化阶段,您将观察到如下日志输出,这些日志展示了分布式训练环境的建立过程:

  • 节点 1
  • 节点 2

任务提交成功后,我们可以看到 GPU 的使用情况了。如下截图只是个示例,这是通过登录到 EC2 节点上,使用 nvidia-smi 命令查看 GPU 的实时指标,在真正的生产系统中,可以通过 Kubernetes 集成监控工具或者 CloudWatch 等进行更全面的监控。

总结

本方案成功地在 Amazon EKS 上使用 DeepSpeed 框架训练了 Llama 2 大语言模型,结合 Kubeflow 进行任务编排,DeepSpeed 的 ZeRO 优化器有效降低内存占用, 利用 EFA 网络加速节点间通信,实现了高效的 GPU 资源调度和低延迟数据传输,为企业级模型训练提供了可扩展的解决方案。


*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您了解行业前沿技术和发展海外业务选择推介该服务。

本篇作者

冯秋爽

亚马逊云科技解决方案架构师,负责跨国企业级客户基于亚马逊云科技的技术架构设计、咨询和设计优化工作。在加入亚马逊云科技之前曾就职于 IBM、甲骨文等 IT 企业,积累了丰富的程序开发和数据库的实践经验。

梁宇辉

亚马逊云科技机器学习产品技术专家,负责基于亚马逊云科技的机器学习方案的咨询与设计,专注于机器学习的推广与应用,深度参与了很多真实客户的机器学习项目的构建以及优化。对于深度学习模型分布式训练,推荐系统和计算广告等领域具有丰富经验。

郭松

亚马逊云科技解决方案架构师,负责企业级客户的架构咨询及设计优化,同时致力于 AWS IoT 和存储服务在国内和全球企业客户的应用和推广。加入亚马逊云科技之前在 EMC 研发中心担任系统工程师,对企业级存储应用的高可用架构,方案及性能调优有深入研究。