PyTorch分布式训练中各节点如何通信
深度学习
文章目录
- 深度学习
- 前言
- pytorch如何初始化分布式训练
- 怎么知道要使用哪几台机器进行训练的
- 如何根据标识进行初始化(init_method)
- 如何获取进程的唯一标识rank
- 如何实现
- 数据如何分发
前言
同学们在处理分布式训练时经常会遇到以下几个疑问:
1、分布式节点如何知道彼此并通信。
每个节点启动训练任务需要设置主节点的ip:port,通过跟主节点通信来启动服务。
2、是不是每个节点都要执行python程序。
是的,写好训练代码后,每个节点都需要执行python命令,但是一般平台会帮我们管理分布式服务,自动把脚本分发到各个节点,并执行python成勋,才会产生不需要每个节点执行python代码的错觉。
3、如果忘记在某个节点执行python命令,结果会怎样。
ddp会阻塞在初始化的位置,一直等到有world_size进程为止,这个就是靠主节点来跟踪进程数。
4、每个节点是不是都需要有完备的数据。
是的,如果使用ddp的DistributedSampler来获取数据,那么一定要保证每个节点都有完整的数据,要不然根据indics无法获取正确的数据。
5、每个节点获取的mini-batch数据如何保证不重合。
不同节点设置相同的随机数种子,这样采样的batch就是一摸一样的,然后再根据各自的rank来从batch中取出自己的mini-batch。1. pytorch如何初始化分布式训练
pytorch如何初始化分布式训练
核心函数如下,下面具体分析一下
torch.distributed.init_process_group(backend=dist_backend,init_method=init_method,world_size=world_size,rank=rank)
backend就是通信协议,使用分布式时,在梯度汇总求平均的过程中,各主机之间需要进行通信。因此,需要指定通信的协议架构等。gpu是nccl,cpu是gloo。
init_method 指定当前进程组初始化方式,也就是获取其他节点的信息,进行同步
world_size是进程的个数,比如我们有3台机器,每台机器有2个gpu,那么就有3x2=6个进程
rank则表示进程的标识
怎么知道要使用哪几台机器进行训练的
假如在同一个局域网内有6台机器,其中三台机器训练bert,另外三台训练gpt,每台机器是如何知道其他节点跟自己是否训练的是同一个任务呢,更准确的是说不同进程之间是如何保证自己是同一个进程组的?同一个机器可以使用同一进程组进行标识,但是训练任务通常会分散在不同的机器上。一种非常简单的方法就是给同一个训练任务的机器分配唯一的id,id相同的时候大家进行通信,id不同时则不通信。pytorch使用的唯一标识是ip+port,也就是说同一个训练任务大家使用同一个通信地址ip:port 不同任务即使使用了同一个机器,即ip相同,也可以通过port来进行区分,可以看到192.168.1.2这台机器上跑了两个任务(这台机器有两个gpu,或者资源不够,用户就是想要指定两个进程来训练不同的任务),我们依然可以通过port来区分这两个任务,
训练任务 | ip地址 | ip+port 标识 | 启动 |
---|---|---|---|
bert : master | 192.168.1.2 | 192.168.1.2:5003 | python train.py --master_addr 192.168.1.2 --master_port 5003 |
bert : slave | 192.168.1.3 | 192.168.1.2:5003 | python train.py --master_addr 192.168.1.2 --master_port 5003 |
bert : slave | 192.168.1.4 | 192.168.1.2:5003 | python train.py --master_addr 192.168.1.2 --master_port 5003 |
gpt : master | 192.168.1.2 | 192.168.1.2:5004 | python train.py --master_addr 192.168.1.2 --master_port 5004 |
gpt : slave | 192.168.1.6 | 192.168.1.2:5004 | python train.py --master_addr 192.168.1.2 --master_port 5004 |
gpt : slave | 192.168.1.7 | 192.168.1.2:5004 | python train.py --master_addr 192.168.1.2 --master_port 5004 |
总而言之,不同任务之间是通过ip和port来作为唯一标识区分的。我们启动任务的时候指定这个ip+port,这个ip:port将会作为服务的主节点。
如何根据标识进行初始化(init_method)
torch获取这个唯一标识的方式也有两种(其实是三种,文件系统共享我没用过)
tcp:直接指定tcp的ip和端口,init_method=‘tcp://192.168.1.2:5003’
env : 我们获取到输入参数master_addr和master_port之后,设置环境变量
os.environ['MASTER_ADDR'] = '192.168.1.2'
os.environ['MASTER_PORT'] = '5003'
然后就可以通过指定init_method="env://"来初始化服务了。很多博客都说要在环境变量中写入MASTER_ADDR和MASTER_PORT,我当时的理解是pytorch会直接把这两个变量写入到系统环境中
export MASTER_ADDR="192.168.1.2"
export MASTER_PORT="5003"
然后就变得非常非常的困惑,有那么多的任务,如果大家都把自己的配置写到系统中不就存在了冲突了吗,最重要的是我在系统中根本就没看到这两个变量。后来才意识到,python会拷贝一份环境变量,os.environ[‘MASTER_ADDR’] = '192.168.1.2’其实添加的是拷贝环境变量的值,而不是真的在环境变量中指定了这个值。
所谓的会自动从环境变量中获取MASTER_ADDR和MASTER_PORT,其实都是这个拷贝的环境变量,修改的也是这个拷贝值。python在使用多进程的时候,会先从本地拷贝一份环境变量,然后这份环境变量再分发给各个进程
可以试试,先启动一个python代码设置环境变量
import os
os.environ['MASTER_ADDR'] = '192.168.1.2'
print(os.environ['MASTER_ADDR']) # '192.168.1.2'
再启动一个python代码读取环境变量
import os
print(os.environ['MASTER_ADDR']) # 空
到系统环境中查看
复制
echo $master #空
如何获取进程的唯一标识rank
world_size很好计算,这个是自己指定的,例如我们使用3台机器,每个节点有4个gpu,全部使用的话world_size=3*4=12,很直接world_size=nnodes * nproc_per_node。其中nnodes就是我们指定的节点个数,nproc_per_node就是单个节点执行的进程数,通常是每个机器gpu的数量。如果是cpu训练的话,就是cpu的个数,通常每台机器只有一个cpu。
上面讲了通过ip+port我们可以确定每个任务的唯一标识,通常一个任务我们会进行多几多卡训练,即启动多个进程。每个进程都有自己的唯一标识,这个就是rank。有趣的是,pytorch的进程id并不是根据全部机器或者world_size来分配的每个进程的rank的,假如我们有3个节点,每个节点4张卡,理想情况是我们执行pytorch的dpp初始化后,每个gpu都有一个rank值,依次递增到world_size-1
ip | gpu1 | gpu2 | gpu3 | gpu4 |
---|---|---|---|---|
192.168.1.2 | 0 | 1 | 2 | 3 |
192.168.1.3 | 4 | 5 | 6 | 7 |
192.168.1.4 | 8 | 9 | 10 | 11 |
但实际上,pytorch只会根据每个节点自身确定一个local_rank值,每次都是从0开始增加的
ip | gpu1 | gpu2 | gpu3 | gpu4 |
---|---|---|---|---|
192.168.1.2 | 0 | 1 | 2 | 3 |
192.168.1.3 | 0 | 1 | 2 | 3 |
192.168.1.4 | 0 | 1 | 2 | 3 |
所以为了获取全局的rank需要我们手动做一次转换rank=node_rank*n_gpu+local_rank
ip | gpu1 | gpu2 | gpu3 | gpu4 |
---|---|---|---|---|
192.168.1.2 | 0 | 1 | 2 | 3 |
192.168.1.3 | 1*4+0 | 1*4+1 | 1*4+2 | 1*4+3 |
192.168.1.4 | 2*4+0 | 2*4+1 | 2*4+2 | 2*4+3 |
node_rank是我们给每个节点的编号。其实在这里有一个问题,可不可以一个节点使用2个gpu,一个节点使用3个gpu呢?这个时候该怎么获取每个进程的id呢?
还有一个问题需要关注,如果使用的init_method=“env://”,那么也需要将rank和world_size也写入到环境变量中。
os.environ['RANK'] = rank
os.environ['WORLD_SIZE'] = 12
如何实现
import os
import argparse
import torch
import random
import numpy as np
import torch.distributed as distdef setup_new_process(local_rank, callee, args):rank = args.node_rank * args.nproc_per_node + local_rankworld_size = args.nnodes * args.nproc_per_noderandom.seed(rank)np.random.seed(rank)torch.manual_seed(rank)torch.cuda.manual_seed_all(rank)dist_backend = 'gloo'init_method = "env://"if args.use_env:os.environ['MASTER_ADDR'] = args.master_addros.environ['MASTER_PORT'] = args.master_portos.environ['RANK'] = str(rank)os.environ['WORLD_SIZE'] = str(world_size)else:init_method = f"tcp://{args.master_addr}:{args.master_port}"if torch.cuda.is_available():torch.cuda.set_device(local_rank)torch.cuda.empty_cache()# 通信后端,nvidia GPU推荐使用NCCLif torch.distributed.is_nccl_available():dist_backend = 'nccl'print(f'start init process: rank = {rank}')dist.init_process_group(backend=dist_backend,init_method=init_method,world_size=world_size,rank=rank)callee(args)import torch.nn as nn
import torch.nn.functional as F
import torchdef train(args):if dist.is_initialized():rank = dist.get_rank()print(f"rank = {rank} | strat train.......")def main(callee):parse = argparse.ArgumentParser()parse.add_argument('--use_env', action="store_true")parse.add_argument('--master_addr', type=str, default="127.0.0.1")parse.add_argument('--master_port', type=str, default="5003")parse.add_argument('--nproc_per_node', type=int, default=0)parse.add_argument('--node_rank', type=int, default=0)parse.add_argument('--nnodes', type=int, default=1)args = parse.parse_args()if args.nproc_per_node == 0:if torch.cuda.device_count() > 0:args.nproc_per_node = torch.cuda.device_count()else:args.nproc_per_node = os.cpu_count()torch.multiprocessing.spawn(setup_new_process, nprocs=args.nproc_per_node,args=(callee, args), join=True)if __name__ == '__main__':main(train)
唯一没有解释的是torch.multiprocessing.spawn这个函数,这个函数用来启动分布式训练,本质就是创建多个进程。我的本地有4个cpu,所以我这里直接创建了4个进程来执行,注意nproc_per_node是单个节点进程数,也就是单机的gpu个数,启动多进程的时候,我们发现并没有传递local_rank这个参数,这是因为使用torch.multiprocessing.spawn会自动传入这个参数,并且是递增,,从0到nproc_per_node。
start init process: rank = 0
start init process: rank = 1
start init process: rank = 2
start init process: rank = 3
rank = 0 | strat train…
rank = 3 | strat train…
rank = 2 | strat train…
rank = 1 | strat train…
如果我们有多个节点,则需要在每个节点执行脚本
ip | 命令 |
---|---|
192.168.1.2 | python train.py --master_addr 192.168.1.2 --master_port 5003 --node_rank=0 --nnodes=3 |
192.168.1.3 | python train.py --master_addr 192.168.1.2 --master_port 5003 --node_rank=1 --nnodes=3 |
192.168.1.4 | python train.py --master_addr 192.168.1.2 --master_port 5003 --node_rank=2 --nnodes=3 |
前面只是启动了分布式训练而已,我们创建一个小模型来试试ddp,直接使用DistributedDataParallel对模型进行进行一层包装即可使用dpp
torch.nn.parallel.DistributedDataParallel(module, device_ids=None, output_device=None, dim=0, broadcast_buffers=True, process_group=None, bucket_cap_mb=25, find_unused_parameters=False, check_reduction=False)
我们写一个小的demo
def train(args):model = nn.Linear(5, 1, bias=False).to(args.rank)if torch.distributed.is_initialized():rank = torch.distributed.get_rank()model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[rank])print(f"rank = {rank} | start train.......")optimizer = torch.optim.Adam(model.parameters(),lr=0.01)for _ in range(10):optimizer.zero_grad()py = model(torch.rand(32,5).to(rank)loss = F.mse_loss(py,torch.rand(32,1).to(rank))print(loss)loss.backward()optimizer.step()
在单机CPU的模式下发现
raise ValueError(ValueError: DistributedDataParallel device_ids and output_device arguments only work with single-device/multiple-device GPU modules or CPU modules, but got device_ids [2], output_device None, and module parameters {device(type=‘cpu’)}.
抛出了一个异常,上面这个代码主要是执行在gpu上的,to(rank)的意思就是把数据或模型加载到编号为rank的gpu上,我本地没有gpu,所以不能使用to(rank),其次torch.nn.parallel.DistributedDataParallel也会把模型输出到某个device_id上(同样要求在gpu),当使用cpu训练的时候需要将device_ids和output_device都设置为None
device_ids
int 列表或 torch.device 对象,用于指定要并行的设备。对于数据并行,即完整模型放置于一个 GPU 上(single-device module)时,需要提供该参数,表示将模型副本拷贝到哪些 GPU 上。
对于模型并行的情况,即一个模型,分散于多个 GPU 上的情况(multi-device module),以及 CPU 模型,该参数比必须为 None,或者为空列表。
output_device
int 或者 torch.device,对于 single-device 的模型,表示结果输出的位置。
对于 multi-device module 和 GPU 模型,该参数必须为 None 或空列表。
def train(args):device = torch.device(f"cuda:{args.local_rank}" if torch.cuda.is_available() else "cpu")model = nn.Linear(5, 1, bias=False).to(device)for name, params in model.named_parameters():print(f'before dpp : rank = {args.rank}, name = {name}, params = {params.tolist()}')if torch.distributed.is_initialized():if torch.cuda.is_available():model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank])else:model = torch.nn.parallel.DistributedDataParallel(model)print(f"rank = {args.rank} | strat train.......")for name, params in model.named_parameters():print(f'after dpp : rank = {args.rank}, name = {name}, params = {params.tolist()}')optimizer = torch.optim.Adam(model.parameters(),lr=0.01)for _ in range(10):optimizer.zero_grad()py = model(torch.rand(32,5).to(device))loss = F.mse_loss(py,torch.rand(32,1).to(device))loss.backward()optimizer.step()for name, params in model.named_parameters():print(f'finish dpp rank = {args.rank}, name = {name}, params = {params.tolist()}')
这里把模型的参数也打印出来了。代码中我们是直接使用随机数初始化每个网络的,因此可以看到每个进程的模型参数是不同的,但是训练结束之后可以看到,模型参数都变成了同一个
before dpp : rank = 0, name = weight, params = [[-0.0033482015132904053, 0.23990488052368164, -0.36807698011398315, -0.3291219472885132, -0.1722462773323059]]
before dpp : rank = 3, name = weight, params = [[-0.44340017437934875, -0.3527894914150238, -0.19154831767082214, -0.423104465007782, -0.025388896465301514]]
before dpp : rank = 1, name = weight, params = [[0.2304326891899109, -0.1973903477191925, -0.08669748902320862, 0.20990818738937378, -0.4210233688354492]]
before dpp : rank = 2, name = weight, params = [[0.10258638858795166, -0.10642534494400024, 0.12263882160186768, -0.022842705249786377, 0.1910441517829895]]
after dpp : rank = 2, name = module.weight, params = [[-0.0033482015132904053, 0.23990488052368164, -0.36807698011398315, -0.3291219472885132, -0.1722462773323059]]
after dpp : rank = 0, name = module.weight, params = [[-0.0033482015132904053, 0.23990488052368164, -0.36807698011398315, -0.3291219472885132, -0.1722462773323059]]
after dpp : rank = 3, name = module.weight, params = [[-0.0033482015132904053, 0.23990488052368164, -0.36807698011398315, -0.3291219472885132, -0.1722462773323059]]
after dpp : rank = 1, name = module.weight, params = [[-0.0033482015132904053, 0.23990488052368164, -0.36807698011398315, -0.3291219472885132, -0.1722462773323059]]
finish dpp rank = 0, name = module.weight, params = [[0.09621907025575638, 0.33785226941108704, -0.26929566264152527, -0.23034155368804932, -0.07334098219871521]]
finish dpp rank = 3, name = module.weight, params = [[0.09621907025575638, 0.33785226941108704, -0.26929566264152527, -0.23034155368804932, -0.07334098219871521]]
finish dpp rank = 1, name = module.weight, params = [[0.09621907025575638, 0.33785226941108704, -0.26929566264152527, -0.23034155368804932, -0.07334098219871521]]
finish dpp rank = 2, name = module.weight, params = [[0.09621907025575638, 0.33785226941108704, -0.26929566264152527, -0.23034155368804932, -0.07334098219871521]]
这是因为torch.nn.parallel.DistributedDataParallel(model)在加载模型的时候,会把rank=0的模型参数传给各个子节点,作为初始化的参数。这样可以保证每个节点拿到的模型参数都是一样的。训练的过程中由于梯度共享的原因,所以每一次迭代梯度也是相同的。
数据如何分发
接着我们探讨另一个问题,如何把数据分发到各个节点上,一个非常简单的想法就是,读取一个batch的数据,然后将这个batch分成n份,n=world_size,也就是进程的数量,这样每个进程就都有完全不同的数据了。
pytorch实现的也非常简单,定义了一个DistributedSampler来将batch拆分成world_size份。首先每个节点都有需要从远端下载一份数据,然后加载到dataloader中。一开始以为是主节点计算好每个进程要的数据,然后再分发到各个节点计算,当时的一个疑惑是,如果分发数据的话,图像该怎么办呢?图像数据那么大分发起来可就太慢了。后来发现并没有采样数据,而是获取采样下标,然后每个节点根据分发过来的indics自己读取真实的数据
看了代码发现,主节点并没有分发数据,而是每个节点直接采样torch.randperm下标的。这就更加困惑,各个节点调用随机函数采样,那数据不就会存在重合的现象了吗。直到有一天福至心灵,突然意识到如果随机种子相同,那么采样的数据是不会发生变化的。
这样,给不同的节点设置相同的随机数种子,那么就会得到相同的结果,然后在根据rank获取自己的数据就可以了。下面代码写的非常详细了。
class DistributedSampler(Sampler[T_co]):def __iter__(self) -> Iterator[T_co]:# deterministically shuffle based on epoch and seedg = torch.Generator()# 设置随机数种子,因为是根据epoch来的,所以每个节点的随机数种子是相同的g.manual_seed(self.seed + self.epoch)# 采样数据下标,seed一样,每个节点采样出来的下标也都一样indices = torch.randperm(len(self.dataset), generator=g).tolist() # type: ignore[arg-type]# 依据自己的rank,获取部分数据返回indices = indices[self.rank:self.total_size:self.num_replicas]return iter(indices)def __len__(self) -> int:return self.num_samples# 每一轮我们调用一下set_epoch函数,这样随机数种子就会更新了# 每一轮我们采样的结果才会发生变化,要不然随机数种子不变,每个epoch采样的顺序都不会变化def set_epoch(self, epoch: int) -> None:self.epoch = epoch
保存和加载模型
保存模型就很简单了,唯一需要注意的点是,我们需要只需要在某个节点保存模型就可以了,不需要每个节点都保存,因为ddp通过all_reduce保证每个节点的模型参数是一致的。pytorch模型保存很有意思,我们知道神经网络有自己的结构,每一层都有自己的名字和参数,更细粒度的,每一个节点都有自己的名字和参数,所以save的是一个dict,key就是每一层的名字,value就是每一层参数tensor。然后把dict序列化成pt或者pth。一个很明显的问题就是只保存了模型的参数,并没有保存模型的结构,所以我们在加载的时候需要使用之前定义的模型结构,然后通过模型加载dict。torch.save的本质就是调用pickle,把dict序列化成pt或pth,仅此而已,torch.load则是使用pickle把之前保存的pt再转换成dict,二model.load_state_dict则是根据dict的key找到模型对应的名字,对权重赋值,仅此而已。所以我们会发现即使大家都使用pytorch,相同的模型结构,加载的时候还是有可能找不到key的,就是因为有的人给网络参数起了不同的名字,这个时候就需要在load之后,把dict的key给重命名一下,然后再保存就可以了。
# save model dict to pt/pth
torch.save(model.state_dict(), PATH)
# load model
model = TheModelClass(*args, **kwargs)
model.load_state_dict(torch.load(PATH))
model.eval()
还有一个小坑也需要注意一下,我们训练模型的时候通常会指定device,如果device=cuda:1,那么保存的时候也会把这个device的信息保存到模型中,直接使用torch.load默认会加载到cuda:1,如果我们只有cpu或者只有一个cuda:0,那就会报错,所以加载的时候需要把device重映射一下(本质就跟修改dict的key一样),
device = torch.device('cpu')
model = TheModelClass(*args, **kwargs)
model.load_state_dict(torch.load(PATH, map_location=device))
mode.to(device)
# cuda:1映射到cuda:0
device = torch.device('cuda:0')
model = TheModelClass(*args, **kwargs)
model.load_state_dict(torch.load(PATH, map_location=device))
mode.to(device)
import os
import torchclass SaveCheckpoint:def __init__(self, output_dir):self.output_dir = output_dirself.model_dir = os.path.join(output_dir, 'checkpoints')if not os.path.exists(self.model_dir):os.makedirs(self.model_dir)def save(self, global_step, model, optimizer=None, scheduler=None, arguments=None):if hasattr(model, 'module'):model = model.module # extract model from a distributed/data-parallel wrappercheckpoint_dir = os.path.join(self.model_dir, f'{global_step}')if not os.path.exists(checkpoint_dir):os.makedirs(checkpoint_dir)checkpoint_path = os.path.join(checkpoint_dir, f"pytorch_model.bin")checkpoint = {'global_step': global_step, 'model_state_dict': model.state_dict()}if optimizer:checkpoint['optimizer_state_dict'] = optimizer.state_dict()if scheduler:checkpoint['scheduler_state_dict'] = scheduler.state_dict()if arguments:checkpoint['arguments'] = argumentstorch.save(checkpoint, checkpoint_path)return checkpoint_pathdef train(args):sb = SaveCheckpoint(args.output_dir)device = torch.device(f"cuda:{args.local_rank}" if torch.cuda.is_available() else "cpu")model = nn.Linear(5, 1, bias=False).to(device)if torch.distributed.is_initialized():if torch.cuda.is_available():model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank])else:model = torch.nn.parallel.DistributedDataParallel(model)optimizer = torch.optim.Adam(model.parameters(),lr=0.01)step = 0for epoch in range(args.epochs):for data in data_loader():step += 1optimizer.zero_grad()py = model(torch.rand(32,5).to(device))loss = F.mse_loss(py,torch.rand(32,1).to(device))loss.backward() # 计算梯度optimizer.step() # 更新权重if args.rank == 0 and step % args.save_step == 0:sb.save(step, model)
相关文章:
PyTorch分布式训练中各节点如何通信
深度学习 文章目录 深度学习前言pytorch如何初始化分布式训练怎么知道要使用哪几台机器进行训练的如何根据标识进行初始化(init_method)如何获取进程的唯一标识rank如何实现数据如何分发 前言 同学们在处理分布式训练时经常会遇到以下几个疑问ÿ…...
又双叒叕Scrapy爬虫相关的面试题及详细解答
Scrapy是Python开发的一个快速、高层次的网络爬虫框架,专注于高效抓取网页并提取结构化数据。其核心设计基于异步处理机制,适合大规模数据采集任务。 文章目录 基础概念1. Scrapy框架的核心组件有哪些?架构与流程2. 描述Scrapy的工作流程核心组件详解3. 如何自定义Item Pipe…...
Docker与K8S是什么该怎么选?
用了很久的容器化,最近突然看到一个问题问: docker和K8S究竟有什么区别,到底该怎么选?我认真思考了一会,发现一时间还真说不明白,于是就研究了一段时间发布今天的博文! Docker vs Kubernetes&a…...
FPGA中串行执行方式之计数器控制
FPGA中串行执行方式之计数器控制 使用计数器控制的方式实现状态机是一种简单且直观的方法。它通过计数器的值来控制状态的变化,从而实现顺序逻辑。计数器的方式特别适合状态较少且状态转移是固定的场景。 基本原理 计数器控制的状态机 例程1:简单的顺序状态机 以下是一个…...
尝试使用tauri2+Django+React的项目
前言 使用Tauri2前端,本质是进程间的通信。并非前后端。 而想使用nw,先后端打包exe,再和前端打包成exe,并没有完成成功。 而笔者从Tauri中看到这种可能性。很有可能成功基于SeaORMMySQLTauri2ViteReact等的CRUD交互项目-CSDN博…...
用@keyframes-animation来实现动画效果
一、使用规则 keyframes 用于定义动画的关键帧。 animation属性 用于将keyframes动画用于元素上。 二、基本语法 keyframes keyframes xuanZhuan { /*xuanZhuan是动画名字,实现旋转*/0%{transform: rotate(0deg);}50%{transform: rotate(180deg);}100%{transform: rotate(…...
kernel中外部传递参数使用方法
在 Linux 内核模块开发中,module_param(rpc_tdebug, uint, 0600); 表示定义一个可通过外部传递参数进行配置的模块级变量,具体解析如下: 参数名称 rpc_tdebug 是模块参数的变量名,该变量需在代码中提前声明为静态全局变量&…...
AI赋能流域生态评估:从多源数据融合到服务价值预测的技术突破
流域生态系统服务评价是生态学与地理信息科学的交叉前沿,传统方法受限于数据碎片化与模型解释力不足。本文以随机森林-时空图卷积联合模型(RF-STGCN)为核心,结合2022年长江中游实际监测数据,详解AI技术在服务评价中…...
SZU软件工程大学生涯 2022~2026
用于个人面试前自我介绍,防止忘记或谈吐不流利。 面试官您好,我是来自深圳大学计算机与软件学院的软件工程专业的王雅贤。在校期间,我修读了程序设计基础、面向对象程序设计、数据结构、算法分析与设计、操作系统等核心课程,系统…...
如何设计一个合理的库存系统
库存管理系统是电商、供应链管理、仓储管理等核心系统之一。一个合理的库存系统需要同时满足高并发、数据一致性、实时性、扩展性等要求,以确保在各种业务场景下都能稳定运行。 本文将探讨如何设计一个合理的库存系统,包括库存模型设计、数据一致性策略…...
数据人的进阶之路:四年数仓实践与成长思考
前言 在数据仓库开发的过程中,常常会遇到很多值得思考的问题,它们不仅关乎技术的深度,也涉及业务理解、个人的成长,甚至是数据行业未来的价值。回顾过去的经历,有很多问题反复出现,甚至成为绕不开的课题&am…...
数据库原理及应用mysql版陈业斌实验一
🏝️专栏:Mysql_猫咪-9527的博客-CSDN博客 🌅主页:猫咪-9527-CSDN博客 “欲穷千里目,更上一层楼。会当凌绝顶,一览众山小。” 目录 实验一:数据库与数据表的定义和数据操作 1.实验数据如下 …...
Linux环境变量:深入解析与实用指南
目录 一、环境变量概述 二、环境变量的作用 三、环境变量的类型 3.1系统环境变量 3.2用户环境变量 四、环境变量的操作 4.1查看环境变量 4.2设置环境变量 4.3删除环境变量 五、环境变量的配置文件 六、环境变量的最佳实践 七、总结 环境变量是Linux系统中至关重要的…...
大数据 Spark 技术简介
Apache Spark 是一种快速、通用、可扩展的大数据处理引擎,最初由加州大学伯克利分校开发。它提供了一种高效的数据处理框架,可以处理大规模数据集,并在分布式计算集群上进行并行处理。 Apache Spark 的基本概念包括以下几个要点:…...
Go语言的基础类型
一基础数据类型 一、布尔型(Bool) 定义:表示逻辑真 / 假,仅有两个值:true 和 false内存占用:1 字节使用场景:条件判断、逻辑运算 二、数值型(Numeric) 1. 整数类型&…...
面试复习-基础网络+运维知识
一、TCP/IP模型及每层对应通信协议 1.1第一层-应用层 作用:服务及应用程序 HTTP --- 超文本传输协议--- 获取网页信息---80(TCP 80) HTTPS --- HTTP SSL(安全传输协议)/TLS ---443(TCP 443) …...
大屏设计新纪元:定制视觉盛宴
当城市天际线的巨型LED幕墙与元宇宙中的虚拟场景无缝交织,当博物馆的数字藏品在8K曲面屏上焕发新生,一个属于大屏设计的全新纪元已悄然降临。这场视觉革命不仅重构了信息传播的维度,更将“定制化体验”推向了前所未有的高度——每一寸屏幕都成…...
JavaIO流的使用和修饰器模式(直击心灵版)
系列文章目录 JavaIO流的使用和修饰器模式 文章目录 系列文章目录前言一、字节流: 1.FileInputStream(读取文件)2.FileOutputStream(写入文件) 二、字符流: 1..基础字符流:2.处理流:3.对象处理流:4.转换流: 三、修饰器…...
10-STL、位运算、常用函数库
1-STL vector vector是变长数组 //定义vector vector<int>a;//第一维长233,第二维长度动态变化 vector<int>b[233];//自定义的结构体类型也可以保存在vector中 struct res{...}; vector<rec>c;//函数 a.size();//返回vector的实际长度…...
【Ratis】Ratis Streaming概览
看了Tsz-Wo Nicholas Sze博士的一个关于Ratis的share,在share里提到了raits做的一个性能优化:客户端流。比较感兴趣,特此记录一下。如果想看原始分享的,可以搜关键词:Apache Ratis - A High Performance Raft Library 关于Ratis Stream的pdf介绍,在这个PR的附件里: [Ra…...
Python Seaborn面试题及参考答案
目录 如何用 stripplot () 绘制带随机偏移的分类散点图?如何控制 jitter 参数? swarmplot () 如何避免散点重叠?适用场景与数据量限制是什么? 使用 catplot () 绘制箱线图时,如何通过 kind 参数切换图表类型? 如何通过 hue 参数在分类图中添加第三个维度(如性别)? …...
linux下基本命令和扩展命令(安装和登录命令、文件处理命令、系统管理相关命令、网络操作命令、系统安全相关命令、其他命令)欢迎补充噢
基本命令 ls: 列出目录内容 ls:列出当前目录内容ls -l:以长格式列出(显示详细信息)ls -a:显示隐藏文件ls -lh:以易读格式显示文件大小 pwd: 显示当前工作目录 pwd:显示当前目录的绝对路径 cd:…...
K8S学习之基础四十:K8S配置altermanager发送告警到钉钉群
配置altermanager发送告警到钉钉群 创建钉钉群,设置机器人助手(必须是管理员才能设置),获取webhook webhook: https://oapi.dingtalk.com/robot/send?access_token25bed933a52d69f192347b5be4b2193bc0b257a6d9ae68d81619e3ae3d93f7c6…...
实用工具--OfficeAI 助手 v0.3.20(长期免费,2025-03-18 本地支持WPSWord联动)
软件简介 OfficeAI助手,作为Microsoft Office与WPS的得力智能插件,集文档自动生成、内容精准校对与润色、公式智能推荐等多功能于一体。它凭借强大的数据分析能力,深度融入Office/WPS办公生态,一键简化复杂流程,让办公…...
Android 关于compose的一些坑和理解
** 1.如何在 WindowManager.addView 中使用 Jetpack Compose** 一、引出问题 Android 开发中,很常见的一个场景,通过 WindowManager.addView() 添加一个 View 到屏幕上。Android 最新的视图框架 Jetpack Compose,如何应用进来。这个被添加的…...
ref setState 合成事件
ref & setState & 合成事件 受控组件的概念:数据改变视图的叫受控组件;通过dom操作改变的叫非受控。 语法:给refxxx赋一个值,然后通过this.refs.xxx就可以获取到相应dom元素,通过你这个名字存储的值就是这个do…...
调用feapder作为子程序时setting.py文件不起作用
feaper 官方文档地址: 简介及安装 - feapder官方文档|feapder-document 问题: 在最近的开发中需要调用feapder作为主程序调用的子程序时发现自动入库时无法入库,通过查看日志信息发现连接数据库时被拒绝连接了,但是我的setting.p…...
gralloc1_perform具体在干什么
gralloc1_perform 会在特定场景下通过 ioctl 调用,执行 缓存 (cache) 管理 和 内存映射 操作,确保 CPU 和 GPU 之间的数据一致性。 📌 为什么需要对 cache 进行操作? 在 Android 系统中,CPU 和 GPU 通常共享 DDR 内存…...
【Pandas】pandas Series plot.barh
Pandas2.2 Series Plotting 方法描述Series.plot([kind, ax, figsize, …])用于绘制 Series 对象的数据可视化图表Series.plot.area([x, y, stacked])用于绘制堆叠面积图(Stacked Area Plot)Series.plot.bar([x, y])用于绘制垂直条形图(Ver…...
机器学习之浅层神经网络
文章目录 一、浅层神经网络概述(一)定义(二)常见类型 二、浅层神经网络的前向传播三、浅层神经网络的反向传播四、编写浅层神经网络案例(Python NumPy 实现)代码解释 五、浅层神经网络与深度学习的区别&am…...
透析Vue的nextTick原理
nextTick 是 Vue.js 中的一个核心机制,用于在 下一次 DOM 更新周期后 执行回调函数。它的核心原理是 利用 JavaScript 的事件循环机制(Event Loop),结合微任务(Microtask)或宏任务(Macrotask&am…...
Beans模块之工厂模块注解模块@Qualifier
博主介绍:✌全网粉丝5W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验…...
产品更新 | 数字助决胜:华望M-Arch平台实现从体系模型到仿真推演
华望产品更新速递 功能介绍 |M-Arch体系建模软件的核心功能 ◆体系架构建模 ◆逻辑仿真 ◆与多种工具集成 ◆多专业协同建模 产品亮点 |M-Arch软件在体系作战中的作用 ◆全面构建任务和体系架构建模的能力 ◆模型化的装备体系分析方法 ◆提升作战体系架构设计与优化 前…...
IP地址结构体与字符串转换函数详解
IP地址结构体与字符串转换函数详解 在Linux C网络编程中,IP地址的二进制结构体(如struct in_addr)与字符串形式(如"192.168.1.1")之间的转换经常涉及到,与IP地址格式相关的函数包括inet_aton、i…...
debug - 安装.msi时,为所有用户安装程序
文章目录 debug - 安装.msi时,为所有用户安装程序概述笔记试试在目标.msi后面直接加参数的测试 备注备注END debug - 安装.msi时,为所有用户安装程序 概述 为了测试,装了一个test.msi. 安装时,只有安装路径的选择,没…...
基于springboot的社区团购系统(012)
摘 要 本课题是根据用户的需要以及网络的优势建立的一个社区团购系统,来满足用户团购的需求。 本社区团购系统应用Java技术,MYSQL数据库存储数据,基于Spring Boot框架开发。在网站的整个开发过程中,首先对系统进行了需求分析&…...
应用权限组列表
文章目录 使用须知位置相机麦克风通讯录日历运动数据身体传感器图片和视频音乐和音频跨应用关联设备发现和连接剪切板文件夹文件(deprecated) 使用须知 在申请目标权限前,建议开发者先阅读应用权限管控概述-权限组和子权限,了解相关概念,再合…...
4.1、网络安全模型
目录 网络安全体系概述网络安全模型-BLP模型网络安全模型-Biba模型网络安全模型 - 信息流模型信息保障模型能力成熟度模型其它安全模型网络安全原则 网络安全体系概述 网络安全体系是网络安全保证系统的最高层概念抽象,是一个体系,体系一般是一个概念&a…...
前端对接生成式AI接口(类ChatGPT)问题汇总
文章目录 前端实现对话流问题总结流式数据传输问题后台Response Headers问题大量数据分段接收问题多个流时间戳(Time)相同导致被合并的问题 中止对话问题复制问题部署上线问题(Nginx缓冲导致) 前端实现对话流问题总结 流式数据传…...
布隆过滤器(Bloom Filter)详解
布隆过滤器详解 1. 什么是布隆过滤器? 布隆过滤器(Bloom Filter)是一种高效的概率型数据结构,主要用于判断某个元素是否存在于一个集合中。它的特点是: 允许误判:可能会误判元素存在(假阳性&…...
QoS(Quality of Service)服务质量概念
一、什么是QoS? QoS(Quality of Service)即服务质量,在网络业务中可以通过保证传输的带宽、降低传输时延、降低数据丢包率以及时延抖动等措施来提高服务质量。QoS是一套用于管理和提高网络性能的工具和技术,用于流量优…...
CSS实现当鼠标悬停在一个元素上时,另一个元素的样式发生变化的效果
CSS可以实现当鼠标悬停在一个元素上时,另一个元素的样式发生变化的效果。可以通过以下几种方法来实现: 1. 使用兄弟选择器(Adjacent Sibling Selector) 如果两个元素是兄弟关系(即它们有相同的父元素)&am…...
【C++11】左值引用、右值引用、移动语义和完美转发
🦄个人主页:修修修也 🎏所属专栏:C ⚙️操作环境:Visual Studio 2022 目录 📌左值引用和右值引用 🎏左值和左值引用 🎏右值和右值引用 📌左值引用和右值引用比较 🎏左值引用 🎏右值…...
Docker镜像迁移
目录 1.查看镜像当前配置 2.镜像迁移 1. 停止 Docker Desktop 2. 关闭 WSL 实例(若基于 WSL 2) 4. 导出原镜像数据 5.注销原实例 6. 导入数据到新路径 7. 设置 Docker Desktop 使用新路径(可选) 8. 启动 Docker Desktop …...
Compose 实践与探索十五 —— 自定义触摸
1、自定义触摸与一维滑动监测 之前我们在讲 Modifier 时讲过如下与手势检测相关的 Modifier: Modifier.clickable { } Modifier.combinedClickable { } Modifier.pointerInput {detectTapGestures { } }这里对以上内容就不再赘述了,直接去讲解更复杂的…...
第P8周:YOLOv5-C3模块实现
🍨 本文为🔗365天深度学习训练营中的学习记录博客 🍖 原作者:K同学啊 1. 模块组成 C3 模块由 卷积层(Convolutional layers)、激活函数(Activation functions) 和 残差连接&#…...
知识蒸馏:让大模型“瘦身“而不失智慧的魔术
引言:当AI模型需要"减肥" 在人工智能领域,一个有趣的悖论正在上演:大模型的参数规模每年以10倍速度增长,而移动设备的算力却始终受限。GPT-4的1750亿参数需要价值500万美元的GPU集群运行,但现实中的智能设备…...
`docker commit`和`docker tag`
1.docker commit docker commit是一个 Docker 命令,用于将一个正在运行的容器(Container)的状态提交为一个新的镜像(Image)。这类似于在版本控制系统中提交更改。 作用 • 当你对一个容器进行了修改(例如安…...
构建下一代AI Agent:自动化开发与行业落地全解析
1. 下一代AI Agent:概念与核心能力 核心能力描述技术支撑应用价值自主性独立规划与执行任务,无需持续人工干预决策树、强化学习、目标导向规划减少人工干预,提高任务执行效率决策能力评估多种方案并选择最优解决方案贝叶斯决策、多目标优化、…...
项目篇:模拟实现高并发内存池(2)
1.整体框架的设计 首先我们要来大概的梳理一下我们的高并发内存池的整体框架设计 在现代很多开发环境其实都是多核多线程,在申请内存的情况下,就必然会存在激烈的锁竞争问题。如果我们需要要实现内存池,必须要考虑以下几方面的问题。 1.性…...