# pdcs-hw3 **Repository Path**: TheTrickboy/pdcs-hw3 ## Basic Information - **Project Name**: pdcs-hw3 - **Description**: 并行与分布计算作业3 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-12-17 - **Last Updated**: 2023-12-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 并行与分布计算作业三 > 姓名 :王震彬 学号 :20337244 ## LLAMA && AirLLM ### 1. 电脑配置 1. 操作系统信息:Ubuntu20.04.4 LTS 2. 处理器信息:Intel® Core™ i9-9900K CPU @ 3.60GHz × 16 3. 内存信息:48.0GB 4. 显卡信息:NVIDIA GeForce RTX 2080 SUPER/PCIe/SSE2 ### 2. LLAMA2 #### 下载链接 [Airllm](https://gitee.com/jswrt/Anima) 所能使用的模型为 LLaMA2 的 `hf` 模型, 同时 `meta` 官方开放下载的 `LLaMA2` 只有 7B, 13B, 70B 三个, 这里提供对应的下载链接。 - [Meta官方版](https://ai.meta.com/llama/) ![meta_llama_model_download](image/meta_model_download.png) - [HuggingFace仓库](https://huggingface.co/meta-llama) ![meta_llama_repo](image/meta_llama.png) 登录并注册账号后发现官方下载链接不支持中国区的下载,目前中国区下载通常以一些人在网上发布的迅雷分享或者其他网盘分享为主,但是由于需要下载的模型非常大,下载时通常面临需要开通各种网盘权限(向各个平台充值),或者下载时间特别长的问题,至今仍未发现一种比较好的快速的解决问题的办法,给本次作业的完成设置了很多不必要的非技术阻碍。 经过解决重重阻碍,终于下载到 `meta-llama/Llama-13b` 的模型,这里需要注意的是,模型后缀不带`hf` 不能直接用于`Airllm`的测试,需要用`transforms`源代码下的`convert_llama_weights_to_hf.py`将对应模型按照如下命令将 `meta-llama/Llama-13b`转换成`meta-llama/Llama-13b-hf`, 1. 第一步检查Python的版本是不是3.8,这是由于后面要用到python依赖包中的transformers的依赖,而transformers的版本需要且必须是达到 >=4.31.0,而python3.8之前的版本不支持。 ```bash # 第一步检查Python的版本是不是 >= 3.8 python --version ``` 2. 第二步下载transformers源代码,将之前的非hf模型转换成hf模型,(ps: 这里的hf应该是huggingface的缩写吧。) ```bash git clone https://github.com/huggingface/transformers.git cd transformers/src/transformers/models/llama python convert_llama_weights_to_hf.py --input_dir 你的模型路径/llama-2-13b --model_size 13B --output_dir 你的指定路径/llama-2-13b-hf ``` ![convert_llama_weights_to_hf](image/convert_llama_weights_to_hf.png) 至此,模型的前期下载工作已经全部完成。 **以下实验所使用的均为 13B 的模型,该模型约49GB。** ### 3. 尝试在CPU运行 AirLLM #### 3.1 开始实验 首先安装程序包: ```python pip install airllm ``` 此时再python依赖中多出了这样的两个包。我们可以看到airllm的源代码,取其中的airllm.py可以开始本次实验。也可以下载[老师仓库](https://gitee.com/jswrt/Anima)中的代码运行`inference_example.py` 查看效果展示。我们下面来看一下这部分代码。该代码像传统的Transformer模型一样执行分层推理,代码如下。为了在cpu上运行,我们主要改动了第6行代码`model = ..`和第19行代码`input_tokens..` ```python from airllm import AirLLMLlama2 import torch import time MAX_LENGTH = 128 # could use hugging face model repo id: model = AirLLMLlama2("/home/trickboy/Desktop/llama-2-13b-hf", device='cpu', dtype=torch.float32) input_text = [ 'What is the capital of United States?', ] input_tokens = model.tokenizer(input_text, return_tensors="pt", return_attention_mask=False, truncation=True, max_length=MAX_LENGTH, padding=True) start_t = time.time_ns() generation_output = model.generate( input_tokens['input_ids'], max_new_tokens=20, use_cache=True, return_dict_in_generate=True) end_t = time.time_ns() output = model.tokenizer.decode(generation_output.sequences[0]) print(output) print(f'Took {end_t - start_t}ns') ``` #### 3.2 实验结果 ![3.2.1](image/ass12.png) ![3.2.2](image/ass11.png) 最后结果展示了 `The capital of United States is Washington, D.C.` 共花费时间 `193360064437` ns,且每秒大概迭代4.6it。 ### 4. 尝试在GPU运行 #### 4.1 实验代码 为了在GPU上运行。我们同样修改`run.py`中的第6和第19行代码,修改成如下所示: ```python # first modification model = AirLLMLlama2("/home/trickboy/Desktop/llama-2-13b-hf", device="cuda", dtype=torch.float32) # second modification generation_output = model.generate( input_tokens['input_ids'].cuda(), max_new_tokens=20, use_cache=False, return_dict_in_generate=True) ``` #### 4.2 实验结果 ![3.2.1](image/ass22.png) ![3.2.2](image/ass21.png) 最后结果展示了 `The capital of United States is Washington, D.C.` 花费时间 `149101421700` ns,总花费时间相比于CPU有很大的减少。但是就单层来看,就单层来看,每秒大概迭代6.0it。每一层的时间相比CPU有所减少。 #### 4.3 GPU 与 CPU对比的实验结果分析 性能有提升。且比较明显。 使用CPU时,模型参数读入内存后还需要从内存复制到缓存, 而使用GPU时,模型参数读入内存后还需要从内存复制到显存, 感觉两者在上述情境下的IO时间并没有太大差异,而之所以GPU的总得所用时间会比CPU要少,其性能的提升主要是体现在GPU的加速上。 ### 5. 尝试从内存读入(预先读入内存) #### 5.1 实验代码 为了预先读入内存,我们修改`airllm.py`,设计如下所示的两个函数,在整个模型评估开始之前,首先将整个模型参数读入内存,程序上的具体操作是用`load_file`函数将整个file都load进一个变量 `layer_state_dict`中,后面需要哪一个层直接从变量`layer_state_dict`中获得即可。最终由于有多个file,我们最终将所有的`file`的torch参数都保存在`load_layer_to_device_output`中以保存在内存中。这里的内存应该被理解为虚拟内存。下面,我们关注到有一个函数`load_file`,该函数可以将一个safetensor类型的文件转换成torch的形式,并返回一个完整的字典类型。 ```python def load_file(filename: Union[str, os.PathLike], device="cpu") -> Dict[str, torch.Tensor]: """ Loads a safetensors file into torch format. Args: filename (`str`, or `os.PathLike`): The name of the file which contains the tensors device (`Dict[str, any]`, *optional*, defaults to `cpu`): The device where the tensors need to be located after load. available options are all regular torch device locations Returns: `Dict[str, torch.Tensor]`: dictionary that contains name as key, value as `torch.Tensor` Example: \```python from safetensors.torch import load_file file_path = "./my_folder/bert.safetensors" loaded = load_file(file_path) \``` """ result = {} with safe_open(filename, framework="pt", device=device) as f: for k in f.keys(): result[k] = f.get_tensor(k) return result ``` 受到该函数的启发我们将代码做如下修改,并在`run.py`开始时调用`Allama2Model.pre_read_to_mem`函数: ```python def load_layers_to_memory(local_path, layer_name): # 将整个文件读入内存的原理: # 将一个文件中的层都放在layer_state_dict中那么 # 在程序运行的过程中,可以直接从内存的layer_state_dict中读取每一层 layer_state_dict = load_file(Path(local_path) / (layer_name + ".safetensors"), device="cpu") return layer_state_dict class Allama2Model: ... def load_a_layer_to_device(self, layer_name): # 从内存中的整个layer_state_dict中获取一个文件中每一层 state_dict = load_layers_to_memory(self.checkpoint_path, layer_name) return state_dict def pre_read_to_mem(self): for i, (layer_name, layer) in tqdm(enumerate(zip(self.layer_names, self.layers)), desc=self.running_device, total=len(self.layers)): load_layer_to_device_output = self.load_a_layer_to_device(layer_name) self.state_dicts[layer_name] = load_layer_to_device_output ``` #### 5.2 实验结果 ![3.2.1](image/ass32.png) ![3.2.2](image/ass31.png) 最后结果展示了 `The capital of United States is Washington, D.C.` 花费时间 `139482371080` ns,时间相比于CPU有减少,而和GPU的推理速度差不多。 #### 5.3 结果分析 性能较之前CPU、GPU有提升。但是较GPU提升不明显。 **首先需要说明的一点是,由于操作系统的虚拟内存机制会使得高级语言认为内存比实际上的物理内存大得多** ,将模型参数全部load进内存中虽然会在理论上为读取模型参数时带来时间上的便利,但是由于linux系统的虚拟内存机制,内存中剩余的部分如果不足时同样会在程序其他部分的运行过程中有频繁的页面缓入缓出的操作,带来时间上的消耗,从而操作系统实际上并不会将所有的模型参数都读入实际物理内存中,并在程序运行的过程中一直存储着,因此可能性能的提升不是会特别明显。 ### 6. 尝试异步预先把每层的参数从内存读入GPU #### 6.1 实验代码 为了在实验第5节的基础上增加异步的特征,我们再次修改`airllm.py`。 - **锁页内存(Pinned Memory/PageLocked Memory)**,通常我们的主机处理器是支持虚拟内存系统的,即使用硬盘空间来代替内存。大多数系统中虚拟内存空间被划分成许多页,它们是寻址的单元,页的大小至少是4096个字节。虚拟寻址能使一个连续的虚拟地址空间映射到物理内存并不连续的一些页。如果某页的物理内存被标记为换出状态,它就可以被更换到磁盘上,也就是说被踢出内存了。如果下次需要该页了,则重新加载到内存里。显然如果这一页切换的非常频繁,那么会浪费不少时间。锁页(pinned page)是操作系统常用的操作,就是为了使硬件外设直接访问CPU内存,从而避免过多的复制操作。被锁定的页面会被操作系统标记为不可被换出的,所以设备驱动程序给这些外设编程时,可以使用页面的物理地址直接访问内存,CPU也可以访问上述锁页内存,但是此内存是不能移动或换页到磁盘上的。另外,在GPU上分配的内存默认都是锁页内存,这只是因为GPU不支持将内存交换到磁盘上 - **Non-Blocking Streams**, 在CUDA里, "Stream"是指一系列的操作,这些操作按照主机代码发出的顺序在设备上执行。同一个Stream里的操作是按顺序执行的,而不同Stream里的操作可以交错执行,并且在可能的情况下,它们甚至可以并发执行。stream有很多种,无特殊指定的话使用的就是默认stream(default stream,也称作 null stream)。它和其他stream的区别就在于:1)如果其他stream上的操作没结束,null stream就不会开始; 2)在device上的其他stream要开始之前,null stream必须先完成。所以说null stream是设备相关操作的同步流(synchronizing stream)。 ```python # 导入异步预先读入工具 from torch.utils.data._utils.pin_memory import pin_memory from torch.cuda.streams import Stream ``` 在第5步的基础上我们将代码做如下修改: ```python class Allama2Model: def __init__(self, model_local_path_or_repo_id, device="cuda:0", dtype=torch.float16, max_seq_len=512, layer_shards_saving_path=None): self.stream = Stream() ... # 使用torch中的pin_memory函数在这里异步预先读入显存 def pre_load_weights_with_cuda(self , checkpoint_path , layer_key): with torch.cuda.stream(self.load_worker): # 锁页内存 self.preloaded_weights = pin_memory(load_layer(checkpoint_path, layer_key)) def get_preloaded_weights(self, checkpoint_path , next_layer_key , last_layer): # 异步预先读入显存 torch.cuda.current_stream().wait_stream(self.stream) loaded_weights = self.preloaded_weights ``` #### 6.2 实验结果 ![3.2.1](image/ass42.png) ![3.2.2](image/ass41.png) 最后结果展示了 `The capital of United States is Washington, D.C.` 花费时间 `139221010786` ns,时间相比于CPU和GPU性能提升不明显。有几次测试结果相比前面实验在时间上反而略有延长。 #### 6.3 结果分析 性能较之前CPU有提升。但是较其他提升不明显。 本实验相比预先读入内存提升性能两个关键技术点主要是在: - 异步 - 预先读入显存 从单个运行任务的时间来看,处理速度可能会变快,而由于一会GPU需要运行计算程序,在计算过程中可能又需要接受异步指令,将部分数据异步读入显存,这个过程将涉及到频繁的上下文切换,这样也会使得GPU存在比较大的性能瓶颈。特别是当GPU显存的大小带来的收益比较小时,反而会使得整体花费的时间要慢一些。