文档中心>TI-ONE 训练平台>实践教程>LLM 训练及评测>使用 DeepSeek-R1 蒸馏其他模型的全流程实践

使用 DeepSeek-R1 蒸馏其他模型的全流程实践

最近更新时间:2025-04-02 16:33:12

我的收藏

总览

DeepSeek-R1提供了卓越的推理能力,但是其超大的模型尺寸(671B)会消耗较大的模型推理资源。为此我们可以通过模型蒸馏的方式将 DeepSeek-R1 的推理能力迁移到开源小尺寸模型(例如Llama-3.2-3B)上,从而在保证效果的同时降低推理消耗成本。

模型蒸馏是一种将复杂模型(教师模型)的知识转移到简单模型(学生模型)的技术,旨在压缩模型规模、提升推理速度,同时尽量保持性能。本文将指导您使用TI平台目前已有的产品能力完成模型蒸馏。本实践的总体步骤如下:
1. 步骤一:部署 DeepSeek-R1 推理服务
2. 步骤二:基于 DeepSeek-R1 蒸馏数据
3. 步骤三:使用蒸馏数据集精调学生模型
4. 步骤四:评测学生模型的蒸馏效果
本实践的结果是您将获得一个带思维链推理能力的 Llama3.2-3B-distill 模型,同时在 GSM8K 数据集场景下获得一定的评测效果提升(客观评测准确率由70%提升至77%)。下图为经过蒸馏后的 Llama3.2-3B-distill 模型在线体验对话框:


下文简称 DeepSeek-R1 为“教师模型”,简称 Llama3.2-3B 为“学生模型”,将通过 DeepSeek-R1 推理生成的带长思维链的数据称为蒸馏数据集,官方数据集为原始数据集。

前置准备条件

算力和存储资源准备

1. 教师模型推理所需资源:部署教师模型最低需要1台8卡 PNV6 机型,此处为了获得更高的推理并发,建议使用2台8卡 HCCPNV6 机型分布式部署推理服务,部署资源详细说明见大模型推理所需资源指南。因此在实践开始前,建议纳管至少2台8卡 HCCPNV6 机型,该模式下需要您提前购买好 CVM 机器并添加至 TI 平台资源组,详细操作步骤请参考资源组管理,资源具体费用可参考模型列表及资源/价格参考
2. 精调学生模型所需资源:在本实践场景下,精调Llama3.2-3b模型至少需要4卡 HCCPNV6 机型。
3. 存储所需资源:为了提升训练速度,建议使用CFS Turbo,可参考各场景使用 CFS 文件系统的指引开通 CFS 服务。

物料准备

模型和镜像

本文用于蒸馏的教师模型(DeepSeek-R1)和学生模型(llama3.2-3B-chat)以及运行这些模型的镜像均为平台内置,您无需额外下载。

数据集

本文用于蒸馏的数据集为OpenO1-SFT,在执行后续步骤之前,您需要将此数据集事先下载至本地。
本文用于评测蒸馏效果的数据集为平台支持的 GSM8K(平台已为您将该数据集上传到 COS,您可以直接点击下载)。

代码

本实践所涉及到的代码主要为调用教师模型生成推理结果脚本和数据集的预处理相关脚本,其中教师模型调用脚本您需要提前下载,下载地址如下generate_reasoning_streams.py
数据集的预处理脚本会可在下文实践过程中直接复制获取。


详细步骤

步骤一:部署 DeepSeek-R1 推理服务

为了获取 DeepSeek-R1 的推理结果并用于后续的蒸馏,首先需要在TI平台部署推理服务。您可以参考快速部署和体验 DeepSeek 系列模型完成 DeepSeek-R1 的部署,需要注意的点是:生成蒸馏数据集对于并发要求较高,因此部署方式​​请务必选择多机分布式部署 (推荐使用2机 HCCPNV6 部署)
部署完成之后,您可以通过在线服务 > 在线体验功能,验证服务部署完成,如下图所示。


步骤二:基于 DeepSeek-R1 蒸馏数据

本步骤主要展示在 TI 平台通过数据构建产品模块基于教师模型(DeepSeek-R1)蒸馏数据的全流程。主要流程如下:
1. 新建数据构建任务
2. 调用和验证教师模型推理服务
3. 上传并清洗原始数据集
4. 调用 DeepSeek-R1 生成蒸馏数据集
5. 清洗蒸馏数据集

2.1 新建数据构建任务

腾讯云 TI 平台的数据构建模块内置了常见的数据预处理代码,可以方便用户执行后续的数据清洗等任务,因此我们使用 TI 平台的数据构建功能进行数据准备(关于数据构建的详细操作指南查看 构建 LLM 精调数据):
1. 进入数据构建模块,新建数据构建任务。

2. 新建数据构建开发机。注意,数据构建开发机仅用作调用模型服务和数据预处理,因此不需要 GPU 算力资源,在本实践中,建议使用8C16G资源。同时,为了在后续调用推理服务时可以使用 VPC 高速调用地址,建议使用和教师模型推理服务相同的资源组。


2.2 调用和验证教师模型推理服务

开发机启动完成后,您可以进入 Terminal 终端执行如下命令验证已部署的教师模型推理服务调用链路是否通畅(将下面的<svc-ip>、<ms-id>和<auth_token>换为正确的值(见下图),这里推荐使用 VPC 调用地址发起在线服务调用,可以避免因为 WAF 网关导致的结果被过滤等问题)。
curl -X POST http://<svc-ip>/<ms-id>/v1/chat/completions -H 'Authorization: <auth_token>' -H 'Content-Type: application/json' -d'{ "model": "<ms-id>", "messages": [ { "role": "user", "content": "描述一下您对人工智能的理解。"} ] }'



2.3 上传并清洗原始数据集

1. 上传原始数据。将已经下载到本地的 openO1SFT 数据(OpenO1-SFT.jsonl)上传至开发机内的 /home/tione/notebook/<您的nb-id,该目录会平台自动生成>/single_round_qa_pipeline/raw_dataset_files/ 路径下,如下图所示(该数据集约380M,直接在开发机页面上传大约需要1分钟,您也可以通过腾讯云 COS 中转)。

同样,您需要把 GSM8K 数据上传至开发机的如下路径:/home/tione/notebook/<您的nb-id,该目录平台会自动生成>/test_data/gsm8k(使用解压后文件夹内的 test.jsonl 数据)。

本次我们的精调任务旨在提高学生模型的数据计算能力,在 OpenO1SFT 数据集中,包含了不少与我们精调场景无关的数据(包括代码生成和开放问答等),这些数据加入到蒸馏数据集中,会占用较多的教师模型推理服务调用,增加蒸馏成本。因此,在真正调用之前,我们需要把这部分数据从数据集中去掉,只保留跟蒸馏场景(数学计算)相关的数据。接下来我们在已创建好的数据构建开发机中开始进行数据预处理。
2. 数据采样与解析
在 quick_start.ipynb 文件中,修改原始数据集文件名,并执行采样,将可以看到数据集信息如下:


解析原始数据,注意convert_dict_to_tione_format自定义函数需要根据数据集填写对应的字段,可看到数据分布,如下图所示:


3. 清洗数据
因为我们主要是为了提升数学场景的模型表现,因为我们主要筛选数学和推理相关的问题,通过数据清洗可以自定义清洗规则。分析原始数据可以发现,原始数据存在大量写代码的问题,以及问答的问题,因此我们尝试清洗掉这些 prompt。
首先我们把系统默认的清洗规则通过 config/clean_config.yaml 配置全部关掉,如下图所示,然后编写自定义清洗规则。

import re
from typing import Union, Dict

class CodeChecker:
def __init__(self):
# 定义常见编程语言名称(可以扩展)
self.languages = ["c++", "python", "javascript", "java", "c#", "typescript", "ruby", "php", "program", "tensorflow", "API", "json", "function", "algorithm", "script", "linux", "Kubernete"]

def is_code_like(self, text: str) -> bool:
# 将文本转为小写,以便不区分大小写
lower_text = text.lower()
# 检查文本中是否包含任何编程语言名称
for lang in self.languages:
if lang in lower_text:
return True
return False

# 注册过滤函数
@filter_func.FilterFunctionManager.register(zh_name="包含常见编程名称")
def check_code(text: str, config: Dict = None) -> Union[bool, str]:
checker = CodeChecker()
if checker.is_code_like(text):
return True, "包含常见编程名称"
return False, ""


# 注册过滤函数
@filter_func.FilterFunctionManager.register(zh_name="不包含5个以上数字")
def filter_three_or_more_digits(text: str, config: Dict = None) -> Union[bool, str]:
# 统计文本中数字的个数
num_count = len(re.findall(r'\\d', text))
# 如果数字个数小于等于5,过滤掉
if num_count <= 5:
return True, "不包含5个以上数字"
return False, ""


可以看到,经过清洗之后,我们得到了2.8W数据,并保存在了如下路径: /home/tione/notebook/<nb-id>/single_round_qa_pipeline/intermediate_dataset_files/02_OpenO1-SFT_cleaned.jsonl,数据示例如下:
{"system": "您是腾讯云TI平台智能问答机器人,能理解用户问题并流利解答。", "question": "Anna goes trick-or-treating in a subdivision where she gets 14 pieces of candy per house. However, for every fifth house, she gets 2 mini chocolate bars in addition to the regular candy. Her brother Billy goes trick-or-tricking in a neighboring subdivision where he gets 11 pieces of candy per house. If Anna visits 75 houses and Billy visits 90 houses, how many more pieces of candy does Anna get?", "answer": "<Thought>\\nAlright, let's tackle this problem about Anna and Billy trick-or-treating. So, Anna is going to a subdivision where she gets 14 pieces of candy per house. But wait, there's a twist—every fifth house she visits, she gets 2 extra mini chocolate bars in addition to the regular candy. That's interesting. Her brother Billy, on the other hand, is going to a neighboring subdivision where he consistently gets 11 pieces of candy per house. The question is asking how many more pieces of candy Anna gets compared to Billy after visiting their respective number of houses: Anna visits 75 houses, and Billy visits 90 houses.\\n\\nFirst, let's break down Anna's candy collection. Normally, she gets 14 pieces per house, so if she visits 75 houses, the straightforward calculation would be 14 pieces multiplied by 75 houses. Let me do that: 14 times 75 equals 1050 pieces of candy. So, without considering the extra mini chocolate bars, Anna would have 1050 pieces.\\n\\nBut wait, there's more to the story. Every fifth house, she gets 2 extra mini chocolate bars. How many times does she hit every fifth house in her 75-house adventure? Well, 75 divided by 5 equals 15. That means she gets those extra 2 pieces of candy 15 times. So, 15 occasions multiplied by 2 extra pieces equal 30 extra pieces of candy. Adding that to her initial total, 1050 plus 30 equals 1080 pieces of candy. Anna ends up with 1080 pieces.\\n\\nNow, let's look at Billy. He gets 11 pieces of candy per house, and he visits 90 houses. So, 11 multiplied by 90 houses equals 990 pieces of candy for Billy.\\n\\nThe question is asking how many more pieces of candy Anna gets compared to Billy. So, I need to subtract Billy's total from Anna's total. That would be 1080 pieces (Anna) minus 990 pieces (Billy), which equals 90 pieces. Therefore, Anna gets 90 more pieces of candy than Billy.\\n\\nBut hold on a second—let me double-check to make sure I didn't make any mistakes. For Anna: 14 pieces per house times 75 houses is indeed 1050, plus 30 extra pieces for the 15 houses where she gets bonus candy equals 1080. For Billy: 11 pieces per house times 90 houses is definitely 990. Subtracting 990 from 1080 does give us 90. Yes, that seems correct.\\n\\nIs there another way to approach this? Maybe by calculating the average per house including the extras for Anna. Let's see: Anna gets an extra 2 pieces every 5 houses, so on average, that's an extra 0.4 pieces per house (since 2 divided by 5 is 0.4). So, her average per house would be 14 plus 0.4, which is 14.4 pieces per house. Multiplying that by 75 houses: 14.4 times 75 equals 1080. Yep, that matches the previous calculation.\\n\\nAlternatively, could there be a way to visualize this? If I imagine walking down a street with 75 houses, every fifth house has an extra candy incentive. Counting those extra pieces could be another way to confirm the total. But I've already done the math, and it seems consistent.\\n\\nAnother angle: What if I considered the total number of extra pieces Anna gets compared to Billy? Since Anna gets 2 extra pieces every 5 houses, and she visits 75 houses, that's 15 times 2 extra pieces, which is 30 extra pieces. Billy doesn't get any extra pieces, so all of Anna's advantage comes from that 30-piece difference, plus the difference in their per-house candy rates.\\n\\nComparing their per-house rates: Anna gets 14 per house, Billy gets 11. That's a difference of 3 pieces per house. Over 75 houses, that's 225 extra pieces from the per-house rate alone. Wait a minute, that doesn't match my earlier total difference. Did I make a miscalculation here?\\n\\nAh, I see the mistake. Billy visits 90 houses, not 75. So, for a fair comparison, Anna's 75 houses at 14 per house equals 1050, and with the extra 30 pieces, that's 1080. Billy's 90 houses at 11 per house is 990. The difference is still 90 pieces. So, my initial calculation stands.\\n\\nIn conclusion, after visiting their respective numbers of houses, Anna ends up with 90 more pieces of candy than Billy.\\n\\n</Thought>\\n<Output>\\nAnna gets 90 more pieces of candy than Billy.\\n</Output>"}

接下来我们去调用R1生成这2.8W条数据的思维链结果。

2.4 调用 DeepSeek-R1 生成蒸馏数据集

首先,我们把上述清洗后的数据保存到单独的目录用于蒸馏数据集生成:
cd /home/tione/notebook/<nb-id>
mkdir -p deepseek_r1
cp single_round_qa_pipeline/intermediate_dataset_files/02_OpenO1-SFT_cleaned.jsonl deepseek_r1/OpenO1-SFT-math.jsonl
接着,调用数据生成脚本获得R1推理结果(我们提供了基于 hugging face官方脚本适配TI平台之后的脚本,您可以直接下载至开发机,使用此脚本,您只需指定数据集地址之后,就会自动调用教师模型推理服务并生成蒸馏用的数据集(保存至 jsonl 文件中))。
pip3 install datasets aiofiles uvloop
python3 generate_reasoning_stream.py -i deepseek_r1/OpenO1-SFT-math.jsonl -o deepseek_r1/OpenO1-SFT-math-generate.jsonl --prompt-column question --max-concurrent 100 --model-addr http://<svc-ip>/<ms-id> --model-token <ms-token> --model-service <ms-id>
prompt-column 是数据集实际的 prompt 所在字段名称,这里清洗完后的数据字段名称为 question。
max-concurrent 参数可以指定最大并发,2机部署的R1服务可以支持最高128并发调用,吞吐量比较高,建议先设置为1进行测试,再改为100实际执行运行。
num-generations 参数可以指定每个 prompt 调用 deepseek-R1 生成多少条数据,此处我们指定为1。
可以通过 python3 generate_reasoning_stream.py -h 查看使用方法
注意:由于R1模型较大,此步骤生成数据耗时可能会很长;如果您想提升推理速度,可以考虑购买更多 HCCPNV6 实例并增加 DeepSeek-R1 副本数。
执行如下,可以看到执行的进度条。等待所有数据全部生成,这个过程一般比较长。

本文假定您通过调用部署好的 DeepSeek-R1 生成了蒸馏数据集,同时通过对2.8W条数据进行随机抽样,生成2.5W条数据,并保存到了如下开发机挂载路径 /home/tione/notebook/<nb-id>/deepseek_r1​,也就是 ​cfs:///ds-distill-demo0214/<nb-id>/deepseek_r1/OpenO1-SFT-math-generate.jsonl
生成的数据示例如下,其中 question/answer 为原始数据集的 prompt 和回答。generations 数组是我们调用模型生成的回答,目前设置 num-generations为1 ,所以数组长度为1。
img
img


2.5 清洗蒸馏数据集

DeepSeek-R1 生成的带思维链的数据并不能直接送去做训练(例如生成结果错误,超长,或者被截断等),我们还需要再做进一步的清洗才能生成质量合适的数据集。注意: 判断R1推理结果是否正确需要引入复杂的判断逻辑(例如通过 MathVerify 等),本文只展示简单的基于规则就可判断的数据清洗(例如数据超长,思维链不完整等)。
1. 数据分析
我们把生成的文件 OpenO1-SFT-math-generate.jsonl 拷贝到数据构建的 raw_dataset_files目录下。
我们指定该文件为输入,再次执行,一共生成2.5W条数据。


接下来我们进行token的分布分析,同时我们将deepseek的tokenizer下载至notebook并解压至 /home/tione/notebook/<nb-id>/tokenizer。
cd /home/tione/notebook/<nb-id>/single_round_qa_pipeline/
wget https://tione-public-cos-1308945662.cos.ap-shanghai.myqcloud.com/tools/deepseek_tools/deepseek_tokenizer.tgz
tar xf deepseek_tokenizer.tgz
修改数据解析代码如下图所示:
# 解析数据

# 解析后的数据输出路径,jsonl 格式,建议输出统一用 "intermediate_dataset_files" 文件夹进行管理
step1_out_file = os.path.join(
"intermediate_dataset_files", "01_{}_parsed.jsonl".format(dataset_name)
)

import random
# 您可以覆写这个函数来实现对应不同json格式的解析,对于您的输入文件,我们会解析成多行的json,并把json格式转成python dict格式。
# 在该函数中,您根据采样的json schema把对应格式的dict转成tione平台指定的格式{"system": "xxxx", "question": "xxxx", "answer": "xxxx"}即可。
def convert_dict_to_tione_format(item: dict) -> dict:
systems = ["You are an AI assistant that helps solve mathematical problems.",
"You will be given a problem. Please reason step by step, and put your final answer within \\\\boxed{{}}:\\n"]
try:
question = item["question"]
answer = item["generations"][0]
# 样本构造:获取系统角色字段“system”,具体的内容用户可按需修改
# 构造tione要求的样本格式
convert_dict = {
"system": random.choice(systems),
"question": question,
"answer": answer,
}
except:
# 数据解析异常
return None
return convert_dict

# 使用同一目录下的 step1_parse_raw_data.py 中定义的函数进行原始数据解析
step1_parse_raw_data.parse_raw_data_single_round_qa(step1_in_file, step1_out_file, record_convert_fn=convert_dict_to_tione_format)

#参数tokenizer_model_path可以指定计算token的本地模型路径,默认为python/len,表示不使用大模型,直接计算字符串的长度;
tokenizer_model_path = "./deepseek_tokenizer"

# 解析完成后,对步骤一解析完成的输出文件执行原始数据统计
# 其中参数num_bins可以指定分布直方图平均分桶的桶数,当num_bins=0使用系统内置的桶,默认值为10;
analysis_raw_data.statistic_raw_data_single_round_qa(step1_out_file, num_bins = 0, tokenizer_model_path = tokenizer_model_path)

从数据解析步骤的输出我们可以清晰的看到数据的 token 长度分布。


2. 数据清洗
为了提升训练速度,我们打算清洗掉token长度在4k以上的推理结果。同时,为了确保数据的质量,我们打算只保留思维链完整的推理结果。为此,我们自定义如下过滤函数并执行清洗:
from tools import data_tokenizer
from typing import Union, Dict
tokenizer = data_tokenizer.Tokenizer("./deepseek_tokenizer")
length_threshold = 4096

# 注册过滤函数
@filter_func.FilterFunctionManager.register(zh_name="token长度过滤")
def check_token_len(text: str, config: Dict = None) -> Union[bool, str]:
text_len = tokenizer.tokenize_len(text)
if text_len > length_threshold :
return True, "长度短于{}".format(length_threshold)
return False, ""

# 注册过滤函数
@filter_func.FilterFunctionManager.register(zh_name="检查思维链是否完整")
def check_think_tag(text: str, config: Dict = None) -> Union[bool, str]:
if "<think>" not in text or "</think>" not in text:
return True, "思维链不完整"
return False, ""
运行以下执行代码:

处理后剩余1.5W条数据。

3. 去重
直接复用数据构建的去重功能,修改如下参数,我们可以去除相似度较高的样本。


4. 转换训练格式
TI 平台内置大模型要求输入数据格式为:
{
"system": "xxx",
"conversation": [
{
"prompt": "xxx",
"response": "xxx"
}
]
}
为了得到这种格式的数据,我们可以通过执行数据构建的最后一步:训练格式生成,生成最终用于蒸馏小模型的数据。

至此,我们完成了蒸馏数据集的构建。
注意:数据质量对于精调效果影响巨大,本文只展示了简单的数据预处理逻辑,当您发现精调结果不符合预期时,您可能需要自己编写更复杂的数据处理逻辑来提升数据质量。

步骤三:使用蒸馏数据集精调学生模型

有了蒸馏数据集之后,就可以开始蒸馏学生模型(以精调内置 llama-3.2-3b 模型为例)。您可以使用如下步骤发起精调任务(详细流程请参考精调内置大模型指引文档)发起针对 llama-3.2-3B-chat 的精调任务。
1. 在新建任务详情页面,选择内置的 llama3.2_3b_chat 模型,并指定训练需要的资源(推荐您至少选择4卡训练资源,按照8卡整台机器均分 CPU 和内存,因此建议配比190核1107G,如果您有充足的计算资源可以指定更多以加快训练速度)。

在新建任务详情页面,将平台内置数据删除后,替换为业务数据。此外,由于 DeepSeek-R1 的包含思维链的结果较长,您还需要调整超参以匹配您的蒸馏数据集(由于数据清洗的时候,针对的是 answer 做过滤,实际训练的时候是 question + answer,因此本文指定的 MaxSequenceLength 为8K而不是前文清洗设置的4K)。此外,请注意正确配置您的模型输出路径,以便后续创建评测任务用于验证训练后的效果(本文假定执行完的模型被保存到了​cfs:///demo/deepseek/train/distill)。本文精调时,配置的超参如下(超参的详细解释,请参考精调内置开源大模型):

任务开始运行后,您可以通过查看监控指标和登录训练实例容器等方式进行训练过程监控,也可以点击页面中的 TensorBoard 按钮自动打开指标监控面板,可以观察到loss的收敛情况,如下图所示:



任务运行过程中,平台会自动在配置的 cfs 输出路径内保存精调后的模型 Checkpoint。此时,您可以直接在 Checkpoint 页面创建轻量体验任务体验蒸馏后的模型效果(客观评测建议配置的资源为单卡47C276G)。

等到轻量体验服务运行后,可点击体验,对模型进行提问,可看到蒸馏后的 Checkpoint 模型已经开始有思考能力,并且输出了长思维链。



步骤四:评测学生模型的蒸馏效果

在步骤三,我们可以看到训练生成的学生模型 Checkpoint 已经逐渐拥有长思维链思考能力,我们进一步通过客观评测对蒸馏前后的模型进行打分,以观察模型的精调效果。
在 TI 平台的模型评测模块新建客观评测任务,填写的自定义评测集地址即为步骤2.3上传的 GSM8K 数据集;在选择待评测的模型处同时将蒸馏前内置 Llama3.2_3b_chat 模型和本次精调后生成的模型同时选中,进行批量评测,如下图所示:

其中,推理超参设置中配置参数为:
{
"temperature": 0.6,
"top_p": 0.95,
"frequency_penalty": 0,
"seed": 12345
}
等待评测跑完之后,您就可以获得蒸馏前后的模型在指定数据集上的打分指标,如下图所示,我们可以看到在该评测集上蒸馏后模型相比于蒸馏前模型在指标上有明显的提升;同时,结合上述轻量体验的对话结果,模型也已具备了长思维链输出能力。