当前位置: 首页 > news >正文

从零复现DeepSeek R1:从V3中对MoE、MLA、MTP的实现,到Open R1对R1中SFT、GRPO的实现

前言

虽然我司从23年起,便逐步从教育为主转型到了科技为主,但不代表教育业务便没有了

随着DeepSeek特别是R1、其次V3模型的大火,我司七月在线的大模型线上营群里一学员朋友DIFY问道:校长好,deepseek 的课程目前有多少内容啦,我想要参与学习,想请问一下关于v3和r1复现的课程有吗,不用那么大参数量,小尺寸就好

实话讲,我一开始确实没咋重点考虑R1和V3复现的问题,一来,想着毕竟人家开源了,二来,即便有诸如Open R1这种复现,但效果和原装的相比还是差太多

但后来有三点改变了我的看法

  1. 对于V3 都没有开源他们最核心的训练数据、训练代码
    比如V3只是开源了模型权重、模型结构(modeling_deepseek)和推理脚本
  2. 虽然Open-R1 只是复现了R1正式版的前两个阶段(如此文所述,R1正式版 有4个阶段)
    虽然效果上 不会太好「所以之前没咋关注 因为对于作商用项目的我司来讲,其落地潜力有限
    但毕竟只是一个从零开始的开源小项目 也没法要求太高,所以放到课程中 还是有一定的科研价值的
  3. 如此,综上可得,或如DIFY所说

加之,我已经 把deepseek各个模型的原理 写透彻了,接下来,确实准备抠下他们已经对外开源的部分代码,然后再带头组织我司部分同事及相关朋友,填补一下无论是V3、R1还是Open R1缺失的代码与流程

以上种种,使得本文来了

最后,我特别强调一下,如果对deepseek各类模型及各类算法还不熟悉的话,强烈建议先看对应的原理:《火爆全球的DeepSeek系列模型》,可以看到

  1. 24年1.5日,DeepSeek LLM发布,没太多创新
    类似llama那一套「llama1的RoPE/RMSNorm/SwiGLU + llama2 70B或llama3的GQA
  2. 24年1.11日,DeepSeekMoE,开启创新之路
    提出细粒度专家分割和共享专家隔离,以及一系列负载均衡
  3. 24年1.25,发布DeepSeek-Coder
    24年2月,发布DeepSeekMath
    提出了Group Relative Policy Optimization(简称GRPO),以替代PPO——舍弃critic模型
  4. 24年5.7日,DeepSeek-V2
    提出多头潜在注意力MLA且改进MoE
    其中的这个MLA是整个deepseek系列最大的几个创新之一,且由此引发了各大厂商百万token的大幅降价
  5. 24年12.26日,DeepSeek-V3发布
    在MoE、GRPO、MLA基础上提出Multi-Token预测,且含FP8训练
    大家纷纷把它和Llama 3.1 405B对比,V3以极低的训练成本造就超强的效果,再度出圈
  6. 25年1.20日,DeepSeek R1发布
    一方面,提出舍弃SFT、纯RL训练大模型的范式,且效果不错
    二方面,性能比肩o1甚至略微超越之
    三方面,直接公布思维链且免费,不藏着掖着,相比o1,对用户极度友好

    至此爆了,火爆全球

总之,原理熟悉之后,再看本文的源码实现,事半功倍

第一部分 DeepSeek V3源码解读:MoE、MLA、MTP的实现

首先,我们来看下V3对外开源的内容

类别开源内容未开源内容
模型权重FP8/BF16 格式的基础模型和聊天版本
相当于开源了FP8 格式模型权重(包含基础模型和聊天优化版本),支持通过脚本转换为 BF16 格式进行推理——用户可通过 Hugging Face 平台直接下载
训练数据
数据清洗、预处理代码或数据集链接,比如 14.8 万亿 Token 的预训练数据集或具体数据来源
架构代码
包含模型定义的核心代码(如model.py),比如MLA、DeepSeekMoE、MTP 模块实现,但仅限架构定义,不包含完整的训练流程
从 DeepSeek-R1 提取知识的详细实现也未提供
训练框架FP8 混合精度框架、上下文扩展工具完整分布式训练代码、超参数配置
比如分布式训练框架(如 DualPipe 流水线并行)、FP8 混合精度训练的具体实现
推理工具主流框架适配方案、Hugging Face 示例
即提供基于 Hugging Face 或 vLLM 的简单推理脚本(如 inference.py),但未公开生产级优化代码(如动态批处理、显存优化等)
生产级优化内核
比如企业级部署工具(如 DeepSeek 自研的推理引擎),涉及动态批处理、显存管理等等

通过此文《一文通透让Meta恐慌的DeepSeek-V3:在MoE、GRPO、MLA基础上提出Multi-Token预测(含FP8训练详解)》可知,在模型的架构层面,V3主要就在MoE、GRPO、MLA的基础上提出了Multi-Token预测

故接下来,咱们便来逐步剖析

1.1 对DeepSeekMoE的实现:涉及RoPE、MoE层、Norm层

根据MoE的结构可知,我们需要实现Norm层、attention层、MoE层,考虑到V3中的attention是潜在多头注意力——即MLA类实现了多头注意力层,支持低秩查询投影和键值投影,并根据配置选项选择不同的注意力实现,故放到下一节中介绍(下图来源于Switch Transformers)

本节中,我们结合V3代码库中的model.py看下这几个部分的实现

  • precompute_freqs_cis函数预计算了用于旋转位置嵌入的频率复数指数值
  • apply_rotary_emb函数将旋转位置嵌入应用于输入张量
  • MLP类实现了一个多层感知机,用于前馈网络层
  • Gate类实现了一个门控机制,用于在专家模型中路由输入
  • Expert类实现了专家模型中的专家层
  • MoE类实现了专家模型模块,包含多个专家和一个共享专家
  • RMSNorm类实现了均方根层归一化,用于对输入张量进行归一化处理
  • Block类实现了Transformer块,结合了注意力层和前馈网络层

1.1.1 RoPE的实现

model.py中,关于RoPE的实现涉及以下两个函数

  • precompute_freqs_cis函数预计算了用于旋转位置嵌入的频率复数指数值
  • apply_rotary_emb函数将旋转位置嵌入应用于输入张量

关于RoPE的更多细节,详见此文《一文通透位置编码:从标准位置编码、旋转位置编码RoPE到ALiBi、LLaMA 2 Long(含NTK-aware简介)》

1.1.1.1 precompute_freqs_cis函数

precompute_freqs_cis函数用于预计算旋转位置嵌入的基于频率的复数指数值。该函数接收一个ModelArgs类型的参数args,其中包含了位置嵌入的相关参数。函数返回一个预计算的复数指数值的张量,用于位置嵌入

def precompute_freqs_cis(args: ModelArgs) -> torch.Tensor:"""预计算用于旋转位置嵌入的基于频率的复数指数值。参数:args (ModelArgs): 包含位置嵌入参数的模型参数。返回:torch.Tensor: 预计算的用于位置嵌入的复数指数值。"""

函数首先从args中提取相关参数,包括嵌入维度dim、最大序列长度seqlen、快速和慢速beta修正因子beta_fast和beta_slow、基数base和缩放因子factor

    dim = args.qk_rope_head_dim      # 获取查询键旋转嵌入的维度seqlen = args.max_seq_len        # 获取最大序列长度beta_fast = args.beta_fast       # 获取快速beta修正因子beta_slow = args.beta_slow       # 获取慢速beta修正因子base = args.rope_theta           # 获取旋转位置编码的基数factor = args.rope_factor        # 获取扩展序列长度的缩放因子

接着,定义了三个辅助函数:find_correction_dim、find_correction_range和linear_ramp_factor

  1. find_correction_dim函数计算旋转位置嵌入中给定旋转次数的修正维度
    它使用输入参数计算修正维度,并返回该值
        def find_correction_dim(num_rotations, dim, base, max_seq_len):"""计算旋转位置嵌入中给定旋转次数的修正维度。参数:num_rotations (float): 要计算修正的旋转次数dim (int): 嵌入空间的维度base (float): 指数计算的基数max_seq_len (int): 最大序列长度返回:float: 基于输入参数的修正维度"""return dim * math.log(max_seq_len / (num_rotations * 2 * math.pi)) / (2 * math.log(base))  # 计算修正维度
  2. find_correction_range函数计算旋转位置嵌入的修正维度范围
    它接收旋转次数的上下界、嵌入维度、基数和最大序列长度作为参数,返回修正维度的范围
        def find_correction_range(low_rot, high_rot, dim, base, max_seq_len):"""计算旋转位置嵌入的修正维度范围参数:low_rot (float): 旋转次数的下界high_rot (float): 旋转次数的上界dim (int): 嵌入空间的维度base (float): 指数计算的基数max_seq_len (int): 最大序列长度返回:Tuple[int, int]: 修正维度的范围(低,高),并限制在有效索引范围内"""low = math.floor(find_correction_dim(low_rot, dim, base, max_seq_len))  # 计算低修正维度high = math.ceil(find_correction_dim(high_rot, dim, base, max_seq_len))  # 计算高修正维度return max(low, 0), min(high, dim-1)  # 返回修正维度范围
  3. linear_ramp_factor函数计算用于在最小值和最大值之间平滑值的线性斜坡函数
    它返回一个张量,该张量的值在0和1之间线性插值,并限制在[0, 1]范围内
        def linear_ramp_factor(min, max, dim):"""计算用于在最小值和最大值之间平滑值的线性斜坡函数参数:min (float): 斜坡函数的最小值max (float): 斜坡函数的最大值dim (int): 斜坡张量的维度返回:torch.Tensor: 形状为(dim,)的张量,值在0和1之间线性插值,并限制在[0, 1]范围内。"""if min == max:      # 如果最小值等于最大值max += 0.001          # 增加最大值以避免除零错误linear_func = (torch.arange(dim, dtype=torch.float32) - min) / (max - min)  # 计算线性函数ramp_func = torch.clamp(linear_func, 0, 1)  # 限制线性函数的值在0到1之间return ramp_func          # 返回线性斜坡函数

接下来,函数计算频率值freqs,这些值是基于嵌入维度和基数的指数函数。如果序列长度大于原始序列长度,则应用修正范围和平滑因子来调整频率值

    # 计算频率值freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))  if seqlen > args.original_seq_len:  # 如果序列长度大于原始序列长度low, high = find_correction_range(beta_fast, beta_slow, dim, base, args.original_seq_len)          # 计算修正范围smooth = 1 - linear_ramp_factor(low, high, dim // 2)      # 计算平滑因子freqs = freqs / factor * (1 - smooth) + freqs * smooth    # 调整频率值

最后,函数计算时间步长t,并使用外积计算频率值的复数指数表示,返回预计算的复数指数值张量freqs_cis

    t = torch.arange(seqlen)           # 生成时间步长freqs = torch.outer(t, freqs)      # 计算频率值的外积freqs_cis = torch.polar(torch.ones_like(freqs), freqs)  # 计算频率值的复数指数表示return freqs_cis                   # 返回预计算的复数指数值
1.1.1.2 apply_rotary_emb的实现

apply_rotary_emb函数用于将旋转位置嵌入应用到输入张量x上。该函数接收两个参数:x是包含位置嵌入的输入张量,freqs_cis是预计算的复数指数值张量,用于位置嵌入

def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:"""将旋转位置嵌入应用于输入张量参数:x (torch.Tensor): 包含要应用位置嵌入的输入张量freqs_cis (torch.Tensor): 预计算的用于位置嵌入的复数指数值返回:torch.Tensor: 应用了旋转嵌入的张量"""
  1. 首先,函数保存输入张量的原始数据类型dtype
        dtype = x.dtype  # 获取输入张量的数据类型
  2. 然后,将输入张量x转换为浮点类型,并重新调整其形状,使其最后一个维度的大小变为2,以便视为复数
        x = torch.view_as_complex(x.float().view(*x.shape[:-1], -1, 2))  # 将输入张量视为复数
  3. 接着,函数将x视为复数张量函数将freqs_cis调整形状,使其与输入张量的形状匹配。具体来说,freqs_cis的形状调整为(1, 序列长度, 1, 嵌入维度/2),以便在后续计算中进行广播
        freqs_cis = freqs_cis.view(1, x.size(1), 1, x.size(-1))  # 调整频率值的形状
  4. 然后,函数将输入张量x与freqs_cis相乘,得到应用了旋转位置嵌入的复数张量。接着,将结果转换回实数张量,并将其形状调整为原始形状
        y = torch.view_as_real(x * freqs_cis).flatten(3)  # 计算应用旋转嵌入后的张量
  5. 最后,函数将结果张量转换回原始数据类型,并返回该张量。这样,输入张量x就应用了旋转位置嵌入
        return y.to(dtype)  # 返回转换为原始数据类型的张量

1.1.2 对MoE层的实现:包含MLP类、Gate类、Expert类、MoE类

接下来,我们来看MoE的实现

涉及如下这几个函数的实现

  • MLP类实现了一个多层感知机,用于前馈网络层
  • Gate类实现了一个门控机制,用于在专家模型中路由输入
  • Expert类实现了专家模型中的专家层
  • MoE类实现了专家模型模块,包含多个专家和一个共享专家
1.2.1.1 MLP类的实现——多层感知机,用于前馈层

MLP类实现了一个多层感知机(MLP),用于前馈层。该类继承自nn.Module,并包含三个线性层:w1、w2和w3。这些线性层分别用于输入到隐藏层的转换、隐藏层到输出层的转换以及特征转换

class MLP(nn.Module):"""多层感知机(MLP),用于前馈层属性:w1 (nn.Module): 输入到隐藏层的线性层w2 (nn.Module): 隐藏层到输出层的线性层w3 (nn.Module): 额外的特征转换线性层"""
  1. 在初始化方法__init__中
    MLP类接收两个参数:dim表示输入和输出的维度,inter_dim表示隐藏层的维度
        def __init__(self, dim: int, inter_dim: int):"""初始化MLP层。参数dim (int): 输入和输出的维度inter_dim (int): 隐藏层的维度"""
    w1和w3是列并行线性层(ColumnParallelLinear),用于将输入维度转换为隐藏层维度
    w2是行并行线性层(RowParallelLinear),用于将隐藏层维度转换回输入维度
            self.w1 = ColumnParallelLinear(dim, inter_dim)   # 定义输入到隐藏层的列并行线性层self.w2 = RowParallelLinear(inter_dim, dim)      # 定义隐藏层到输出层的行并行线性层self.w3 = ColumnParallelLinear(dim, inter_dim)   # 定义额外的特征转换列并行线性层
1.2.1.2 门控网络Gate类的实现——输入路由的门控机制

Gate类实现了一个用于混合专家(MoE)模型中的输入路由的门控机制

一般就两个计算公式

类似此文《一文速览DeepSeekMoE:从Mixtral 8x7B到DeepSeekMoE(含DeepSeek LLM的简介)》所述,如果每个token选择2个专家,则门控网络的权重矩阵计算对应2个专家的权重,比如w1,w2,然后做softmax,最后与2个专家的输出expert1、expert做加权求和


类似
softmax(X × w1) × expert1 + softmax(X× w2) × expert2

该类继承自nn.Module,并包含多个属性

class Gate(nn.Module):"""混合专家(MoE)模型中用于路由输入的门控机制。属性:dim (int): 输入特征的维度topk (int): 每个输入激活的顶级专家数量n_groups (int): 路由组的数量topk_groups (int): 路由输入的组数score_func (str): 评分函数('softmax'或'sigmoid')route_scale (float): 路由权重的缩放因子weight (torch.nn.Parameter): 门控机制的可学习权重bias (Optional[torch.nn.Parameter]): 门控机制的可选偏置项"""
  1. 在初始化方法__init__中,Gate类接收一个ModelArgs类型的参数args,其中包含了门控机制的参数
        def __init__(self, args: ModelArgs):"""初始化门控模块。参数:args (ModelArgs): 包含门控参数的模型参数。"""super().__init__()               # 调用父类的初始化方法self.dim = args.dim              # 设置输入特征的维度self.topk = args.n_activated_experts       # 设置每个输入激活的顶级专家数量self.n_groups = args.n_expert_groups       # 设置路由组的数量self.topk_groups = args.n_limited_groups   # 设置路由输入的组数self.score_func = args.score_func          # 设置评分函数self.route_scale = args.route_scale        # 设置路由权重的缩放因子self.weight = nn.Parameter(torch.empty(args.n_routed_experts, args.dim))  # 初始化可学习权重self.bias = nn.Parameter(torch.empty(args.n_routed_experts)) if self.dim == 7168 else None  # 初始化可选偏置项
    根据这些参数,类初始化了各个属性,并创建了权重和偏置项的量
  2. 在前向传播方法forward中,Gate类接收一个输入张量x
        def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:"""门控机制的前向传播。参数:x (torch.Tensor): 输入张量。返回:Tuple[torch.Tensor, torch.Tensor]: 路由权重和选择的专家索引。"""
    首先,输入张量通过线性变换函数linear与权重weight相乘,得到评分`score`
            scores = linear(x, self.weight)  # 计算输入张量与权重的线性变换,得到评分
    根据评分函数score_func的不同,评分可以通过softmax或sigmoid函数进行归一化
            if self.score_func == "softmax":       # 如果评分函数是softmaxscores = scores.softmax(dim=-1, dtype=torch.float32)  # 对评分进行softmax归一化else:scores = scores.sigmoid()          # 对评分进行sigmoid归一化
    然后,如果存在偏置项bias,则将其加到评分上
            original_scores = scores      # 保存原始评分if self.bias is not None:            # 如果存在偏置项scores = scores + self.bias      # 将偏置项加到评分上
    接下来,如果路由组的数量n_groups大于1,评分将被重新调整形状,并计算每组的最大评分或前两个评分的和
           if self.n_groups > 1:           # 如果路由组的数量大于1scores = scores.view(x.size(0), self.n_groups, -1)       # 调整评分的形状if self.bias is None:      # 如果没有偏置项group_scores = scores.amax(dim=-1)      # 计算每组的最大评分else:  group_scores = scores.topk(2, dim=-1)[0].sum(dim=-1)  # 计算每组前两个评分的和
    然后,选择顶级组的索引,并创建一个掩码,将评分与掩码相乘并展平
                indices = group_scores.topk(self.topk_groups, dim=-1)[1]  # 选择顶级组的索引mask = torch.zeros_like(scores[..., 0]).scatter_(1, indices, True)  # 创建掩码scores = (scores * mask.unsqueeze(-1)).flatten(1)          # 将评分与掩码相乘并展平
1.2.1.3 Expert类的实现:MoE模型中的专家层

Expert类实现了混合专家(MoE)模型中的专家层。该类继承自nn.Module,并包含三个线性层:w1、w2和w3。这些线性层分别用于输入到隐藏层的转换、隐藏层到输出层的转换以及特征转换。

class Expert(nn.Module):"""混合专家(MoE)模型中的专家层属性:w1 (nn.Module): 输入到隐藏层的线性层w2 (nn.Module): 隐藏层到输出层的线性层w3 (nn.Module): 额外的特征转换线性层"""
  1. 在初始化方法__init__中,Expert类接收两个参数:dim表示输入和输出的维度,inter_dim表示隐藏层的维度
        def __init__(self, dim: int, inter_dim: int):"""初始化专家层。参数:dim (int): 输入和输出的维度inter_dim (int): 隐藏层的维度"""super().__init__()  # 调用父类的初始化方法
    w1是一个线性层,用于将输入维度转换为隐藏层维度
            self.w1 = Linear(dim, inter_dim)  # 定义输入到隐藏层的线性层
    w2是另一个线性层,用于将隐藏层维度转换回输入维度
            self.w2 = Linear(inter_dim, dim)  # 定义隐藏层到输出层的线性层
    w3是一个额外的线性层,用于特征转换
            self.w3 = Linear(dim, inter_dim)  # 定义额外的特征转换线性层
  2. 在前向传播方法forward中,Expert类接收一个输入张量x
        def forward(self, x: torch.Tensor) -> torch.Tensor:"""专家层的前向传播。参数:x (torch.Tensor): 输入张量返回:torch.Tensor: 经过专家层计算后的输出张量"""
    首先,输入张量通过w1线性层,并应用SiLU激活函数(F.silu)
    然后,结果与通过w3线性层的输入张量相乘
    最后,乘积通过w2线性层,得到输出张量
            # 计算前向传播,应用SiLU激活函数并进行特征转换return self.w2(F.silu(self.w1(x)) * self.w3(x))
1.2.1.4 MoE类:实现了专家模型模块,包含多个专家和一个共享专家

首先,关于什么是共享专家,可以详见此文 《一文速览DeepSeekMoE:从Mixtral 8x7B到DeepSeekMoE(含DeepSeek LLM的简介)》所述

其次,我们来看V3代码库里的model.py中对这一部分的实现

// 待更

1.1.3 Norm层的实现

// 待更

1.2 对多头潜在注意力MLA的实现

// 待更

1.3 对多token预测MTP的实现

// 待更

第二部分 Open R1——对DeepSeek R1正式版完整训练流程前两个阶段的复现

其次,再看R1对外开源的内容,根据R1的GitHub可知

类别开源内容未开源内容
模型权重R1、R1-Zero 及蒸馏模型权重(MIT 协议)原始训练数据
未公开冷启动数据、RL 训练数据集或合成数据的具体内容,仅提供依赖的公开数据集名称(如 AI-MO、NuminaMath-TIR)
技术文档GRPO 算法、奖励系统设计、冷启动流程等技术报告训练代码,比如分布式训练代码细节
训练工具合成数据生成脚本、评估基准代码完整 RL 训练框架
推理支持API 接口、本地部署方案、框架适配指南生产级优化内核
即动态显存管理、生产级批处理等企业级部署工具未开源

不过,有意思的是,有个开源项目——Open R1倒是复现了R1正式版完整训练流程的前两个阶段「以Qwen2.5-1.5B为基础,以deepseek-R1的训练过程打造」,并把代码开源了,其GitHub仓库主要包括:

  • src/open_r1:包含训练和评估模型以及生成合成数据的脚本:
    grpo.py:在给定的数据集上使用 GRPO 训练模型
    sft.py:在数据集上执行模型的简单 SFT
    evaluate.py:在 R1 基准上评估模型
    generate.py:使用Distilabel从模型生成合成数据
  • Makefile:包含利用上述脚本的 R1 管道中每个步骤的易于运行的命令

且如下图所示,分别实现了

  1. 从 DeepSeek-R1 中提取高质量语料库来复现 R1-Distill 模型
    这里有个很重要的问题是,到底如何从R1中提取的高质量语料库
    其实如Open R1的GitHub所说,从 DeepSeek-R1 提炼出的具有推理轨迹的数据集(例如Bespoke-Stratos-17k)上运行 SFT
  2. 基于DeepSeek V3 创建 R1-Zero 的纯 RL 管道
  3. 复现R1正式版完整训练流程的前两个阶段(SFT + 规则奖励下的RL),其中比较有价值的便是对GRPO的实现
    毕竟完整的R1正式版训练流程有4个阶段呢
    阶段一 冷启动SFT阶段二 规则奖励下的RL
    R1-Zero模型生成的冷启动数据:微调V3面向推理的RL:结合三个规则奖励——准确率奖励、格式奖励、语言一致性奖励
    阶段三 增强SFT阶段四 规则+偏好奖励下的RL

    来自阶段二模型的60w推理数据

    和V3模型的20w非推理数据:微调V3

    全场景RL

    规则奖励、偏好奖励

我司也会在这个课程《DeepSeek原理与项目实战营》里讲一下这个Open R1的复现思路,及深入解读其源码,以帮助更多人可以更好的用好该Open R1

// 待更


http://www.mrgr.cn/news/90233.html

相关文章:

  • 操作系统|ARM和X86的区别,存储,指令集
  • 用Kibana实现Elasticsearch索引的增删改查:实战指南
  • Windows 系统下使用 Ollama 离线部署 DeepSeek - R1 模型指南
  • 本地部署DeepSeek-R1模型(新手保姆教程)
  • Win11下搭建Kafka环境
  • python基础入门:4.4模块与包管理
  • 【Java】多线程和高并发编程(四):阻塞队列(上)基础概念、ArrayBlockingQueue
  • Vue.js 状态管理库Pinia
  • C++类和对象进阶:构造函数和析构函数详解
  • linux部署node服务
  • 使用ThreeJS实现的宇宙大爆炸3D粒子特效思路,原理和关键代码解析
  • 达梦数据库(DM)线程管理
  • 【Java】多线程和高并发编程(三):锁(中)深入ReentrantLock
  • C++ STL汇总
  • C++智能指针的使用
  • 移动(新)魔百盒刷机教程[M301A_YS]
  • SpringSecurity:授权服务器与客户端应用(入门案例)
  • 9 数据流图
  • Linux: ASoC 声卡硬件参数的设置过程简析
  • KITE提示词框架:引导大语言模型的高效新工具
  • 【故障处理】 - 12C ADG备库密码文件的MD5值不断变化
  • 51c自动驾驶~合集49
  • 2.10..
  • c# http
  • 【CXX-Qt】2 CXX-Qt #[cxx_qt::bridge] 宏指南
  • react redux用法学习