【2024】Datawhale AI夏令营 Task3笔记——Baseline2部分代码解读及初步上分思路
【2024】Datawhale AI夏令营 Task3笔记——Baseline2部分代码解读及初步上分思路
本文对可完成赛事“逻辑推理赛道:复杂推理能力评估”初赛的Baseline2部分关键代码进行详细解读,介绍Baseline2涉及的关键技术和初步上分思路。
Baseline2代码由Datawhale AI夏令营提供,核心内容是将大语言模型部署至本地,并在此基础上使用Lora技术微调大语言模型。另外,为了加快本地推理的速度,本次任务代码使用vllm技术加速大语言模型推理(Task1和Task2方法的核心是直接调用外部大语言模型的API进行推理)。
🔴注意:
1、在进行代码解读时,为了简化代码、不影响代码理解,本文对baseline2代码顺序进行了一定调整。
2、运行Baseline2代码有部分注意点,分别是:尽量使用cuda12.1的设备(一)、根据选择的微调数据集修改答案提取函数(五)。
3、使用ana.json作为微调数据集,效果由于使用an.json。关于这两个文件的区别详见后文。
4、本文另外尝试在不使用vLLM的情况下进行推理,加速前单个问题的推理速度约为5分钟/题,加速后单个问题的推理速度约为0.35分钟/题,加速效果显著。
5、本文另外发现了vLLM服务启动后GPU占用较高的问题,vLLM服务启动后整体推理速度也仍有待提高(目前推理所有问题在V100-32G上需要7小时。改进方式可参考使用vLLM加速大语言模型推理)。
✨Baseline2涉及的关键技术
1、Lora微调(上分)
LoRA(Low-Rank Adaptation)微调是一种高效的模型微调技术,特别适用于大型预训练语言模型的适应性调整。LoRA的核心思想是通过引入低秩矩阵来调整模型的权重,从而在不显著增加模型参数数量的情况下,实现对模型的微调。
LoRA优势:
-
可以针对不同的下游任务构建小型 LoRA 模块,从而在共享预训练模型参数基础上有效地切换下游任务。
-
LoRA 使用自适应优化器(Adaptive Optimizer),不需要计算梯度或维护大多数参数的优化器状态,训练更有效、硬件门槛更低。
-
LoRA 使用简单的线性设计,在部署时将可训练矩阵与冻结权重合并,不存在推理延迟。
-
LoRA 与其他方法正交,可以组合。
实际使用方法可参考本文“Baseline2部分代码解读”中的第二、三章节。
2、vLLM(加速推理)
vLLM(Virtual Large Language Model)是一个由伯克利大学LMSYS组织开源的大规模语言模型高速推理框架,设计目标是在实时应用场景中大幅提升语言模型服务的吞吐量和内存使用效率。vLLM的特点包括易于使用、与Hugging Face等流行工具无缝集成以及高效的性能。
实际使用方法可参考本文“Baseline2部分代码解读”中的第四章节。
3、多路投票(上分)
Datawhale教程中对于多路投票/多路召回的解释:
所谓的“多路召回策略”就是指采用不同的策略、特征或者简单模型,分别召回一部分候选集,然后再把这些候选集混合在一起后供后续排序模型使用的策略。
在实践中,“不同的策略、特征或者简单模型”就是指不同次调用微调后的大语言模型;“召回一部分候选集”就是指分别得到不同的回答/答案选项(可能不同,也可能相同),“把这些候选集混合在一起后供后续排序模型使用”就是指统计这些回答中每个出现过的回答的次数,按照出现次数进行排序,最终决定出现次数最多的回答即为答案。
实际使用方法可参考本文“Baseline2部分代码解读”中的第五章节。
👩💻Baseline2部分代码解读
Baseline2的代码有一部分与Baseline1的代码相似,在此文章只记录Baseline2中新出现的技术与代码内容。
一、环境配置与数据文件准备
1.1 环境配置
Datawhale教程建议使用的环境配置如下:
unbuntu22.04
cuda12.1.0
py3.10
torch2.1.2
我认为cuda的版本比较关键,在自己配置环境的过程中,一开始先选择了一台pytorch版本也是2.1.2但cuda版本是11.8的GPU进行配置,最后到了启动vllm的环节时会无法启动(报错信息可见本章的补充部分)。
其余依赖的python第三方包:
pip install modelscope==1.9.5
pip install "transformers>=4.39.0"
pip install streamlit==1.24.0
pip install sentencepiece==0.1.99
pip install transformers_stream_generator==0.0.4
pip install datasets==2.18.0
pip install peft==0.10.0
pip install openai==1.17.1
pip install tqdm==4.64.1
pip install transformers==4.39.3
python -m pip install setuptools==69.5.1
pip install vllm==0.4.0.post1
pip install nest-asyncio
pip install accelerate
pip install tf-keras
1.2 数据文件准备
数据文件用于微调大语言模型,使之推理能力增强。本次任务使用赛事官方提供的训练集文件round1_train_data.jsonl作为微调数据。Datawhale教程对round1_train_data.jsonl文件中的数据进行了两种处理,分别得到an.json
和ana.json
文件。本次任务将使用an.json
或ana.json
文件作为微调数据集。
-
处理一:将每个问题中的子问题和答案构建成问答对,得到的结果示例:
{"instruction": "你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为\"答案是:A\"。题目如下:\n\n### 题目:\n假设您需要构建一个二叉搜索树,其中每个节点或者是一个空的节点(称为\"空节点\"),或者是一个包含一个整数值和两个子树的节点(称为\"数值节点\")。以下是构建这棵树的规则:\n\n1. 树中不存在重复的元素。\n2. 对于每个数值节点,其左子树的所有值都小于该节点的值,其右子树的所有值都大于该节点的值。\n3. 插入一个新值到一个\"空节点\"时,该\"空节点\"会被一个包含新值的新的数值节点取代。\n4. 插入一个已存在的数值将不会改变树。\n\n请基于以上规则,回答以下选择题:\n\n### 问题:\n选择题 1:\n给定一个空的二叉搜索树,插入下列数字: [5, 9, 2, 10, 11, 3],下面哪个选项正确描述了结果树的结构?\nA. tree(5, tree(2, tree(3, nil, nil), nil), tree(9, tree(10, nil, nil), tree(11, nil, nil)))\nB. tree(5, tree(2, nil, tree(3, nil, nil)), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\nC. tree(5, tree(3, tree(2, nil, nil), nil), tree(9, nil, tree(10, tree(11, nil, nil), nil)))\nD. tree(5, nil, tree(2, nil, tree(3, nil, nil)), tree(9, tree(11, nil, nil), tree(10, nil, nil)))\n","input": "","output": "B" },
整个文件处理完成后,数据存储在
an.json
文件中。 -
处理二:通过qwen2-72b模型在train数据集上跑得正确结果并分析,将每个问题中的子问题、分析及答案构建成问答对,得到的结果示例:
{"instruction": "你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为\"答案是:A\"。题目如下:\n\n### 题目:\n假设您需要构建一个二叉搜索树,其中每个节点或者是一个空的节点(称为\"空节点\"),或者是一个包含一个整数值和两个子树的节点(称为\"数值节点\")。以下是构建这棵树的规则:\n\n1. 树中不存在重复的元素。\n2. 对于每个数值节点,其左子树的所有值都小于该节点的值,其右子树的所有值都大于该节点的值。\n3. 插入一个新值到一个\"空节点\"时,该\"空节点\"会被一个包含新值的新的数值节点取代。\n4. 插入一个已存在的数值将不会改变树。\n\n请基于以上规则,回答以下选择题:\n\n### 问题:\n选择题 1:\n给定一个空的二叉搜索树,插入下列数字: [5, 9, 2, 10, 11, 3],下面哪个选项正确描述了结果树的结构?\nA. tree(5, tree(2, tree(3, nil, nil), nil), tree(9, tree(10, nil, nil), tree(11, nil, nil)))\nB. tree(5, tree(2, nil, tree(3, nil, nil)), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\nC. tree(5, tree(3, tree(2, nil, nil), nil), tree(9, nil, tree(10, tree(11, nil, nil), nil)))\nD. tree(5, nil, tree(2, nil, tree(3, nil, nil)), tree(9, tree(11, nil, nil), tree(10, nil, nil)))\n","input": "","output": "根据题目中的规则,我们依次插入数字 [5, 9, 2, 10, 11, 3] 来构建二叉搜索树。\n\n首先插入数字 5,树为:\n```\ntree(5, nil, nil)\n```\n\n然后插入数字 9,由于 9 > 5,因此插入到右子树:\n```\ntree(5, nil, tree(9, nil, nil))\n```\n\n接下来插入数字 2,由于 2 < 5,因此插入到左子树:\n```\ntree(5, tree(2, nil, nil), tree(9, nil, nil))\n```\n\n再插入数字 10,由于 10 > 5 并且 10 > 9,所以插入到 9 的右子树:\n```\ntree(5, tree(2, nil, nil), tree(9, nil, tree(10, nil, nil)))\n```\n\n继续插入数字 11,由于 11 > 5 并且 11 > 9 和 11 > 10,所以插入到 10 的右子树:\n```\ntree(5, tree(2, nil, nil), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\n```\n\n最后插入数字 3,由于 3 < 5 但是 3 > 2,所以插入到 2 的右子树:\n```\ntree(5, tree(2, nil, tree(3, nil, nil)), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\n```\n\n比较这个最终结果与给定的选项,可以看到选项 B 正确地描述了结果树的结构。\n\n答案是:B"},
整个文件处理完成后,数据存储在
ana.json
文件中。
数据文件由Datawhale处理后提供,可在链接Datawhale2024年第二届世界科学智能大赛逻辑推理赛道:复杂推理能力评估baseline2中查看(链接中除了数据文件还有相关的代码,本文分析的代码源自此链接中代码)。
可以使用如下命令将数据文件下载到服务器中:
git clone https://www.modelscope.cn/datasets/Datawhale/DW2024_Complex_reasoning_ability_assessment_qwen2-7b-lora.git
这里多说一句,微调数据集的质量对微调后模型推理的性能有较大的影响。根据我自己的实验结果,使用an.json中数据进行微调,在比赛中的分数低于使用ana.json中数据进行微调。
1.3 大语言模型文件下载
下载魔搭社区的Qwen2-7B-Instruct模型文件。将模型下载到服务器后,我们可以直接使用模型文件推理(不考虑推理速度问题的话),不必再调用外部的大语言模型API。
使用如下命令进行下载:
import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('qwen/Qwen2-7B-Instruct', cache_dir='./', revision='master')
(😢补充)使用cuda11.8的GPU配置服务,无法启动vllm
在安装完需要使用的python第三方包、微调完成后,在启动vllm时出现如下多则报错信息,最终未能解决,此处仅将尝试解决报错的过程进行记录。
报错信息1
ImportError: libcudart.so.12: cannot open shared object file: No such file or directory
vllm的github仓库有人就这一问题进行提问:ImportError: libcudart.so.12,得到的回复是:
这个回复的意思是,要么使用cuda 12.1的环境,要么安装适合cuda 11.8的vllm。
在xformers的github仓库中,安装cuda 11.8对应的vllm使用的指令如下,前提是需要已有pytorch 2.3.1,这与教程推荐的pytorch版本又有出入(但是我忘记当时自己是不是没看到这个前提、仍然在pytorch=2.1.2的情况下执行了这条命令,安装成功了,但是后续仍然有新的报错)。
# cuda 11.8 version
pip3 install -U xformers --index-url https://download.pytorch.org/whl/cu118
报错信息2
xFormers can't load C++/CUDA extensions.
针对这则报错信息,Stackflow上的一位网友建议检查xFormers与pytorch的版本是否对应。可在此网站上查看xFormers和pytorch的版本对应信息:https://anaconda.org/xformers/xformers/files。在这个网页上,看到与pytorch2.1.2、cuda11.8对应的xformers包是linux-64/xformers-0.0.23.post1-py310_cu11.8.0_pyt2.1.2.tar.bz2,使用wget命令将这个包下载到服务器后安装:
wget https://anaconda.org/xformers/xformers/0.0.23.post1/download/linux-64/xformers-0.0.23.post1-py310_cu11.8.0_pyt2.1.2.tar.bz2
pip install xformers-0.0.23.post1-py310_cu11.8.0_pyt2.1.2.tar.bz2
参考:BUG:C++/CUDA extensions. xFormers was built for: PyTorch 2.1.2+cu121 with CUDA 1201 (you have 2.1.0+
报错信息3
RuntimeError: CUDA error: no kernel image is available for execution on the device
文章《解决CUDA error: no kernel image is available for execution on the device》的作者认为此报错由GPU算力与pytorch依赖的CUDA的算力不匹配造成,建议重新安装pytorch,故使用如下命令重新安装pytorch:
pip install --force-reinstall torch==2.1.2 torchvision==0.16.2 torchaudio==2.1.2 --index-url https://download.pytorch.org/whl/cu118
然而这并没有帮助我解决这一报错。由于时间已晚,不再深入debug,使用新的cuda版本为2.1.2的GPU服务器配置环境。
二、Lora微调
2.1 加载微调所需环境及数据文件
from datasets import Dataset
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer, GenerationConfig
import torch
from peft import LoraConfig, TaskType, get_peft_modeldf = pd.read_json('an.json') # 这里先使用an.json文件
ds = Dataset.from_pandas(df) # 将JSON文件转换为CSV文件
2.2 加载Tokenizer及预训练模型
pretrained_model_path = './qwen/Qwen2-7B-Instruct' # 具体路径需按实际情况修改# 加载Tokenizer
tokenizer = AutoTokenizer.from_pretrained(pretrained_model_path, use_fast=False, trust_remote_code=True)# 加载预训练模型
model = AutoModelForCausalLM.from_pretrained(pretrained_model_path, device_map="auto",torch_dtype=torch.bfloat16)
2.3 定义数据格式化函数并检查
使用Lora微调大语言模型时,训练数据需要经过格式化、编码(即tokenize操作),然后再输入模型进行微调训练。这个过程一般包括:
- 将输入文本编码为
input_ids
; - 将输出文本编码为
labels
。
编码之后的结果都是多维的向量。
定义预处理函数process_func
,对每一个样本的输入、输出文本进行编码,返回编码后的字典。
def process_func(example):MAX_LENGTH = 1800 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性input_ids, attention_mask, labels = [], [], []# 对输入部分进行分词instruction = tokenizer(f"<|im_start|>system\n你是一个逻辑推理专家,擅长解决逻辑推理问题。<|im_end|>\n<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False) # add_special_tokens=False表示不在开头加special_tokens# 对输出部分进行分词response = tokenizer(f"{example['output']}", add_special_tokens=False)input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id] # input_ids是指token在词典中对应的序号attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为eos token也要关注,所以 补充为1labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id] # 对于instruction部分的 token,用-100填充(通常用于忽略计算损失)response部分的token则直接用其token IDs,并在末尾添加一个pad_token_id。if len(input_ids) > MAX_LENGTH: # 做截断,只保留最靠后的部分input_ids = input_ids[:MAX_LENGTH]attention_mask = attention_mask[:MAX_LENGTH]labels = labels[:MAX_LENGTH]return {"input_ids": input_ids,"attention_mask": attention_mask,"labels": labels}
调用process_func
函数处理数据。
tokenized_id = ds.map(process_func, remove_columns=ds.column_names)
调用process_func
函数处理数据后,得到的数据集结构如下:
Dataset({features: ['input_ids', 'attention_mask', 'labels'],num_rows: 1204
})
进行decode
数据检查,选择数据集中第一个问答对,检查其输入文本的解码情况;选择数据集中第二个问答对,检查其输出文本的解码情况。
tokenizer.decode(tokenized_id[0]['input_ids']) # 检查输入文本的解码结果
tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[1]["labels"]))) # 检查输出文本的解码结果
输入文本的解码结果如下:
'<|im_start|>system\n你是一个逻辑推理专家,擅长解决逻辑推理问题。<|im_end|>\n<|im_start|>user\n你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下:\n\n### 题目:\n假设您需要构建一个二叉搜索树,其中每个节点或者是一个空的节点(称为"空节点"),或者是一个包含一个整数值和两个子树的节点(称为"数值节点")。以下是构建这棵树的规则:\n\n1. 树中不存在重复的元素。\n2. 对于每个数值节点,其左子树的所有值都小于该节点的值,其右子树的所有值都大于该节点的值。\n3. 插入一个新值到一个"空节点"时,该"空节点"会被一个包含新值的新的数值节点取代。\n4. 插入一个已存在的数值将不会改变树。\n\n请基于以上规则,回答以下选择题:\n\n### 问题:\n选择题 1:\n给定一个空的二叉搜索树,插入下列数字: [5, 9, 2, 10, 11, 3],下面哪个选项正确描述了结果树的结构?\nA. tree(5, tree(2, tree(3, nil, nil), nil), tree(9, tree(10, nil, nil), tree(11, nil, nil)))\nB. tree(5, tree(2, nil, tree(3, nil, nil)), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\nC. tree(5, tree(3, tree(2, nil, nil), nil), tree(9, nil, tree(10, tree(11, nil, nil), nil)))\nD. tree(5, nil, tree(2, nil, tree(3, nil, nil)), tree(9, tree(11, nil, nil), tree(10, nil, nil)))\n<|im_end|>\n<|im_start|>assistant\n根据题目中的规则,我们依次插入数字 [5, 9, 2, 10, 11, 3] 来构建二叉搜索树。\n\n首先插入数字 5,树为:\n```\ntree(5, nil, nil)\n```\n\n然后插入数字 9,由于 9 > 5,因此插入到右子树:\n```\ntree(5, nil, tree(9, nil, nil))\n```\n\n接下来插入数字 2,由于 2 < 5,因此插入到左子树:\n```\ntree(5, tree(2, nil, nil), tree(9, nil, nil))\n```\n\n再插入数字 10,由于 10 > 5 并且 10 > 9,所以插入到 9 的右子树:\n```\ntree(5, tree(2, nil, nil), tree(9, nil, tree(10, nil, nil)))\n```\n\n继续插入数字 11,由于 11 > 5 并且 11 > 9 和 11 > 10,所以插入到 10 的右子树:\n```\ntree(5, tree(2, nil, nil), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\n```\n\n最后插入数字 3,由于 3 < 5 但是 3 > 2,所以插入到 2 的右子树:\n```\ntree(5, tree(2, nil, tree(3, nil, nil)), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\n```\n\n比较这个最终结果与给定的选项,可以看到选项 B 正确地描述了结果树的结构。\n\n答案是:B<|endoftext|>'
输出文本的解码结果如下:
'根据题目描述的函数逻辑,我们来逐步分析输入列表 [3,7,5,9] 的处理过程:\n\n1. 列表非空,开始处理第一个元素3。\n2. 3是奇数,根据规则应被删除。\n3. 处理下一个元素7,同样为奇数,删除。\n4. 继续处理5,同样是奇数,删除。\n5. 最后处理9,依然是奇数,删除。\n\n整个过程中,由于输入列表中的所有元素都是奇数,根据规则它们都将被删除,不会有任何元素保留在结果列表中。\n\n因此,对于输入列表 [3,7,5,9],函数返回的结果是空列表。\n\n答案是:D. []<|endoftext|>'
2.4 设置Lora微调相关参数(LoraConfig)
config = LoraConfig(task_type=TaskType.CAUSAL_LM, # 模型类型target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], # 需要训练的模型层的名字,主要是attention部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式inference_mode=False, # 训练模式r=8, # Lora 秩,具体可以看Lora原理lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理# Lora的缩放是lora_alpha/r, 在这个LoraConfig中缩放就是4倍lora_dropout=0.1# Dropout 比例
)
2.5 创建微调模型(PeftModel)
PEFT(Parameter-Efficient Fine-Tuning,参数高效微调),是一个用于在不微调所有模型参数的情况下,高效地将预训练语言模型(PLM)适应到各种下游应用的库。
每个PEFT方法由一个PeftConfig类来定义,该类存储了用于构建PeftModel的所有重要参数。由于本文使用LoRA微调,因此实际创建的是LoraConfig类。随后,使用get_peft_model()
函数创建PeftModel
,需要基本模型(可从Transformers
库加载)和 LoraConfig
(其中包含如何配置模型以使用 LoRA 进行训练的参数)。
model = get_peft_model(model, config)
打印可训练参数的规模:
model.print_trainable_parameters()
# trainable params: 20,185,088 || all params: 7,635,801,600 || trainable%: 0.26434798934534914
2.6 设置训练器相关参数(TrainingArguments)
args = TrainingArguments(output_dir="./output/Qwen2_instruct_lora", # 微调完成后,微调模型的输出路径(仅包括微调层的部分,并不是整个模型。如果需要得到微调后的整个模型,需要进行模型合并,下文会继续提到)per_device_train_batch_size=1, # batch_sizegradient_accumulation_steps=4, # 梯度累加,如果显存比较小,可以把batch_size设置小一点,梯度累加增大一些logging_steps=10, # 多少步,输出一次lognum_train_epochs=1, # epochsave_steps=100, # 为了快速演示,这里设置10,建议你设置成100learning_rate=1e-4,save_on_each_node=True,gradient_checkpointing=True # 梯度检查,这个一旦开启,模型就必须执行
)
开启梯度检查点时,要执行此方法:
model.enable_input_require_grads()
关于梯度检查点(gradient_checkpoint),这是一种用于节省训练时显存的同时不拖慢训练的技术。更多介绍可参考文章:大模型高效训练基础知识:梯度检查点(Gradient Checkpointing)。
2.7 定义训练器
trainer = Trainer(model=model,args=args,train_dataset=tokenized_id,data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)
2.8 开启训练
torch.backends.cuda.enable_mem_efficient_sdp(False)
trainer.train()
多说一句:在魔搭平台的notebook(A10,8核cpu)运行训练过程耗时约10min,在我自己配置的V100-32GB(6核cpu)服务器上运行训练过程则耗时约60min,差异较大,暂时未弄清差异来源。
三、微调模型测试及模型合并存储
3.1 调用微调后模型进行测试
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from peft import PeftModelmode_path = './qwen/Qwen2-7B-Instruct' # 预训练大语言模型的路径
lora_path = './output/Qwen2_instruct_lora/checkpoint-100' # 微调完成后,微调模型的输出路径# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(mode_path, trust_remote_code=True)
# 加载模型
model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.float16, trust_remote_code=True).eval()
# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path)# 测试问题,这里写好prompt
prompt = '''你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下:\n\n### 题目:\n假设您需要构建一个二叉搜索树,其中每个节点或者是一个空的节点(称为"空节点"),或者是一个包含一个整数值和两个子树的节点(称为"数值节点")。以下是构建这棵树的规则:\n\n1. 树中不存在重复的元素。\n2. 对于每个数值节点,其左子树的所有值都小于该节点的值,其右子树的所有值都大于该节点的值。\n3. 插入一个新值到一个"空节点"时,该"空节点"会被一个包含新值的新的数值节点取代。\n4. 插入一个已存在的数值将不会改变树。\n\n请基于以上规则,回答以下选择题:\n\n### 问题:\n选择题 1:\n给定一个空的二叉搜索树,插入下列数字: [5, 9, 2, 10, 11, 3],下面哪个选项正确描述了结果树的结构?\nA. tree(5, tree(2, tree(3, nil, nil), nil), tree(9, tree(10, nil, nil), tree(11, nil, nil)))\nB. tree(5, tree(2, nil, tree(3, nil, nil)), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\nC. tree(5, tree(3, tree(2, nil, nil), nil), tree(9, nil, tree(10, tree(11, nil, nil), nil)))\nD. tree(5, nil, tree(2, nil, tree(3, nil, nil)), tree(9, tree(11, nil, nil), tree(10, nil, nil)))'''inputs = tokenizer.apply_chat_template([{"role": "user", "content": "你是一个逻辑推理专家,擅长解决逻辑推理问题。"},{"role": "user", "content": prompt}],add_generation_prompt=True,tokenize=True,return_tensors="pt",return_dict=True
).to('cuda')gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1}# 使用generate函数生成推理结果
with torch.no_grad():outputs = model.generate(**inputs, **gen_kwargs)outputs = outputs[:, inputs['input_ids'].shape[1]:]print(tokenizer.decode(outputs[0], skip_special_tokens=True))
输出:
……(一些日志信息)
2024-07-28 00:15:34.902958: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Loading checkpoint shards: 100%|██████████| 4/4 [00:40<00:00, 10.25s/it]
B
3.2 模型合并存储
将lora微调后的模型融入到原模型中。
new_model_directory = "./merged_model_an" # 合并后模型的存储路径
merged_model = model.merge_and_unload()
# 将权重保存为safetensors格式的权重, 且每个权重文件最大不超过2GB(2048MB)
merged_model.save_pretrained(new_model_directory, max_shard_size="2048MB", safe_serialization=True)
预训练大语言模型的tokenizer.json
文件也需要一并放入合并后模型的路径(下方命令在终端执行,路径按实际情况修改):
cp ./qwen/Qwen2-7B-Instruct/tokenizer.json ./merged_model_an/
四、启动vllm服务加速推理
由于在环境配置时已经安装vllm第三方包,直接在终端输入如下命令即可启动vllm服务。执行后通过vllm的类openai接口便可成功将微调后的模型部署到本地8000端口。
VLLM_USE_MODELSCOPE=False python -m vllm.entrypoints.openai.api_server --model ./merged_model --served-model-name Qwen2-7B-Instruct-lora --max-model-len=4096 --gpu_memory_utilization=0.6
-
VLLM_USE_MODELSCOPE=False
表示不使用modelscope上面的模型,使用本地的模型。添加这个参数的设置可以避免vllm错误识别模型路径或名称、远程下载模型。 -
-
-model ./merged_model
表示需要调用./merged_model
中的模型,路径需根据实际情况修改 -
--served-model-name Qwen2-7B-Instruct-lora
表示启动的模型名称为Qwen2-7B-Instruct-lora -
--gpu_memory_utilization=0.6
(可选,设置这个参数是为了避免占用过多GPU内存,默认情况下是0.9)
vllm服务启动成功的输出:
(😢补充)vLLM服务启动后GPU占用较高
发现启动vLLM服务后GPU占用较高,以V100-32GB显卡为例,GPU内存使用达到17799MiB / 32768MiB(GPU利用率则为0%)。开始推理后,GPU内存使用达到17971MiB / 32768MiB(这是在设置了gpu_memory_utilization=0.6
的情况下,如果不设置这个参数,会几乎拉满),GPU利用率则为92%左右。
讨论这个问题的文章有:【实践】VLLM显存暴增 | 多卡推理 | 批量推理
推理速度的瓶颈应该还是在于显卡显存的大小,而非GPU内存使用。
五、开始推理(多路投票)
正式进行推理这部分的代码与Baseline1中的代码相似,这里只记录几个不同的地方。
5.1 MODEL_NAME设置
由于在启动vllm服务时,定义了served-model-name
是Qwen2-7B-Instruct-lora,所以在代码中,MODEL_NAME的值也需要是Qwen2-7B-Instruct-lora,即:
MODEL_NAME = 'Qwen2-7B-Instruct-lora'
5.2 类openAI接口
改写Baseline1中的call_qwen_api
代码,改为调用本地类openai的qwen微调模型接口。
def call_qwen_api(MODEL_NAME, query):# 这里采用dashscope的api调用模型推理,通过http传输的json封装返回结果 client = OpenAI(base_url="http://localhost:8000/v1",api_key="sk-xxx", # 随便填写,只是为了通过接口参数校验)completion = client.chat.completions.create(model=MODEL_NAME,messages=[# {'role':'system','content':'你是一个解决推理任务的专家,你需要分析出问题中的每个实体以及响应关系。然后根据问题一步步推理出结果。并且给出正确的结论。'},{"role": "user", "content": query}])return completion.choices[0].message.content
5.3 多路投票
1、设计投票函数
定义函数most_frequent_chat
,通过三次结果推理,将选择答案最多的结果作为最终结果返回。
def most_frequent_char(char1, char2, char3): # 这里设置三个char作为参数是因为后面会三次调用LLM,每个char对应一个LLM调用的回答# 创建一个字典来存储每个字符的出现次数frequency = {char1: 0, char2: 0, char3: 0}# 增加每个字符的出现次数frequency[char1] += 1frequency[char2] += 1frequency[char3] += 1# 找到出现次数最多的字符most_frequent = max(frequency, key=frequency.get)return most_frequent
假设输入此函数的参数分别为A、A、B,初始化字符频率字典 frequency
为 {A: 0, A: 0, B: 0}
。但是由于字典的键必须是唯一的,所以最终字典将是 {A: 0, B: 0}
。
更新频率字典,增加每个字符的出现次数:
frequency[A] += 1
,频率字典变为{A: 1, B: 0}
。frequency[A] += 1
,频率字典变为{A: 2, B: 0}
。frequency[B] += 1
,频率字典变为{A: 2, B: 1}
。
使用 max(frequency, key=frequency.get)
找到出现次数最多的字符。此时 A
的频率是 2
,B
的频率是 1
,所以 A
是出现次数最多的字符。
2、多次调用大语言模型,生成多个结果以供投票
改写process_datas
函数,三次调用LLM,统计出现的答案的出现次数,最终返回投票数最多的结果。
def process_datas(datas,MODEL_NAME):results = []# 送入多线程任务for data in tqdm(datas, desc="Submitting tasks", total=len(datas)):problem = data['problem']for id,question in enumerate(data['questions']):prompt = get_prompt(problem, question['question'], question['options'],)# 三次调用LLMres, res1, res2 = api_retry(MODEL_NAME, prompt), api_retry(MODEL_NAME, prompt), api_retry(MODEL_NAME, prompt)# 获取不同的答案extract_response, extract_response1, extract_response2 = extract(res), extract(res1), extract(res2)ans = most_frequent_char(extract_response, extract_response1, extract_response2)data['questions'][id]['answer'] = ansresults.append(data) return results
(🔴补充)答案提取
值得一提的是,答案提取函数extract
的写法根据微调训练使用的数据集不同也有较小的改动,具体改动内容见注释:
def extract(input_text):ans_pattern = re.compile(r"(.)", re.S) # 使用an.json文件作为微调训练集时使用# ans_pattern = re.compile(r"答案是:(.)", re.S) # 使用ana.json文件作为微调训练集时使用problems = ans_pattern.findall(input_text)if(problems == []):return 'A'return problems[0]
这个改动是相当重要的,否则推理完成后,可能会发现提取的答案不是合格的字符,浪费推理资源。
(💦补充)在不使用vllm加速推理的情况下进行推理
在不使用/无法使用vllm进行加速推理时,由于微调后的大语言模型就存储在本地服务器中,因此直接使用模型进行推理是可行的,这里展示一段直接使用模型进行推理的代码(与3.1节中的代码其实很相似):
from transformers import AutoModelForCausalLM, AutoTokenizer
from datetime import datetimedevice = "cuda" # the device to load the model ontomodel_path = './merged_model' # 按实际情况修改
model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(model_path)prompt = '''你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下:\n\n### 题目:\n假设您需要构建一个二叉搜索树,……\n\n请基于以上规则,回答以下选择题:\n\n### 问题:\n选择题 1:\n给定一个空的二叉搜索树,插入下列数字: [5, 9, 2, 10, 11, 3],下面哪个选项正确描述了结果树的结构?\nA. ……\nB. ……\nC. ……\nD. ……'''
messages = [{"role": "user", "content": prompt}]
for i in range(5): # 这个循环只是为了模拟回答多个问题的情形currentDateAndTime = datetime.now()print("The current date and time is", currentDateAndTime) # 方便观察回答一个问题耗时text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)model_inputs = tokenizer([text], return_tensors="pt").to(device)generated_ids = model.generate(model_inputs.input_ids, max_new_tokens=512, do_sample=True) # max_new_tokens需要设置得大一点generated_ids = [output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)]response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]print(response)'''具体地组织不同问题的prompt、提取答案选项、保存答案至文件等环节的代码省略'''
多说一句,在我的环境和配置下,不使用vllm加速推理,处理测试集中500个问题(由于每个问题背景下有多个子问题,因此实际是需要处理1204个问题),每个问题需要处理5 minutes;使用vllm加速推理的情况下,处理全部问题需要约7 hours,即每个问题需要处理约0.35 minute,加速效果还是挺明显的(但是整体推理速度还是比较慢,需要7小时)。
💦初步上分思路
1、prompt优化
在Task4文档里有此部分建议,提出了若干种类型的prompt。
- 训练时和推理时用的prompt建议用一样的
- 题目有数学计算、逻辑推理、判断等主要类型。可以考虑将数据集进行分类,对不同类型的题目设计不同的prompt(有没有相关的案例?)
2、agent设计
3、数据集扩充
-
使用相同数据集或开源数据集进行数据集扩充
-
用GPT 4或者其他能力更强的大语言模型生成数据
4、其他微调方法
目前微调是使用参数高效微调peft框架进行微调。peft框架文档:https://huggingface.co/docs/peft/conceptual_guides/adapter
-
可以尝试微调和RAG一起弄
-
FFT全量微调
-
LoRA-Pro对齐全量微调
-
DoRA
-
LoRA +
具体地组织不同问题的prompt、提取答案选项、保存答案至文件等环节的代码省略
‘’’
多说一句,在我的环境和配置下,不使用vllm加速推理,处理测试集中500个问题(由于每个问题背景下有多个子问题,因此实际是需要处理1204个问题),每个问题需要处理5 minutes;使用vllm加速推理的情况下,处理全部问题需要约7 hours,即每个问题需要处理约0.35 minute,加速效果还是挺明显的(但是整体推理速度还是比较慢,需要7小时)。## 💦初步上分思路### 1、prompt优化在Task4文档里有此部分建议,提出了若干种类型的prompt。- 训练时和推理时用的prompt建议用一样的
- 题目有数学计算、逻辑推理、判断等主要类型。可以考虑将数据集进行分类,对不同类型的题目设计不同的prompt(有没有相关的案例?)### 2、agent设计### 3、数据集扩充- 使用相同数据集或开源数据集进行数据集扩充- 用GPT 4或者其他能力更强的大语言模型生成数据### 4、其他微调方法目前微调是使用参数高效微调peft框架进行微调。peft框架文档:https://huggingface.co/docs/peft/conceptual_guides/adapter- 可以尝试微调和RAG一起弄- FFT全量微调- LoRA-Pro对齐全量微调
- DoRA
- LoRA +
相关文章:
【2024】Datawhale AI夏令营 Task3笔记——Baseline2部分代码解读及初步上分思路
【2024】Datawhale AI夏令营 Task3笔记——Baseline2部分代码解读及初步上分思路 本文对可完成赛事“逻辑推理赛道:复杂推理能力评估”初赛的Baseline2部分关键代码进行详细解读,介绍Baseline2涉及的关键技术和初步上分思路。 Baseline2代码由Datawhal…...
软件测试——测试分类(超超超齐全版)
为什么要对软件测试进行分类 软件测试是软件⽣命周期中的⼀个重要环节,具有较⾼的复杂性,对于软件测试,可以从不同的⻆度加以分类,使开发者在软件开发过程中的不同层次、不同阶段对测试⼯作进⾏更好的执⾏和管理测试的分类⽅法。…...
深入解析 Go 语言 GMP 模型:并发编程的核心机制
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:点击跳转到网站,对人工智能感兴趣的小伙伴可以点进去看看。 前言 本章是Go并发编程的起始篇章,在未来几篇文章中我们会…...
PHP中如何处理字符串
在PHP中,处理字符串是一项非常常见的任务,PHP提供了大量的内置函数来方便地处理字符串。以下是一些常用的字符串处理函数: strlen() - 返回字符串的长度。 php复制代码 $text "Hello, World!"; echo strlen($text); // 输出&…...
windows内存泄漏检查汇总
VLD(Visual Leak Detector) 下载 官方下载地址2.5 另一分支2.7 安装 点击运行安装...
yolo格式数据集之空中及地面拍摄道路病害检测7种数据集已划分好|可以直接使用|yolov5|v6|v7|v8|v9|v10通用
yolo格式数据集之空中及地面拍摄道路病害检测7种数据集已划分好|可以直接使用|yolov5|v6|v7|v8|v9|v10通用 本数据为空中及地面拍摄道路病害检测检测数据集,数据集数量如下: 总共有:33585张 训练集:6798张 验证集:3284张 测试集&a…...
[Meachines] [Easy] Mirai Raspberry树莓派默认用户登录+USB挂载文件读取
信息收集 IP AddressOpening Ports10.10.10.48TCP:22,53,80,1276,32400,32469 $ nmap -p- 10.10.10.48 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 6.7p1 Debian 5deb8u3 (protocol 2.0) | ssh-hostkey: | 1024 aa:ef:5c:…...
从零开始安装Jupyter Notebook和Jupyter Lab图文教程
前言 随着人工智能热浪(机器学习、深度学习、卷积神经网络、强化学习、AGC以及大语言模型LLM, 真的是一浪又一浪)的兴起,小伙伴们Python学习的热情达到了空前的高度。当我20年前接触Python的时候,做梦也没有想到Python会发展得怎么…...
数据库魔法:SQL Server中自定义分区函数的奥秘
数据库魔法:SQL Server中自定义分区函数的奥秘 在SQL Server中,分区表是管理大型表和提高查询性能的强大工具。分区函数和分区方案允许你根据特定的规则将数据分散到不同的文件组中。本文将深入探讨如何在SQL Server中实现数据库的自定义分区函数&#…...
网页禁止移除水印
一般的话水印分为明水印和暗水印两种 明水印的话就是在视频canvas上面蒙上一个div(如我上篇文章) ,暗水印的话就是把文字通过技术嵌入到图像里。 具体实现的话可以使用MutationObserver API 来监视 DOM 的变化,特别是针对目标节…...
Node Red 与axios简易测试环境的搭建
为了学习在vue3中如何使用axios,我借Sider Fusion的帮助搭建了基于node的简易测试环境。 Axios 是一个基于 Promise 的 HTTP 客户端,通常用于浏览器环境,但它也可以在 Node.js 环境中使用。因此,可以在 Ubuntu 的 Bash 环境下通过…...
测试面试宝典(四十三)—— 接口测试流程
回答一: 接口测试一般遵循以下流程: 需求分析 仔细研究接口的需求文档,包括接口的功能、输入输出参数、业务逻辑、性能要求等。与开发人员、产品经理等沟通,确保对需求的理解准确无误。 测试计划制定 确定测试的目标、范围和策略。…...
arkhamintelligence 请求头加密 X-Payload 完整逆向分析+自动化解决方案
大家好!我是爱摸鱼的小鸿,关注我,收看每期的编程干货。 逆向是爬虫工程师进阶必备技能,当我们遇到一个问题时可能会有多种解决途径,而如何做出最高效的抉择又需要经验的积累。本期文章将以实战的方式,带你详细地逆向分析 arkhamintelligence 请求头加密字段 X-Payload 的…...
Vue Router哈希模式和历史模式
Vue官方文档 哈希模式(hash mode) 特点 URL 格式:使用 # 符号分隔路径,哈希值之后的部分由客户端解析。 https://example.com/#/about无需服务器配置:哈希值部分不会被发送到服务器,因此不需要额外的服…...
Springboot实战:AI大模型+亮数据代理助力短视频时代
目录 前言1.如何入门亮数据1.1、注册登录1.2、注册账号1.3、登录1.4、购买静态住宅代理1.5、展示购买的代理 2. 使用Springboot、AI大模型构建系统2.1 使用Springboot、AI大模型构建爬虫2.2、在Springboot项目添加工具 3、编写代码,爬取视频素材3.1、代码里使用代理…...
Postman请求问题 connect ECONNREFUSED 127.0.0.1:80解决方法
问题描述: 解决方法: (1)点击file-settings (2)点击Proxy,并将右边的Use the system proxy 取消选中 (3)勾选use custom proxy configuration 这个8080是默认的…...
维护SQL Server数据库索引:保持性能的黄金法则
维护SQL Server数据库索引:保持性能的黄金法则 在SQL Server中,数据库索引是优化查询性能的关键工具。然而,随着数据的不断变化,索引可能会变得碎片化或过时,从而降低数据库性能。因此,定期维护索引是确保…...
nvm管理node版本问题处理集合
windows上通过nvm管理node版本,通过nvm安装node,报错了,信息: > Could not retrieve https://nodejs.org/dist/latest/SHASUMS256.txt. Get > https://nodejs.org/dist/latest/SHASUMS256.txt: dial tcp 104.20.23.46:443: …...
word打印---doc转html后进行打印,window.print、print-js、vue-print-nb
提示:word预览方式—插件 文章目录 [TOC](文章目录) 前言一、vue-office-docx把docx转换html二、调取window.print三、print-js四、vue-print-nb总结 前言 word预览 一、vue-office-docx把docx转换html npm install vue-office-docx -S-DofficeDocx.vue <templ…...
CTF学习笔记汇总(非常详细)零基础入门到精通,收藏这一篇就够了
CTF学习笔记汇总 Part.01 Web 01 SSRF 主要攻击方式如下: 01 对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息。 02 攻击运行在内网或本地的应用程序。 03 对内网Web应用进行指纹识别,识别企业内部的资产信息。 …...
如果想不明白,那就读书吧
人生起伏是常态,平平淡淡的日子亦是常态,但是在常态中的普通人往往面对着各种各样的风险和挑战,稍有不慎,生活天翻地覆。 回到现在这家公司是一次吃回头草的过程,其中亦是一次生活的坎坷,祸福相伴。来公司…...
Golang处理Word文档模板实现标签填充|表格插入|图标绘制和插入|删除段落|删除标签
本教程主要实现【Golang处理Word文档模板实现标签填充|表格插入|图标绘制和插入|删除段落|删除标签】。 本文源码:https://gitee.com/songfayuan/go-zero-demo 教程源码分支:master 分支(_examples/word-template/fill-word-template.go&…...
PHP学习:PHP基础
以.php作为后缀结尾的文件,由服务器解析和运行的语言。 一、语法 PHP 脚本可以放在文档中的任何位置。 PHP 脚本以 <?php 开始,以 ?> 结束。 <!DOCTYPE html> <html> <body><h1>My first PHP page</h1><?php …...
Xinstall新玩法:Web直接打开App,用户体验再升级!
在移动互联网时代,App已成为我们日常生活中不可或缺的一部分。然而,在App推广和运营过程中,许多开发者面临着从Web端引流到App的难题。这时,Xinstall作为国内专业的App全渠道统计服务商,提供了一种创新的解决方案——通…...
perf 排查高延迟
高延迟的 2 个场景,触发 perf 录包思路 当前 perf 没有常驻内存,后续提供 perf 常驻内存功能。且 perf 启动需要 0.5~1s,所以,存在 2 个场景 1.频繁连续高延迟(复现后的几秒内,继续频繁复现)&a…...
配置8188eu无线网卡的热点模式
下载驱动 github:8188eu的最新驱动,注意下载5.2.2.4分支 关于这一驱动,不要下载master分支,master分支代码较早,会导致以下两点问题: 1.STA模式下连接wifi信号较差时会卡死 2.无法启动AP模式 所以请务必下载5.2.2.4分…...
为什么 DDoS 攻击偏爱使用 TCP 和 UDP 包?
Distributed Denial of Service (DDoS) 攻击是指攻击者利用多个计算机系统或网络设备(通常是被恶意软件感染的计算机,被称为“僵尸网络”)来淹没目标服务器的资源,导致合法用户无法访问服务。TCP 和 UDP 是两种最常见的用于 DDoS …...
多址技术(FDMA,TDMA,CDMA,帧,时隙)(通俗易懂)
多址技术是一种区分用户的技术。 举个例子,一个基站发出信息,如何确定是发给谁的? 这个技术就是解决这个问题的。 多址技术常见的有三种: 频分多址(FDMA)、时分多址(TDMA)、码分…...
基于 KubeSphere 的 Kubernetes 生产环境部署架构设计及成本分析
转载:基于 KubeSphere 的 Kubernetes 生产环境部署架构设计及成本分析 前言 导图 1. 简介 1.1 架构概要说明 今天分享一个实际小规模生产环境部署架构设计的案例,该架构设计概要说明如下: 本架构设计适用于中小规模(<50)的 Kubernetes …...
RabbitMQ 入门篇
接上一篇《RabbitMQ-安装篇(阿里云主机)-CSDN博客》 安装好RabbitMQ后,我们将开始RabbitMQ的使用,根据官网文档RabbitMQ Tutorials | RabbitMQ,我们一步一步的学习。 1. "Hello World!" 这里先说明几个概…...
wordpress高阶教程/怎么让某个关键词排名上去
目录 Bash 的变量和运算符 什么是变量 变量的分类 用户自定义变量 变量定义 变量调用 变量查看 变量删除 Bash 的变量和运算符 什么是变量 在定义变量时,有一些规则需要遵守:变量名称可以由字母、数字和下划线组成,但是不能以数字…...
wordpress页面视频播放/百度网址大全旧版
转载请注明出处:http://blog.csdn.net/cuiran/article/details/30054835 最近一直研究android的视频直播部分,从最开始的直接播放本地视频文件,到使用http、rtsp,mms和udp.但是发现目前支持UDP TS流的很少。 刚刚接触的Vitamio 网址…...
泊头网站建设/宁波seo网站排名优化公司
为什么80%的码农都做不了架构师?>>> ./config --prefix/usr/local/OpenSSL -fPIC shared 转载于:https://my.oschina.net/eduosi/blog/782783...
自己的网站怎么做跳转/廊坊网络推广公司
网易这回良心了: 7月16日,网易公司旗下云服务商网易数帆宣布开源一款名为 Curve 的高性能分布式存储系统,官方称其性能可达 Ceph 的 1.84 倍。 据官方介绍,Curve 的定位是提供一个高性能、低延迟的存储底座,基于这个…...
wordpress卸载 数据库/seo整站优化服务教程
点击蓝字关注我们UDExpo-Display2月28日消息 2020 年 6 月 19 日,三星电子向 WIPO(世界知识产权局)申请了一项名为 “包括子显示屏及其操作方法的电子设备”的专利,描述了一种手机,其前置摄像头部分可通过切换屏幕分层进行隐藏。该专利已于 1…...
会做网站的公司/2345网址导航智能主板
工具 window10 django2.2.2 mysql5.6 django默认使用mysqlclient连接mysql,但是在window下直接安装mysqlclient不容易成功,可以使用pymysql 代替。 但使用如下方式报错: #settings.py import pymysql pymysql.intall_as_MySQLdb()django 3.…...