【Megatron-DeepSpeed】张量并行工具代码mpu详解(四):张量并行版Embedding层及交叉熵的实现及测试

news2024/11/26 12:26:22

相关博客
【Megatron-DeepSpeed】张量并行工具代码mpu详解(四):张量并行版Embedding层及交叉熵的实现及测试
【Megatron-DeepSpeed】张量并行工具代码mpu详解(三):张量并行层的实现及测试
【Megatron-DeepSpeed】张量并行工具代码mpu详解(一):并行环境初始化
【Megatron-DeepSpeed】张量并行工具代码mpu详解(二):Collective通信操作的封装mappings
【深度学习】【分布式训练】DeepSpeed:AllReduce与ZeRO-DP
【深度学习】混合精度训练与显存分析
【深度学习】【分布式训练】Collective通信操作及Pytorch示例
【自然语言处理】【大模型】大语言模型BLOOM推理工具测试
【自然语言处理】【大模型】GLM-130B:一个开源双语预训练语言模型
【自然语言处理】【大模型】用于大型Transformer的8-bit矩阵乘法介绍

张量并行版Embedding层及交叉熵的实现及测试

​ Megatron-DeepSpeed是DeepSpeed版本的NVIDIA Megatron-LM。像BLOOM、GLM-130B等主流大模型都是基于Megatron-DeepSpeed开发的。这里以BLOOM版本的Megetron-DeepSpeed为例,介绍其模型并行代码mpu的细节(位于megatron/mpu下)。

​ 理解该部分的代码需要对模型并行的原理以及集合通信有一定的理解,可以看文章:

  • 【深度学习】【分布式训练】Collective通信操作及Pytorch示例
  • 【深度学习】【分布式训练】一文捋顺千亿模型训练技术:流水线并行、张量并行和3D并行
  • 【深度学习】【分布式训练】DeepSpeed:AllReduce与ZeRO-DP

强烈建议阅读,不然会影响本文的理解:

  • 【Megatron-DeepSpeed】张量并行工具代码mpu详解(一):并行环境初始化
  • 【Megatron-DeepSpeed】张量并行工具代码mpu详解(二):Collective通信操作的封装mappings
  • 【Megatron-DeepSpeed】张量并行工具代码mpu详解(三):张量并行层的实现及测试

阅读建议:

  1. 本文仅会解析核心代码,并会不介绍所有代码;
  2. 本文会提供一些测试脚本来展现各部分代码的功能;
  3. 建议实际动手实操来加深理解;
  4. 建议对Collective通信以及分布式模型训练有一定理解,再阅读本文;

一、总览

​ mpu目录下核心文件有:

  • initialize.py:负责数据并行组、张量并行组和流水线并行组的初始化,以及获取与各类并行组相关的信息;
  • data.py:实现张量并行中的数据广播功能;
  • cross_entropy.py:张量并行版本的交叉熵;
  • layers.py:并行版本的Embedding层,以及列并行线性层和行并行线性层;
  • mappings.py:用于张量并行的通信操作;

二、张量并行版Embedding层

在这里插入图片描述

​ Embedding层本质就是一个查找表。如上图所示,张量并行版embedding层就是将完整的embedding层,在vocab的维度切分。张量并行组中的每个进程仅持有部分embedding层。

1. 实现代码

​ 这里直接在原始的文件(megatron/mpu/layers.py)中,添加一个自定义的并行版Embedding层。其与原始版完全相同,仅添加了一些输出来展示整个过程。

# layers.py
class MyVocabParallelEmbedding(torch.nn.Module):
    def __init__(self, num_embeddings, embedding_dim,
                 init_method=init.xavier_normal_):
        super(MyVocabParallelEmbedding, self).__init__()
        # 初始化一些参数
        self.num_embeddings = num_embeddings # 词表大小
        self.embedding_dim = embedding_dim
        self.padding_idx = None
        self.max_norm = None
        self.norm_type = 2.
        self.scale_grad_by_freq = False
        self.sparse = False
        self._weight = None
        self.tensor_model_parallel_size = get_tensor_model_parallel_world_size()
        # 张量并行组中的每个rank仅持有部分vocab embedding
        # 这里会计算当前rank持有的vocab的起始和结束位置
        self.vocab_start_index, self.vocab_end_index = \
            VocabUtility.vocab_range_from_global_vocab_size(
                self.num_embeddings, get_tensor_model_parallel_rank(),
                self.tensor_model_parallel_size)
        # 当前rank持有的部分vocab的大小
        self.num_embeddings_per_partition = self.vocab_end_index - \
            self.vocab_start_index

        args = get_args()

        # embedding层添加LayerNorm
        if mpu.is_pipeline_first_stage() and (args.use_bnb_optimizer or args.embed_layernorm):
            self.norm = LayerNorm(embedding_dim)

        # bnb是指bitsandbytes,该库针对8-bit做了一些cuda函数的封装,这里忽略
        if args.use_bnb_optimizer:
            # for BNB we ignore the passed init_method and use torch.nn.init.xavier_uniform_
            # modified to calculate std on the unpartitioned embedding
            init_method = partial(xavier_uniform_tensor_parallel_, tp_degree=self.tensor_model_parallel_size)
        
        # 初始化embedding层的权重
        # 每个rank仅初始化自己所持有的那部分
        if args.use_cpu_initialization:
            self.weight = Parameter(torch.empty(
                self.num_embeddings_per_partition, self.embedding_dim,
                dtype=args.params_dtype))
            _initialize_affine_weight_cpu(
                self.weight, self.num_embeddings, self.embedding_dim,
                self.num_embeddings_per_partition, 0, init_method)
        else:
            self.weight = Parameter(torch.empty(
                self.num_embeddings_per_partition, self.embedding_dim,
                device=torch.cuda.current_device(), dtype=args.params_dtype))
            _initialize_affine_weight_gpu(self.weight, init_method,
                                          partition_dim=0, stride=1)
        # bnb(忽略)
        if args.use_bnb_optimizer:
            from bitsandbytes.optim import GlobalOptimManager
            GlobalOptimManager.get_instance().override_config(self.weight, 'optim_bits', 32)
            GlobalOptimManager.get_instance().register_parameters(self.weight)
            
    def forward(self, input_):
        if torch.any(input_ >= self.num_embeddings):
            raise ValueError(f"There is an input id in the input that is greater than the highest possible input id.\nInput: {input_}\nnum_embeddings: {self.num_embeddings}")
        # 全局rank
        global_rank = torch.distributed.get_rank()
        # 张量并行组中的rank
        tp_rank = get_tensor_model_parallel_rank()
        info = f"*"*20 + \
                f"\n> global_rank={global_rank}\n" + \
                f"> tensor parallel rank={tp_rank}\n" + \
                f"> full embedding size={(self.num_embeddings, self.embedding_dim)}\n" + \
                f"> partial embedding size={list(self.weight.size())}\n" \
                f"> input = {input_}\n" \
                f"> vocab_start_index={self.vocab_start_index}, vocab_end_index={self.vocab_end_index}\n"
        if self.tensor_model_parallel_size > 1:
            # Build the mask.
            input_mask = (input_ < self.vocab_start_index) | \
                         (input_ >= self.vocab_end_index)
            # Mask the input.
            masked_input = input_.clone() - self.vocab_start_index
            masked_input[input_mask] = 0
        else:
            # input_ is garanted to be in the range [0:self.vocab_end_index - self.vocab_start_index] thanks to the first check
            masked_input = input_
        info += f"> input_mask={input_mask} \n"
        info += f"> masked_input={masked_input} \n"

        # 获得embedding
        output_parallel = F.embedding(masked_input, self.weight,
                                      self.padding_idx, self.max_norm,
                                      self.norm_type, self.scale_grad_by_freq,
                                      self.sparse)
        # 由于在当前rank上,仅能获得部分输入的embedding
        # 因此,将mask掉的input对应的embedding设置为全0
        if self.tensor_model_parallel_size > 1:
            output_parallel[input_mask, :] = 0.0
        info += f"> output_parallel={output_parallel}\n"
        # 上一步设置为全0的embedding会在这一步通过allreduce,组装成完整的embedding
        output = reduce_from_tensor_model_parallel_region(output_parallel)
        info += f"> output={output}\n"

        if hasattr(self, 'norm'):
            output = self.norm(output)
        print(info, end="")
        return output

2. 测试脚本

​ 实验设置为:张量并行度为2,流水线并行度也为2。测试脚本比较简单,直接调用上面实现的MyVocabParallelEmbedding

# test_embedding.py
import sys
sys.path.append("..")

from megatron.mpu import layers
from commons import set_random_seed
from commons import print_separator
from megatron.initialize import _initialize_distributed
from megatron.global_vars import set_global_variables
import megatron.mpu as mpu
from torch.nn.parameter import Parameter
import torch.nn.init as init
import torch
import random

def test_parallel_embedding():
    batch_size = 2
    seq_length = 4
    vocab_size = 6
    hidden_size = 8
    seed = 123

    set_random_seed(seed)
    # (2,4)
    input_data = torch.LongTensor(
        size=(batch_size, seq_length)).random_(0, vocab_size).cuda()

    embedding_vocab_parallel = layers.MyVocabParallelEmbedding(
        vocab_size, hidden_size, init_method=init.normal_).cuda()
    output = embedding_vocab_parallel(input_data)

def main():
    set_global_variables(ignore_unknown_args=True)
    _initialize_distributed()
    world_size = torch.distributed.get_world_size()

    print_separator('Test test_parallel_embedding')
    test_parallel_embedding()


if __name__ == '__main__':
    main()

启动命令:

options=" \
        --tensor-model-parallel-size 2 \
        --pipeline-model-parallel-size 2 \
        --num-layers 10 \
        --hidden-size 768 \
        --micro-batch-size 2 \
        --num-attention-heads 32 \
        --seq-length 512 \
        --max-position-embeddings 512\
        --use_cpu_initialization True
        "

cmd="deepspeed test_embedding.py $@ ${options}"

eval ${cmd}

3. 测试结果

在这里插入图片描述

  • 全局rank为2,在张量并行组中的rank为0;
  • 完整的embedding层大小应为(6, 8),当前设备持有的embedding层大小为(3, 8),符合张量并行度为2的假设;
  • 当前设备持有的词表id范围介于0到3,输入中超出该词表范围都会被mask;
  • 当前设备的输出(output_parallel),会有部分embedding为全0,而完整的输出(output)则将张量并行组中所有的embedding输出都聚合在一起;

三、张量并行版交叉熵

​ 我们以自然语言模型为例,展示交叉熵的计算原理。

​ 若模型针对单个token预测的logit表示为 l ⃗ = [ l 1 , … , l k ] \vec{l}=[l_1,\dots,l_k] l =[l1,,lk],经过softmax转换后的概率分布为 p ⃗ = [ p 1 , … , p k ] \vec{p}=[p_1,\dots,p_k] p =[p1,,pk],其中:
p i = e l i ∑ j k e l j p_i=\frac{e^{l_i}}{\sum_{j}^k e^{l_j}} pi=jkeljeli
该token的真实标签表示为 y ⃗ = [ y 1 , … , y k ] \vec{y}=[y_1,\dots,y_k] y =[y1,,yk],由于其是one-hot编码,所以 y ⃗ \vec{y} y 中仅有一个值为1,其余均为0。那么该token上的交叉熵损失函数为
loss = − ∑ i = 1 k y i log ⁡ ( p i ) = − ∑ i = 1 k y i log ⁡ ( e l i ∑ j k e l j ) = ∑ i = 1 k y i [ log ⁡ ( ∑ j k e l j ) − log ⁡ ( e l i ) ] = log ⁡ ( ∑ j k e l j ) − ∑ i = 1 k y i log ⁡ ( e l i ) = log ⁡ ( ∑ j k e l j ) − ∑ i = 1 k y i l i \begin{align} \text{loss}&=-\sum_{i=1}^k y_i\log(p_i) \\ &=-\sum_{i=1}^k y_i\log(\frac{e^{l_i}}{\sum_{j}^k e^{l_j}}) \\ &=\sum_{i=1}^k y_i[\log(\sum_{j}^k e^{l_j})-\log(e^{l_i})] \\ &=\log(\sum_{j}^k e^{l_j})-\sum_{i=1}^k y_i \log(e^{l_i}) \\ &=\log(\sum_{j}^k e^{l_j})-\sum_{i=1}^k y_i {l_i} \end{align} loss=i=1kyilog(pi)=i=1kyilog(jkeljeli)=i=1kyi[log(jkelj)log(eli)]=log(jkelj)i=1kyilog(eli)=log(jkelj)i=1kyili
由于模型输出的 l ⃗ \vec{l} l 是已知的,那么上式第一项 log ⁡ ( ∑ j k e l j ) \log(\sum_{j}^k e^{l_j}) log(jkelj)是一个固定的常数;由于所有的 y i y_i yi中仅有一个是1,那么第二项 ∑ i = 1 k y i l i \sum_{i=1}^k y_i {l_i} i=1kyili本质上就是正确token对应的logit值。

mpu代码中的交叉熵实现基本上遵循上面的分析,仅是添加了batch size和seq_length维度,但核心思想不变

1. 实现代码

​ 同样,也是在原始文件(megatron/mpu/cross_entropy.py)中,添加一个自定义的并行版交叉熵。该实现与原版完全相同,仅添加了一些输出来展示整个过程。

# cross_entropy.py
class _MyVocabParallelCrossEntropy(torch.autograd.Function):

    @staticmethod
    def forward(ctx, vocab_parallel_logits, target):
        # vocab_parallel_logits: (batch_size, seq_length, vocab_size)
        # target: (batch_size, seq_length)
        global_rank = torch.distributed.get_rank()
        tp_rank = get_tensor_model_parallel_rank()
        # 在vocab维度取最大值,也就是每个token对于logits的最大值
        logits_max = torch.max(vocab_parallel_logits, dim=-1)[0]
        torch.distributed.all_reduce(logits_max,
                                     op=torch.distributed.ReduceOp.MAX,
                                     group=get_tensor_model_parallel_group())
        vocab_parallel_logits.sub_(logits_max.unsqueeze(dim=-1))
        
        info = f"*"*20 + \
                f"\n> global_rank={global_rank}\n" + \
                f"> tp_rank={tp_rank}\n" + \
                f"> size of vocab_parallel_logits={list(vocab_parallel_logits.size())}\n" + \
                f"> size of target={list(target.size())}\n"

        # 依据当前进程持有的部分词表大小partition_vocab_size,以及张量并行组中rank和world size,
        # 确定出当前进程持有词表的起始索引vocab_start_index和结束索引vocab_end_index
        get_vocab_range = VocabUtility.vocab_range_from_per_partition_vocab_size
        partition_vocab_size = vocab_parallel_logits.size()[-1]
        rank = get_tensor_model_parallel_rank()
        world_size = get_tensor_model_parallel_world_size()
        vocab_start_index, vocab_end_index = get_vocab_range(
            partition_vocab_size, rank, world_size)
        
        # 将不在词表中的target遮蔽掉
        target_mask = (target < vocab_start_index) | (target >= vocab_end_index)
        masked_target = target.clone() - vocab_start_index
        masked_target[target_mask] = 0

        # ligits_2d: (batch_size*seq_length, vocab_size)
        logits_2d = vocab_parallel_logits.view(-1, partition_vocab_size)
        # masked_target_1d: (batch_size*seq_length)
        masked_target_1d = masked_target.view(-1)
        arange_1d = torch.arange(start=0, end=logits_2d.size()[0],
                                 device=logits_2d.device)
        # predicted_logits_1d 表示正确token对应的logit
        predicted_logits_1d = logits_2d[arange_1d, masked_target_1d]
        predicted_logits_1d = predicted_logits_1d.clone().contiguous()
        
        predicted_logits = predicted_logits_1d.view_as(target)
        # 将当前进程无法获得的logits设置为0,用于后续allreduce组成完成logits
        predicted_logits[target_mask] = 0.0

        info += f"> size of logits_2d={list(logits_2d.size())}\n" + \
                f"> size of masked_target_1d={list(masked_target_1d.size())}\n" + \
                f"> size of predicted_logits={list(predicted_logits_1d.size())}\n"

        # 各个进程持有的predicted_logits的大小是完全相同的
        # 但是,当前进程持有的predicted_logits仅在当前词表上才有取值,其余值为0
        # 通过allreduce即可得到完整的predicted_logits
        torch.distributed.all_reduce(predicted_logits,
                                     op=torch.distributed.ReduceOp.SUM,
                                     group=get_tensor_model_parallel_group())

        # 求softmax分母的部分
        exp_logits = vocab_parallel_logits
        
        torch.exp(vocab_parallel_logits, out=exp_logits)
        sum_exp_logits = exp_logits.sum(dim=-1)
        torch.distributed.all_reduce(sum_exp_logits,
                                     op=torch.distributed.ReduceOp.SUM,
                                     group=get_tensor_model_parallel_group())

        # 对应上面公式推导的最终结果
        # loss: (batch_size, seq_length)。
        # loss是一个矩阵,矩阵的值对应单个token的交叉熵
        loss = torch.log(sum_exp_logits) - predicted_logits
        info += f"> size of sum_exp_logits={list(sum_exp_logits.size())}\n" + \
                f"> size of loss={list(loss.size())}\n"

        print(info, end="")

        exp_logits.div_(sum_exp_logits.unsqueeze(dim=-1))
        ctx.save_for_backward(exp_logits, target_mask, masked_target_1d)

        return loss
    
    @staticmethod
    def backward(ctx, grad_output):

        # Retreive tensors from the forward path.
        softmax, target_mask, masked_target_1d = ctx.saved_tensors

        # All the inputs have softmax as thier gradient.
        grad_input = softmax
        # For simplicity, work with the 2D gradient.
        partition_vocab_size = softmax.size()[-1]
        grad_2d = grad_input.view(-1, partition_vocab_size)

        # Add the gradient from matching classes.
        arange_1d = torch.arange(start=0, end=grad_2d.size()[0],
                                 device=grad_2d.device)
        grad_2d[arange_1d, masked_target_1d] -= (
            1.0 - target_mask.view(-1).float())

        # Finally elementwise multiplication with the output gradients.
        grad_input.mul_(grad_output.unsqueeze(dim=-1))

        return grad_input, None

2. 测试脚本

# test_cross_entropy.py
import sys
sys.path.append("..")

from commons import set_random_seed
from commons import IdentityLayer
from commons import print_separator
from commons import initialize_distributed
from megatron.mpu.cross_entropy import _MyVocabParallelCrossEntropy
import megatron.mpu as mpu
import torch.nn.functional as F
import torch
import random

def test_cross_entropy():
    tensor_model_parallel_size = mpu.get_tensor_model_parallel_world_size()

    batch_size = 32
    seq_length = 128
    vocab_size_per_partition = 500
    logits_scale = 1000.0
    vocab_size = vocab_size_per_partition * tensor_model_parallel_size
    seed = 1234

    set_random_seed(seed)
    identity = IdentityLayer((batch_size, seq_length, vocab_size),
                             scale=logits_scale).cuda()
    logits = identity()
    logits_parallel = mpu.scatter_to_tensor_model_parallel_region(logits)
    target = torch.cuda.LongTensor(
        size=(batch_size, seq_length)).random_(0, vocab_size)
    loss = _MyVocabParallelCrossEntropy.apply(logits_parallel, target).mean()
    
if __name__ == '__main__':
    initialize_distributed()
    world_size = torch.distributed.get_world_size()
    tensor_model_parallel_size = 2
    pipeline_model_parallel_size = 2

    mpu.initialize_model_parallel(
            tensor_model_parallel_size,
            pipeline_model_parallel_size)

    test_cross_entropy()

启动命名:

deepspeed test_cross_entropy.py

3. 测试结果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/872656.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

时序预测 | MATLAB实现基于CNN卷积神经网络的时间序列预测-递归预测未来(多指标评价)

时序预测 | MATLAB实现基于CNN卷积神经网络的时间序列预测-递归预测未来(多指标评价) 目录 时序预测 | MATLAB实现基于CNN卷积神经网络的时间序列预测-递归预测未来(多指标评价)预测结果基本介绍程序设计参考资料 预测结果 基本介绍 1.Matlab实现CNN卷积神经网络时间序列预测未…

webpack中常见的Loader

目录 1.webpack中的loader是什么&#xff1f;配置方式 2. loader特性3.常见的loader 1.webpack中的loader是什么&#xff1f; loader 用于对模块的"源代码"进行转换&#xff0c;在 import 或"加载"模块时预处理文件 webpack做的事情&#xff0c;仅仅是分…

Linux printf函数输出问题

1.printf函数并不会直接将数据输出到屏幕&#xff0c;而是先放到缓冲区中。 原因是&#xff1a; 解决效率和性能的问题。 比如说&#xff0c;printf在打印数据到屏幕上的时候不经过缓冲区&#xff0c;而是直接调用内核&#xff0c;此时内核就相当于另外一个进程&#xff0c;这…

Linux之【进程间通信(IPC)】-总结篇

Linux之【进程间通信&#xff08;IPC&#xff09;】-总结篇 管道System V共享内存System V消息队列System V信号量IPC资源的管理方式 往期文章 1.进程间通信之管道 2.进程间通信之System V共享内存 管道 进程之间具有独立性&#xff0c;拥有自己的虚拟地址空间&#xff0c;因…

基于TorchViz详解计算图(附代码)

文章目录 0. 前言1. 计算图是什么&#xff1f;2. TorchViz的安装3. 计算图详解 0. 前言 按照国际惯例&#xff0c;首先声明&#xff1a;本文只是我自己学习的理解&#xff0c;虽然参考了他人的宝贵见解&#xff0c;但是内容可能存在不准确的地方。如果发现文中错误&#xff0c;…

【学会动态规划】买卖股票的最佳时机 IV(18)

目录 动态规划怎么学&#xff1f; 1. 题目解析 2. 算法原理 1. 状态表示 2. 状态转移方程 3. 初始化 4. 填表顺序 5. 返回值 3. 代码编写 写在最后&#xff1a; 动态规划怎么学&#xff1f; 学习一个算法没有捷径&#xff0c;更何况是学习动态规划&#xff0c; 跟我…

【马蹄集】第二十二周——进位制与字符串专题

进位制与字符串专题 目录 MT2179 01操作MT2182 新十六进制MT2172 萨卡兹人MT2173 回文串等级MT2175 五彩斑斓的串 MT2179 01操作 难度&#xff1a;黄金    时间限制&#xff1a;1秒    占用内存&#xff1a;128M 题目描述 刚学二进制的小码哥对加减乘除还不熟&#xff0c;他…

DataGrip 安装 与 连接MySQL数据库

DataGrip 安装 与 连接MySQL数据库 Jetbrains是著名的编程工具商业软件提供商&#xff0c;旗下有很多软件。包括IDE、团队开发工具、插件和微软.Net辅助工具、包括自创语言Kotlin等。我们通常用的和说的全家桶&#xff0c;主要就是指它的IDE套件。Jetbrains的IDE工具都支持跨平…

web-Element

在vueapp里<div><!-- <h1>{{message}}</h1> --><element-view></element-view></div> <div><!-- <h1>{{message}}</h1> --><element-view></element-view></div>在view新建个文件 <t…

AIGC+游戏:一个被忽视的长赛道

&#xff08;图片来源&#xff1a;Pixels&#xff09; AIGC彻底变革了游戏&#xff0c;但还不够。 数科星球原创 作者丨苑晶 编辑丨大兔 消费还没彻底复苏&#xff0c;游戏却已经出现拐点。 在游戏热度猛增的背后&#xff0c;除了版号的利好因素外&#xff0c;AIGC技术的广泛…

项目实战 — 消息队列(8){网络通信设计②}

目录 一、客户端设计 &#x1f345; 1、设计三个核心类 &#x1f345; 2、完善Connection类 &#x1f384; 读取请求和响应、创建channel &#x1f384; 添加扫描线程 &#x1f384; 处理不同的响应 &#x1f384; 关闭连接 &#x1f345; 3、完善Channel类 &#x1f384; 编…

机器学习编译系列

机器学习编译MLC 1. 引言2. 机器学习编译--概述2.1 什么是机器学习编译 1. 引言 陈天奇目前任教于CMU&#xff0c;研究方向为机器学习系统。他是TVM、MXNET、XGBoost的主要作者。2022年夏天&#xff0c;陈天奇在B站开设了《机器学习编译》的课程。   《机器学习编译》课程共分…

2023最新水果编曲软件FL Studio 21.1.0.3267音频工作站电脑参考配置单及系统配置要求

音乐在人们心中的地位日益增高&#xff0c;近几年音乐选秀的节目更是层出不穷&#xff0c;喜爱音乐&#xff0c;创作音乐的朋友们也是越来越多&#xff0c;音乐的类型有很多&#xff0c;好比古典&#xff0c;流行&#xff0c;摇滚等等。对新手友好程度基本上在首位&#xff0c;…

全网最牛,Appium自动化测试框架-关键字驱动+数据驱动实战(一)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、关键字驱动框架…

Stm32-使用TB6612驱动电机及编码器测速

这里写目录标题 起因一、电机及编码器的参数二、硬件三、接线四、驱动电机1、TB6612电机驱动2、定时器的PWM模式驱动电机 五、编码器测速1、定时器的编码器接口模式2、定时器编码器模式测速的原理3、编码器模式的配置4、编码器模式相关代码5、测速方法 六、相关问题以及解答1、…

关于Cesium的常见需求整理之点位和弹窗(点位弹窗)

一、点位上图 ①在Cesium中&#xff0c;每个自定义的地图元素被视为一个entity对象&#xff0c;如果我们要添加点位到地图上&#xff0c;那就必须先创建一个entity对象。 var entity new Cesium.Entity({position: position, });以上代码我们创建了一个entity对象&#xff0…

Autosar通信入门系列06-聊聊CAN通信的线与机制与ACK应答

本文框架 1. 概述2. CAN通信的线与机制3. ACK应答机制理解 1. 概述 本文为Autosar通信入门系列介绍&#xff0c;如您对AutosarMCAL配置&#xff0c;通信&#xff0c;诊断等实战有更高需求&#xff0c;可以参见AutoSar 实战进阶系列专栏&#xff0c;快速链接&#xff1a;AutoSa…

数据库基础(增删改查)

目录 MySQL 背景知识 数据库基础操作 1.创建数据库 2.查看所有数据库 3.选中指定的数据库 4.删除数据库 数据库表操作 MySQL的数据类型 1.创建表 3.查看指定表的结构 4.删除表 增删改 新增操作 修改(Updata) 删除语句 面试题 查询操作 指定列查询 查询的列为表达式…

系统设计:通用思路之4S分析法

1.系统设计 系统设计是一个定义系统架构、功能模块、服务及接口和数据存储等满足特定需求的过程。 与面向对象设计不同的是&#xff0c;面向对象设计通常是对于某个特定功能模块的设计&#xff0c;通常要求设计类图关系、接口关系、实现关系等涉及具体代码层面的设计&#xff…