1. 概述
大模型目前处于风口,公有云上的客户选择的训练的平台多数都是基于PAI,作为公有云的一名客户的云产品售后技术支持人员,对PAI核心模块功能的了解迫在眉睫。本次实践涉及PAI的DSW、DLC以及EAS三个模块,分别涉及了模型的可视化分析,容器训练以及服务部署,以最近比较火的清华开源的ChatGLM为例,对生成式的大语言模型以及PAI的平台做了简单的实践,旨在根据
2. on DSW
白话DSW(个人理解):
- 资源弹性及监控友好(cu和显卡的不同规格,cpu、内存、gpu使用率的一站式监控)
- 数分、算法、开发友好(现成的镜像+jupyterlab+webIDE)
- 底层可达(terminal个性化开发运维)
2.1. 创建dsw实例
官方文档:https://help.aliyun.com/document_detail/163684.html?spm=a2c4g.465473.0.0.307838d0aIlBiY
强烈推荐挂载数据集(NAS、某些场景或者经济条件限制下oss也可),不同实例之间可以共享模型文件,一次下载,多次使用,省去git clone github和huggingface断连的烦恼。
2.2. conda环境构建
有的时候不同的github项目依赖的python env不同,通过conda的不同虚拟环境进行管理,使用的时候方便快捷。dsw镜像默认在路径:/home/pai/bin下存在一个conda环境,可以使用该环境,也可以通过如下的方式下载最新版或者miniconda使用
# 下载最新版本的miniconda wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh # 安装 一路回车即可 sh Miniconda3-latest-Linux-x86_64.sh # 初始化conda环境,会自动配置PATH保证不同环境激活的是专用的py环境 conda init # 重启kernel exit # 查看虚拟环境列表 conda env list # 创建 glm6b专用的虚拟环境 conda create -n glm6b python=3.9 # 激活 glm6b的环境 conda activate glm6b
2.3. 注册自己的conda环境到jupyterlab
# 通过ipykernel将环境注册到jupyter中 conda install -n glm6b ipykernel # 或 pip install ipykernel # 注册当前环境 python -m ipykernel install --user --name glm6b --display-name "glm6b" #验证 import sys; print('Python %s on %s\nsys path:\n%s' % (sys.version, sys.platform, sys.path))
2.4. chatGLM体验
2.4.1. 项目地址
资源受限,简单实践就采用相对较小的6B的模型:
github:https://github.com/THUDM/ChatGLM-6B
土豪看这个:https://github.com/THUDM/GLM-130B
2.4.2. 项目环境准备
# 通过git下载项目代码和模型文件 # 更新apt-get库 sudo apt-get update # 下载git 以及较大模型文件所依赖的 git-lfs sudo apt-get install git sudo apt-get install git-lfs # 初始化 git init git lfs install mkdir glm # 下载项目代码 git clone https://github.com/THUDM/ChatGLM-6B # 安装项目依赖 python -m pip install -r requirements.txt mkdir models # 下载模型文件 git clone https://huggingface.co/THUDM/chatglm-6b # 模型文件比较大,20G,可以用下面这个看下下载速度 sudo apt-get install bwm-ng bwm-ng
2.4.3. 测试体验
2.4.3.1. jupyterlab
# 官网样例 import os import torch from transformers import AutoTokenizer, AutoModel model_path = "/mnt/workspace/glm/models/chatglm-6b" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModel.from_pretrained(model_path, trust_remote_code=True).half().cuda() response, history = model.chat(tokenizer, "你好", history=[]) print(response)
2.4.3.2. webui
# 更改一下脚本里面的模型加载路径,改为从本地加载(上面通过git在huggingface上已经做了下载),hugging face有时候网络环境不太好 python web_demo.py # Running on local URL: http://127.0.0.1:7860 # 直接点击日志弹出的url,dsw会自动进行公网代理的跳转,很丝滑
2.4.3.3. 简单了解一下这个模型
模型参数量 6B,大模型上来后对于B和M应该都有了更深刻的记忆,这个6B就是60E+参数量。
模型结构如下图
- word_embeddings:词嵌入。白话:文字转向量
- transformer:这部分一共28块,每个部分包括如下几层:
- 所有带norm的都是在做类归一化,减均值除标准差,与我们平时说的BN层(BatchNormalization )作用一致,与网络结构中的bias残差共同防止反向传播的时候梯度爆炸和消失的问题
- attention:注意力机制。白话:学习我们输入文本的上下文关系,有句比较经典的话就是attention is all your need
- mlp:多层感知机。白话:对输出进行非线性变换
- lm_head:线性输出层。白话:输出每个字的概率,看这个词表应该是130528个
2.4.4. fine-tune
场景化微调的方案很多,前2年的方法adapter、prefix、lora都能以较少的待学习的参数量达到某个场景垂域微调的效果,而最近微软的deepspeed框架可以用于全参数训练。
这次实践采用的是github推荐的p-tuning方法,具体原理和prefix tuning类似。
cd /mnt/workspace/glm/ChatGLM-6B/ptuning # 看一下官网给的训练脚本 vim tran.sh # 里面需要根据自己的场景,更改或者重新设定下如下参数 # sequence长度(PRE_SEQ_LEN) # 学习率(LR) # GPU(CUDA_VISIBLE_DEVICES) # 训练集(tran_file) # 验证集(validation_file) # prompt字段(prompt_column) # 返回字段(response_column) # 模型名称或者文件路径(model_name_or_path) # 输出文件目录(output_dir) # 量化位(quantization_bit)
# fine-tune数据集下载 wget -O AdvertiseGen.tar.gz https://cloud.tsinghua.edu.cn/f/b3f119a008264b1cabd1/?dl=1 # 解压 tar -xzvf AdvertiseGen.tar.gz # 训练数据
sh dsw.sh /mnt/workspace > ./dsw/log.out.20230615 & # 可以看到训练日志中 有加载模型,打印训练超参数,打印样例数据,训练的loss,学习率和epoch的进度 # ↓训练输出的文档中会包含每次的cp,这个和flink的sp或者cp是一致的,异常failover后可以从这里恢复,不用再从头开训
2.4.5. 训后模型加载
# 导入相关模块 import os import torch from transformers import AutoConfig, AutoModel, AutoTokenizer # 导入原始模型 model_path = "/mnt/workspace/glm/models/chatglm-6b" # 导入tokenizer tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) # 添加prefix层的config config = AutoConfig.from_pretrained(model_path, trust_remote_code=True, pre_seq_len=128) # 加载原始模型 model = AutoModel.from_pretrained(model_path, config=config, trust_remote_code=True) # 加载checkpoint的训练结果 prefix_state_dict = torch.load(os.path.join("/mnt/workspace/model/ChatGLM-6B/ptuning/output/adgen-chatglm-6b-pt-128-2e-2/checkpoint-3000", "pytorch_model.bin")) # 导入prefix encoder new_prefix_state_dict = {} for k, v in prefix_state_dict.items(): new_prefix_state_dict[k[len("transformer.prefix_encoder."):]] = v model.transformer.prefix_encoder.load_state_dict(new_prefix_state_dict)
前后对比
2.4.6. 模型推理逻辑-WebIDE的debug用法
这部分实践旨在两方面:
- 通过WebIDE来调试脚本,在场景化fine-tune的过程中避免不了的处理数据和做一些工程性的pipline,通过该功能可以让我们迅速定位工程脚本不符合预期的问题
- 具体看一下ChatGLM是怎么将回答生成出来的,推荐一个大佬的文档:https://zhuanlan.zhihu.com/p/617929228。从DEBUG的过程中我们可以看出,ChatGLM作为GPT家族的模型之一,每次模型预测的是:当前输入的文本(我们的问题),下一个词是什么的概率,13万词表中取概率最大的token,并添加到输入文本后,直到遇到停止符或者指标小于某个阈值。
例:
问题:FLINK是什么
模型生成:
- F
- FLINK
- FLINK是一种
- FLINK是一种高效的
- FLINK是一种高效的流计算
- ……
2.5. LangChain-GLM体验
2.5.1. 基础知识与原理
https://github.com/imClumsyPanda/langchain-ChatGLM
白话理解:大模型参数量大,训练成本高,周期长。某些场景下很多知识内容可能是天级别的,那为了保证该场景下大模型回答的准确性,可以通过langchain的方式将知识库与大模型结合起来,在使用大模型进行提问时,到知识库中搜索相关内容,通过组合实时的知识内容与问题形成新的prompt,利用大模型的摘要汇总能力,进行问题的回答。
2.5.2. web前端测试体验
知识库内容(瞎写的):“大板作为日本的首都有着悠久的历史。”
可以明显的看出上传知识库后,大模型的回答发生了变化。
2.5.3. 本地知识库集成及部分代码逻辑简要分析
# client demo入口代码 cli_demo.py # 初始化基于本地文件做QA的主类,包含了llm,embedding等 - local_doc_qa = LocalDocQA() # 初始化向量库 - local_doc_qa.init_knowledge_vector_store(filepath) ## 通过上传的本地知识库地址加载知识,支持 .md .txt .pdf .jps .png以及非结构化的知识文档,详见图1 -- load_file(filepath, sentence_size) # 调用方法根据知识库,生成回答 - local_doc_qa.get_knowledge_based_answer() ## 加载存储的向量库 -- load_vector_store(vs_path, self.embeddings) ## 得到和问题最相似的topk个知识内容 -- related_docs_with_score = vector_store.similarity_search_with_score(query, k=self.top_k) ## 判断知识库中内容是否有被检索出来 -- if len(related_docs_with_score)>0: ### 根据相关联的知识库和问题生成promt,详见图2 --- true: promt = generate_prompt(related_docs_with_score, query) #### 根据预设的promt将知识内容和问题组合,详见图3,图4 ---- PROMPT_TEMPLATE.replace("{question}", query).replace("{context}", context) ### 知识库中没有内容,直接将问题输入大模型 --- false: promt = query ## 输入到大模型进行回答的生成 -- self.llm.generatorAnswer(prompt)
这里涉及到一个向量检索的应用,目前阿里云上adb,es都是可以的,据了解,开源的FAISS,milvus性能也很强劲可以关注关注。
3. on DLC
白话DLC(个人理解):
- 一站式容器训练平台,高效,开箱即用
- 无缝衔接ACR,同步历史训练镜像
- 底层弹性资源组,灵竣等
- 避坑-shell命令不要加 & 会导致任务直接完成,这个应该是容器的行为
- 安装python依赖
- 可以手动copy进去,pai会把内容处理下放到tmp/requirements.txt下执行;
- 也可以直接指定requirements.txt在数据集或者镜像中的路径,pai会拼好命令执行;
- 最好可以自己在执行命令的shell里面做自定义安装,如果镜像涉及多个python环境,可以激活/指定某个做pip,否则pai会以默认环境变量第一个python环境做安装和执行,有可能有版本代码不兼容的问题
- 写训练日志
- 命令重定向到日志输出的时候不要忽略python -u和flush参数,最好重写重要节点的logger类以保证实时获取模型训练状态,或者使用tensorboard来监控模型损失等指标
4. on EAS
白话EAS(个人理解):一站式,高效,高性能的平台,把自己训好的模型部署成一个http接口,兼顾版本管理、预热、压测等实用的功能。
几种方式:
- 自定义镜像(ACR+自己开发的HTTP接口,诸如flask,tornado,Django等,我觉得是企业已有老服务平迁的神器,只用改下请求的域名和token即可)
- 通用镜像(TensorFlow Serving这种开源的部署深度学习模型的镜像)
- 模型+processor(官网推荐,python用来快速训练和分析,c++保证服务性能)
- 预制(TorchModel.save()等类似接口保存下来的模型或者torch.jit.trace(TorchModel)或者torch.jit.scipt(TorchModel)保存下来的模型)
- 自定义(兼容cpp、java、python可以自定义加入业务逻辑、自定义请求数据和日志的)
4.1. eascmd
PAI-EAS的官方客户端,可以查看、发布、更新服务
# 初始化ak ./eascmd64 config -i <access_id> -k <access_key> # 查看当前endpoint 下ak对应的eas服务的list ./eascmd list
4.2. pysdk-allspark
PAI-EAS平台提供的以C++为底座的高效、便捷的HTTP服务框架,我们可以在相应的库下看到C++的头文件以及不同平台编译好的包
# 初始化环境。 # 输入Python版本(默认为3.6版本),系统会自动创建Python环境ENV目录、预测服务代码模板app.py及服务部署模板app.json。 ./eascmd pysdk init ./pysdk_demo # 打包 eascmd pysdk pack ./demo # 或者 手动打成zip或者tar.gz的包 tar -zxvf pysdk_demo.tar.gz ./pysdk_demo
通过该框架,我们可以快速的搭建一个HTTP服务接收自定义数据格式的请求,自定义模型的预测以及自定义格式的返回。以ChatGLM为例,我们在initialize方法中加载模型,pre_process方法中预处理请求数据,process方法中进行模型预测,post_process方法中进行后处理并返回,以下是一个简单的样例实现:
# -*- coding: utf-8 -*- import allspark from transformers import AutoTokenizer, AutoModel import json class MyProcessor(allspark.BaseProcessor): """ MyProcessor is a example you can send message like this to predict curl -v http://127.0.0.1:8080/api/predict/service_name -d '2 105' """ def initialize(self): """ load module, executed once at the start of the service do service initialization and load models in this function. """ # 非量化后的模型 self.model_path = "/mnt/data/glm/models/chatglm-6b" self.tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True) self.model = AutoModel.from_pretrained(self.model_path, trust_remote_code=True).half().cuda() self.model.eval() print('model init done') def pre_process(self, data): """ data format pre process { "id": "1662", "query": "FLINK是什么", "history": [] } """ pre = json.loads(data.decode(encoding='utf-8')) print(f'row data: {pre}') return pre def post_process(self, state: str, id: str, res: str, history: list): """ process after process """ init = {'id': id, 'state': state, 'response': res, 'history': history } res = json.dumps(init, ensure_ascii=False) print(f'return data: {res}') return bytes(res, encoding='utf8') def process(self, data): """ process the request data """ try: data = self.pre_process(data) res, history = self.model.chat(self.tokenizer, data.get('query'), history=data.get('history')) return self.post_process("SUCCESS", data.get('id'), res, history), 200 except Exception as e: return self.post_process("FAILED", data.get('id'), None, None), 400 if __name__ == '__main__': # parameter worker_threads indicates concurrency of processing runner = MyProcessor(worker_threads=10) runner.run()
4.3. 部署
4.3.1. 官网样例
# 更改 app.json中的服务名称 和oss地址 eascmd create app.json
# 简单测试 curl http://****.vpc.cn-beijing.pai-eas.aliyuncs.com/api/predict/pysdk_demo -H 'Authorization: **' -d 'hello eas'
4.3.2. 自定义镜像迁移上云
在公有云的场景中,随着大模型的训练上云,很多客户都在做历史算法服务的迁移上云,而在客户线下IDC的场景中,自定义镜像部署HTTP服务是最常见的,EAS也提供了根据自定义镜像部署的方式,为客户服务平迁上云,享受弹性扩缩容及完善的监控告警打下了坚实的基础,以下是模拟客户自定义镜像迁移上云部署的样例代码。
4.3.2.1. Dockerfile
FROM ubuntu:22.04 LABEL maintainer="***" RUN chsh -s /bin/bash SHELL ["/bin/bash", "-c"] RUN mkdir -p /root/data RUN mkdir -p /root/opt RUN mkdir -p /root/service COPY s.txt /root/data/ COPY requirements.txt /root/service/ COPY tornado_test02.py /root/service/ RUN apt-get update && \ apt-get install -y curl RUN curl "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh" -o /root/opt/miniconda.sh && \ /bin/bash /root/opt/miniconda.sh -b -p /root/miniconda && \ ln -s /root/miniconda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \ echo ". /root/miniconda/etc/profile.d/conda.sh" >> ~/.bashrc ENV PATH /root/miniconda/bin:/root/miniconda/condabin:$PATH RUN conda create -n glm python=3.10 -y RUN echo "conda activate glm" >> ~/.bashrc ENV PATH /root/miniconda/envs/glm/bin:$PATH RUN python -m pip install -r /root/service/requirements.txt
4.3.2.2. 镜像打包上传
# 构建镜像 # ENTRYPOINT ["python" ,"/root/service/tornado_test02.py"] docker build -t **:1.0 . # 查看本地镜像 docker image ls -a # 登录acr镜像库 docker login --username=***@**.cn-beijing.aliyuncs.com # tag docker tag [ImageId] registry.cn-beijing.aliyuncs.com/**/**:[镜像版本号] # push docker push registry.cn-beijing.aliyuncs.com/**/**:[镜像版本号]
4.3.2.3. 自建服务样例代码
import tornado.ioloop import tornado.web class PostMethodHandler(tornado.web.RequestHandler): def post(self): data = self.request.body print(data) # do something with the data self.write("Data received successfully!") if __name__ == "__main__": app = tornado.web.Application([ (r"/post-method", PostMethodHandler), ]) app.listen(8888) tornado.ioloop.IOLoop.current().start()
4.3.2.4. 请求样例
4.3.3. GLM
通过json形式的配置文档使用eascmd create 来进行创建,或者在官网进行webui的配置,本质也是以配置成功的json配置文件为主。
{ "cloud": { "computing": { "instance_type": "ecs.gn6i-c8g1.2xlarge" }, "networking": { "security_group_id": "sg-2zefgh******tc3", "vpc_id": "vpc-2ze8******5fby1av", "vswitch_id": "vsw-2ze******aw56hdi" } }, "metadata": { "cpu": 8, "gpu": 1, "instance": 1, "memory": 31000, "name": "****m_test02" }, "processor_entry": "pai_eas_glm.py", "processor_path": "o**://**/***/pysdk_demo.tar.gz", "processor_type": "python", "storage": [ { "mount_path": "/mnt/data/", "nfs": { "path": "/", "server": "***rn23.cn-beijing.nas.aliyuncs.com" }, "properties": { "resource_type": "code" } } ] }
4.4. 其他
4.4.1. 模型预热
和车预热发动机一样,预热可以让模型在GPU上进行推理的时候有最好的性能
4.4.2. 在线调试
- 根据我们在脚本中设定的数据格式,进行在线调试请求,可以在右侧看到我们该请求的返回值。
- 大模型的推理会反复调用6B的模型多次,对于某些回答较长的问题来说,有些时候会出现408的超时熔断错误码,这是因为eas默认5s超时时间,可以在配置文件中修改metadata.rpc.keepalive参数的值来延长或者通过EAS提供的异步调用服务来解决这一问题。
4.4.3. 一键压测
个人感觉用“单个数据”这个就可以,直接找个业务逻辑最复杂的压,这样性能是可以保证的,如果想还原某个时序下的请求场景,可以通过其他方式上传请求数据的文件。
import base64 req = """{ "id": "1662", "query": "你好", "history": [] }""" # 64编码 print(base64.b64encode(req.encode('utf-8'))) # b'ewogICAgICAgICAgImlkIjo***ICAiaGlzdG9yeSI6IFtdCiAgICAgICAgfQ==' # 这里复制上去压测的时候不要把b\'\'带上
4.4.4. 扩缩容
4.4.4.1. 普通扩缩容
白话理解,流量要来了直接加实例就完事了,有资源就有qps
4.4.4.2. 弹性扩缩容
根据指标设置弹性的最大最小值和持续时间。
如果有自己业务上的积累还需要自定义上报metrics才能选用。
4.4.5. 更新服务
- 控制台更新服务,白屏webui的方式,重新传package然后点点点就行了
- eascmd:https://help.aliyun.com/document_detail/111031.htm?spm=a2c4g.465149.0.0.786d182a51fwei#section-tmv-2c4-jvq
eascmd modify <service_name> -s <service_desc_json>
4.4.6. 服务请求
请求注意下sync还是async,根据代码样例,修改自己服务的路由,endpoint以及token即可正常调用。参考文档:https://help.aliyun.com/document_detail/250806.html?spm=a2c4g.250807.0.0.13f6bc67AJ8xqi