Transformer从菜鸟到新手(五)

引言

上篇文章我们在单卡上完成了完整的训练过程。

从本文开始介绍模型训练/推理上的一些优化技巧,本文主要介绍多卡并行训练。

下篇文章将介绍大模型推理常用的缓存技术。

多卡训练

第一个要介绍的是利用多GPU优化,因为在单卡上训练实在是太慢。这里使用的是PyTorch提供的DistributedDataParallel

还有一种简单的方法是DataParallel,但效率没有DistributedDataParallel高。

DistributedDataParallel is proven to be significantly faster than torch.nn.DataParallel for single-node multi-GPU data parallel training.

分布式数据并行训练(Distributed Data Parallel Training, DDP)是一种广泛采用的单程序多数据训练范式。使用DDP,模型在每个进程上都被复制,每个模型副本将被提供不同的输入数据样本。DDP负责梯度通信,以保持模型副本同步,并将其与梯度计算重叠,以加快训练速度。

如果想让你的单GPU训练代码可并行化,而且只想做最少的改动,那么你可以选择DataParallel,但正如上面所说,它的效率不高。因此我们使用DistributedDataParallel来进一步加速训练。

由于只有单机资源,因此本文不会涉及多机训练,只关注单机多GPU。

我们先了解下将涉及到的几个术语:

  • 主节点(master node):负责同步、复制以及加载模型和记录日志的主GPU;
  • 进程组(process group):要并行训练的N个GPU组成一个组,由nccl后端支持;
  • 排名(rank):在进程组内,每个进程通过其排名进行标识,从0到N-1。rank=0为主节点;
  • 世界大小(world size):进程组内的进程数量,即GPU数量N;

DistributedDataParallel通过在每个模型副本之间同步梯度来提供数据并行,要同步的设备由输入process_group指定,默认情况下是所有设备(entire world)。注意DistributedDataParallel需要由用户指定如何对参与的GPU进行分片,比如通过使用DistributedSampler对数据进行分片。也就是说假设有N个GPU,我们可以对数据切分成N部分,每个GPU只需要处理原来 1 N \frac{1}{N} N1大小的数量,但批大小可以保持不变,从而加速训练过程。

假设在一个包含N个GPU的设备上。
image-20240105155641931

多GPU示意图, 图片来自https://pytorch.org/tutorials/beginner/ddp_series_theory.html

首先通过torch.distributed.init_process_group来创建进程组;

我们接着需要创建(spawn)N个进程,并且要确保每个进程独占从0到N-1的单个GPU,可以通过为每个进程设置torch.cuda.set_device(i)来实现。要创建进程可以通过torch.multiprocessing.spawn来实现;

torch.distributed.init_process_group(backend='nccl', world_size=N, init_method='...'
)
model = DistributedDataParallel(model, device_ids=[i], output_device=i)

DistributedDataParallel可以与 torch.distributed.optim.ZeroRedundancyOptimizer 结合使用,以减少每个rank上优化器状态的内存占用。

nccl 后端目前是使用 GPU 时最快且最受推荐的后端,适用于单节点和多节点分布式训练。

当模型在M个节点上以 batch=N进行训练时,如果损失在一个批次中的样本之间进行求和(而不是常用的平均),梯那度将比在单个节点上以 batch=M*N 进行训练的相同模型小 M 倍(因为不同节点之间的梯度是平均的)。

当想要获得与本地训练对应的数学等价训练过程时,你应该考虑这一点。但在大多数情况下,可以将一个 DistributedDataParallel 包装的模型和一个普通的单 GPU 上的模型视为相同的(例如,可以为同样的批大小使用同样的学习率)。

参数永远不会在进程之间广播。该模块(DistributedDataParallel)对梯度执行全局归约(all-reduce)步骤,并假定它们将以相同的方式被优化器在所有进程中修改。缓冲(如BatchNorm统计信息)从rank为 0 的进程开始,在每次迭代中对系统中的所有副本进行广播。

总结一下,我们要做的事情是:

  1. 设置进程组;
  2. 拆分进程组内的数据加载器;
  3. 通过DDP封装我们的模型;
  4. 训练/测试模型,与单GPU相同;
  5. 最后清理进程组,释放内存;

核心流程如下:

from argparse import ArgumentParserimport torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, Dataset
from torch.utils.data.distributed import DistributedSamplerSEED = 42
BATCH_SIZE = 8
NUM_EPOCHS = 3class YourDataset(Dataset):def __init__(self):passdef main():parser = ArgumentParser('DDP usage example')parser.add_argument('--local_rank', type=int, default=-1, metavar='N', help='Local process rank.')  # you need this argument in your scripts for DDP to workargs = parser.parse_args()# 记录当前进程是否为主节点args.is_master = args.local_rank == 0# 获取当期设备args.device = torch.cuda.device(args.local_rank)# 初始化进程组dist.init_process_group(backend='nccl', init_method='env://', world_size=N)# 设置GPU设备torch.cuda.set_device(args.local_rank)# 设置所有GPU的随机种子torch.cuda.manual_seed_all(SEED)# 初始化模型model = YourModel()# 将模型设置到GPUmodel = model.to(device)# 初始化DDPmodel = DDP(model,device_ids=[args.local_rank],output_device=args.local_rank)# 初始化数据集dataset = YourDataset()# 初始化分布式采样器sampler = DistributedSampler(dataset)# 基于分布式采样器初始化数据加载器dataloader = DataLoader(dataset=dataset,sampler=sampler,batch_size=BATCH_SIZE)# 开始训练for epoch in range(NUM_EPOCHS):model.train()# 在开始新epoch之前,让所有进程保持同步dist.barrier()for step, batch in enumerate(dataloader):# 将数据发送到对应的设备batch = tuple(t.to(args.device) for t in batch)# 正常的前向传播outputs = model(*batch)# 计算损失 假设是基于Transformers的模型,它会在第一个变量中返回损失loss = outputs[0]if __name__ == '__main__':main()

下面来对单GPU训练代码进行改造。

首先额外引入三个包:

from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler
import torch.multiprocessing as mp

接着,定义一个函数用于初始化进程组:

def setup(rank: int, world_size: int) -> None:"""Args:rank (int): within the process group, each process is identified by its rank, from 0 to world_size - 1world_size (int): the number of processes in the group"""# Initialize the process group# world_size process forms a group which is supported by a backend(nccl)# rank 0 as master node# master node: the main gpu responsible for synchronizations, making copies, loading models, writing logs.dist.init_process_group("nccl", rank=rank, world_size=world_size)

同时定义清理函数:

def cleanup():"Cleans up the distributed environment"dist.destroy_process_group()

然后修改脚本入口代码:

if __name__ == "__main__":os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, train_args.gpus))# Sets up the process group and configuration for PyTorch Distributed Data Parallelismos.environ["MASTER_ADDR"] = "localhost"os.environ["MASTER_PORT"] = "12355"world_size = min(torch.cuda.device_count(), len(train_args.gpus))print(f"Number of GPUs used: {world_size}")mp.spawn(main, args=(world_size,), nprocs=world_size)

通过CUDA_VISIBLE_DEVICES环境变量设置可见的GPU;设置Master地址和端口;

调用spawn方法来创建进行,它需要传入要使用的GPU总数量,假设为N,它会依次创建rank=0到N-1的进程。

那么我们就看这个main函数是如何定义的。

def main(rank, world_size):print(f"Running  DDP on rank {rank}.")# 设置GPU设备torch.cuda.set_device(rank)setup(rank, world_size)# 加载分词器source_tokenizer, target_tokenizer = load_tokenizer(rank)# 设置随机种子set_random_seed(train_args.seed)# 获取训练集train_dataset = get_dataset(rank, source_tokenizer, target_tokenizer, "train")valid_dataset = get_dataset(rank, source_tokenizer, target_tokenizer, "dev")# 准备数据加载器train_dataloader = prepare_dataloader(train_dataset, rank, world_size, train_args.batch_size)valid_dataloader = prepare_dataloader(valid_dataset, rank, world_size, train_args.batch_size)# 定义模型并发送到设备rank上model = TranslationHead(model_args,target_tokenizer.pad_id(),target_tokenizer.bos_id(),target_tokenizer.eos_id(),).to(rank)# 是否为masteris_main_process = rank == 0# master负责打印if is_main_process:print(f"The model has {count_parameters(model)} trainable parameters")# 通过DDP封装modelmodel = DDP(model, device_ids=[rank])# 获取封装的modelmodule = model.module  # the wrapped modelargs = asdict(model_args)args.update(asdict(train_args))if train_args.use_wandb and is_main_process:import wandb# start a new wandb run to track this scriptwandb.init(# set the wandb project where this run will be loggedproject="transformer",config=args,)train_criterion = LabelSmoothingLoss(train_args.label_smoothing, model_args.pad_idx)valid_criterion = LabelSmoothingLoss(pad_idx=model_args.pad_idx)optimizer = torch.optim.Adam(model.parameters(), betas=train_args.betas, eps=train_args.eps)scheduler = WarmupScheduler(optimizer,warmup_steps=train_args.warmup_steps,d_model=model_args.d_model,factor=train_args.warmup_factor,)if train_args.calc_bleu_during_train:# bleu scoreearly_stopper = EarlyStopper(mode="max", patience=train_args.patient)best_score = 0.0else:# dev lossearly_stopper = EarlyStopper(mode="min", patience=train_args.patient)best_score = 1000if is_main_process:print(f"begin train with arguments: {args}")print(f"total train steps: {len(train_dataloader) * train_args.num_epochs}")for epoch in range(train_args.num_epochs):# 记录训练时长start = time.time()# 每个数据加载器的sampler需要指定当前的epochtrain_dataloader.sampler.set_epoch(epoch)valid_dataloader.sampler.set_epoch(epoch)# 调用训练函数train_loss = train(model,train_dataloader,train_criterion,optimizer,train_args.grad_clipping,train_args.gradient_accumulation_steps,scheduler,rank,)if is_main_process:print()# 显示GPU利用率GPUtil.showUtilization()# 清除GPU缓存torch.cuda.empty_cache()if is_main_process:print("begin evaluate")valid_loss = evaluate(model, valid_dataloader, valid_criterion, rank)torch.cuda.empty_cache()if train_args.calc_bleu_during_train:if is_main_process:print("calculate bleu score for dev dataset")# 计算bleu得分valid_bleu_score = calculate_bleu(model.module,target_tokenizer,valid_dataloader,train_args.max_gen_len,rank,save_result=True,save_path="result-dev.txt",)torch.cuda.empty_cache()metric_score = valid_bleu_scoreelse:valid_bleu_score = 0metric_score = valid_losselapsed = time.time() - start# 每个GPU都打印信息print(f"[GPU{rank}] end of epoch {epoch+1:3d} [{elapsed:4.0f}s]| train loss: {train_loss:.4f} | valid loss: {valid_loss:.4f} |  valid bleu_score {valid_bleu_score:.2f}")if is_main_process:if train_args.use_wandb:wandb.log({"train_loss": train_loss,"valid_bleu_score": valid_bleu_score,"valid_loss": valid_loss,})wandb.save(f"result-dev.txt")if train_args.calc_bleu_during_train:if metric_score > best_score:best_score = metric_scoreprint(f"Save model with best bleu score :{metric_score:.2f}")# 保存验证集上bleu得分最好的模型torch.save(module.state_dict(), train_args.model_save_path)else:if metric_score < best_score:best_score = metric_scoreprint(f"Save model with best valid loss :{metric_score:.4f}")torch.save(module.state_dict(), train_args.model_save_path)# 早停if early_stopper.step(metric_score):print(f"stop from early stopping.")break# 清理cleanup()

其中用到的一些函数定义如下。

准备数据加载器:

def prepare_dataloader(dataset, rank, world_size, batch_size, pin_memory=False, num_workers=0
):# 定义分布式采样器sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank, shuffle=False, drop_last=False)dataloader = DataLoader(dataset,batch_size=batch_size,pin_memory=pin_memory,num_workers=num_workers,collate_fn=dataset.collate_fn,drop_last=False,shuffle=False,sampler=sampler,)return dataloader

训练函数:

def train(model: nn.Module,data_loader: DataLoader,criterion: torch.nn.Module,optimizer: torch.optim.Optimizer,clip: float,gradient_accumulation_steps: int,scheduler: torch.optim.lr_scheduler._LRScheduler,rank: int,
) -> float:model.train()  # train mode# let all processes sync up before starting with a new epoch of trainingdist.barrier()total_loss = 0.0tqdm_iter = tqdm(data_loader)for step, batch in enumerate(tqdm_iter, start=1):# 发送到指定设备source, target, labels = [x.to(rank) for x in (batch.source, batch.target, batch.labels)]logits = model(source, target)# loss calculationloss = criterion(logits, labels)loss.backward()# 支持梯度累积if step % gradient_accumulation_steps == 0:if clip:torch.nn.utils.clip_grad_norm_(model.parameters(), clip)optimizer.step()optimizer.zero_grad(set_to_none=True)scheduler.step()total_loss += loss.item()description = f"[GPU{rank}] TRAIN  loss={loss.item():.6f}, learning rate={scheduler.get_last_lr()[0]:.7f}"del losstqdm_iter.set_description(description)# average training lossavg_loss = total_loss / len(data_loader)return avg_loss

主要修改差不多就完了,更详细的可以访问文末的仓库地址。

下面基于一个调好的配置训练一下,看下效果:

class TrainArugment:"""Create a 'data' directory and store the dataset under it"""dataset_path: str = f"{os.path.dirname(__file__)}/data/wmt"save_dir = f"{os.path.dirname(__file__)}/model_storage"src_tokenizer_file: str = f"{save_dir}/source.model"tgt_tokenizer_path: str = f"{save_dir}/target.model"model_save_path: str = f"{save_dir}/best_transformer.pt"dataframe_file: str = "dataframe.{}.pkl"use_dataframe_cache: bool = Truecuda: bool = Truenum_epochs: int = 40batch_size: int = 32gradient_accumulation_steps: int = 1grad_clipping: int = 0  # 0 dont use grad clipbetas: Tuple[float, float] = (0.9, 0.98)eps: float = 1e-9label_smoothing: float = 0warmup_steps: int = 4000warmup_factor: float = 0.5only_test: bool = Falsemax_gen_len: int = 60use_wandb: bool = Falsepatient: int = 5gpus = [1, 2, 3]seed = 12345calc_bleu_during_train: bool = True

这里使用了3块RTX 3090GPU。

训练过程日志为:

Number of GPUs used: 3
Running  DDP on rank 1.
Running  DDP on rank 0.
source tokenizer size: 32000
target tokenizer size: 32000
Loads cached train dataframe.
Loads cached dev dataframe.
Loads cached test dataframe.
The model has 93255680 trainable parameters
begin train with arguments: {'d_model': 512, 'n_heads': 8, 'num_encoder_layers': 6, 'num_decoder_layers': 6, 'd_ff': 2048, 'dropout': 0.1, 'max_positions': 5000, 'source_vocab_size': 32000, 'target_vocab_size': 32000, 'pad_idx': 0, 'norm_first': True, 'dataset_path': 'nlp-in-action/transformers/transformer/data/wmt', 'src_tokenizer_file': 'nlp-in-action/transformers/transformer/model_storage/source.model', 'tgt_tokenizer_path': 'nlp-in-action/transformers/transformer/model_storage/target.model', 'model_save_path': 'nlp-in-action/transformers/transformer/model_storage/best_transformer.pt', 'dataframe_file': 'dataframe.{}.pkl', 'use_dataframe_cache': True, 'cuda': True, 'num_epochs': 40, 'batch_size': 32, 'gradient_accumulation_steps': 1, 'grad_clipping': 0, 'betas': (0.9, 0.98), 'eps': 1e-09, 'label_smoothing': 0, 'warmup_steps': 4000, 'warmup_factor': 0.5, 'only_test': False, 'max_gen_len': 60, 'use_wandb': True, 'patient': 5, 'calc_bleu_during_train': True}
total train steps: 73760
[GPU0] TRAIN  loss=7.039197, learning rate=0.0001612: 100%|██████████| 1844/1844 [03:51<00:00,  7.98it/s]
[GPU1] TRAIN  loss=7.088427, learning rate=0.0001612: 100%|██████████| 1844/1844 [03:58<00:00,  7.74it/s]0%|          | 0/264 [00:00<?, ?it/s]
| ID | GPU | MEM |
------------------
|  0 |  1% | 22% |
|  1 | 82% | 80% |
|  2 | 96% | 74% |
|  3 | 88% | 75% |
begin evaluate
100%|██████████| 264/264 [00:06<00:00, 38.75it/s]
100%|██████████| 264/264 [00:06<00:00, 38.41it/s]
calculate bleu score for dev dataset
100%|██████████| 264/264 [00:07<00:00, 37.36it/s]
100%|██████████| 264/264 [03:28<00:00,  1.27it/s]98%|█████████▊| 260/264 [03:30<00:03,  1.24it/s][GPU1] end of epoch   1 [ 457s]| train loss: 8.0777 | valid loss: 7.1328 |  valid bleu_score 0.44
100%|██████████| 264/264 [03:33<00:00,  1.23it/s]
100%|██████████| 264/264 [03:34<00:00,  1.23it/s]
[GPU2] end of epoch   1 [ 463s]| train loss: 8.0691 | valid loss: 7.1192 |  valid bleu_score 0.470%|          | 0/1844 [00:00<?, ?it/s][GPU0] end of epoch   1 [ 456s]| train loss: 8.0675 | valid loss: 7.1118 |  valid bleu_score 0.42
Save model with best bleu score :0.42[GPU0] end of epoch   2 [ 429s]| train loss: 6.5028 | valid loss: 5.8428 |  valid bleu_score 6.66
Save model with best bleu score :6.66[GPU0] end of epoch   3 [ 422s]| train loss: 5.2749 | valid loss: 4.6848 |  valid bleu_score 16.72
Save model with best bleu score :16.72[GPU0] end of epoch   4 [ 430s]| train loss: 4.3027 | valid loss: 4.1180 |  valid bleu_score 21.81
Save model with best bleu score :21.81...[GPU0] end of epoch  12 [ 415s]| train loss: 2.1461 | valid loss: 3.6046 |  valid bleu_score 26.98
Save model with best bleu score :26.98[GPU0] end of epoch  17 [ 413s]| train loss: 1.6261 | valid loss: 3.7982 |  valid bleu_score 26.19
[GPU0] stop from early stopping.wandb: | 3.412 MB of 3.412 MB uploaded
wandb: Run history:
wandb:       train_loss █▆▅▄▃▃▃▂▂▂▂▂▁▁▁▁▁
wandb: valid_bleu_score ▁▃▅▇▇▇███████████
wandb:       valid_loss █▆▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁
wandb: 
wandb: Run summary:
wandb:       train_loss 1.62611
wandb: valid_bleu_score 26.19141
wandb:       valid_loss 3.79825

日志太多了,因此只摘录一部分,设置了随机种子,有条件的可以尝试复现。

从日志可以看到,在第12个epoch后就取得了验证集最佳得分26.98,并且每个epoch耗时从20分钟减少到了430秒,即7分钟左右, 基本上是减少了3倍,和GPU数量一致。

如果仔细分析每个epoch中耗时占比,会发现计算bleu得分和训练耗时和一样多,虽然我们已经对计算bleu得分进行批处理优化,但实际上我们还可以继续优化这个时间。

详见下篇文章~。

代码地址

完整代码点此

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

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

相关文章

java开发中如何使用定时任务

定时任务概述&#xff1a; 任务调度&#xff1a; 是指系统为了自动完成特定任务&#xff0c;在约定的特定时刻执行任务的过程。有了任务调度&#xff0c;即可解放更多的人力&#xff0c;而是由系统自动去执行任务。 常用业务场景案例&#xff1a; 某电商系统需要在每天上午10点…

利用“与非”运算实现布尔代数中的与,或,非三种运算

什么是“与非”运算&#xff1f; 要想明白“与非”运算&#xff0c;首先要明白“与”运算和“非”运算。 “与”运算在离散数学中叫做合取式&#xff0c;也就是A和B相同时为1的时候结果才为1&#xff0c;其余情况都为0 下面是“与”运算的真值表 “非”运算在离散数学中叫做否…

网络协议与攻击模拟_01winshark工具简介

一、TCP/IP协议簇 网络接口层&#xff08;没有特定的协议&#xff09; 物理层&#xff1a;PPPOE宽带拨号&#xff08;应用场景&#xff1a;宽带拨号&#xff0c;运营商切网过来没有固定IP就需要拨号&#xff0c;家庭带宽一般都采用的是拨号方式&#xff09;数据链路层网络层…

基于共享储能电站的工业用户日前优化经济调度【复现】

文章提出一种基于共享储能电站的工业用户日前优化经济调度方法。首先提出共享储能电站的概念&#xff0c;分析其 商业运营模式。然后将共享储能电站应用到工业用户经济优化调度中&#xff0c;通过协调各用户使用共享储能电站进行充电和 放电的功率&#xff0c;实现用户群日运行…

越南童模受邀参加上海顶级奢侈大秀

陈宝珠 - 2010年出生 - 从小就参与艺术 - 宝珠家庭中的长女。自幼就参加艺术活动&#xff0c;并在现代舞、编舞、古装舞、走秀等方面表现出色&#xff0c;在每一个科目上&#xff0c;宝珠都展现了她的风格和才华。 - 多次获得在越南艺术大赛冠军如&#xff1a;IKIDS越南冠军、T…

Linux第5步_测试虚拟机网络连接

安装好VMwareTools后&#xff0c;就可以测试虚拟机网络连接了&#xff0c;目的是实现虚拟机上网。 1、打开“控制面板”&#xff0c;得到下图&#xff1a; 2、双击“网络和 Internet” &#xff0c;得到下图&#xff1a; 3、双击“网络和共享中心” 4、点击“更改适配器设置”…

rime中州韵小狼毫 中英互绎 滤镜

英文在日常生活中已经随处可见&#xff0c;我们一般中英互译需要使用专业的翻译软件来实现。但如果我们在输入法中&#xff0c;在输入中文的时候&#xff0c;可以顺便瞟一眼对应的英文词汇&#xff0c;或者在输入英文的时候可以顺便了解对应的中文词汇&#xff0c;那将为我们的…

linux网络配置

一、查看Linux基础得网络设置 1.网关——route -n 2.IP地址——ifconfig 或 ip a ethtool -p ens33 让ens33网卡快速闪烁&#xff0c;分辨网线对应哪个网卡 3.DNS服务器——cat /etc/resolv.conf 4.主机名——hostname 5.路由——route 6.网络连接状态——ss 或 net…

【Java并发】聊聊concurrentHashMap的put核心流程

结构介绍 1.8中concurrentHashMap采用数组链表红黑树的方式存储&#xff0c;并且采用CASSYN的方式。在1.7中主要采用的是数组链表&#xff0c;segment分段锁reentrantlock。本篇主要在1.8基础上介绍下. 那么&#xff0c;我们的主要重点是分析什么呢&#xff0c;其实主要就是p…

银联扫码第三方支付接口申请:开启便捷支付新时代

随着移动支付的普及&#xff0c;越来越多的商家开始接受微信、支付宝等第三方支付平台的付款方式。然而&#xff0c;作为国内最大的银行卡组织&#xff0c;银联也在不断拓展其业务范围&#xff0c;推出了自己的扫码支付接口。本文将为您详细介绍银联扫码第三方支付接口的申请流…

GO语言笔记3-指针

指针的概念 先看一段代码的输出 package main import "fmt" func main(){ var age int 18fmt.Println("age的内存地址值是:",&age)//age的内存地址值是: 0xc000012090// 定义一个指针变量// *int 是一个指针类型&#xff0c;可以理解为指向int类型的…

两个视频怎么合并成一个视频?教你合并视频

两个视频怎么合并成一个视频&#xff1f;如果你是一名视频爱好者&#xff0c;或者是一名自媒体创作者&#xff0c;那么你一定遇到过需要将两个视频合并为一个的情况。有时候&#xff0c;你可能需要将一个长视频切割成多个片段&#xff0c;或者将多个视频片段合并成一个完整的视…

Spring MVC的RequestMapping注解、controller方法返回值

1.使用说明 作用&#xff1a;用于建立请求URL和处理请求方法之间的对应关系。 出现位置&#xff1a; 类上&#xff1a; 请求 URL的第一级访问目录。此处不写的话&#xff0c;就相当于应用的根目录。写的话需要以/开头。它出现的目的是为了使我们的 URL 可以按照模块化管理&…

我的1827创作纪念日

机缘 习惯性早上打开电脑&#xff0c;看看CSDN上的资讯&#xff0c;了解行业动态、当前新的技术和大佬的分享。自己动手写应该是2019 年 01 月 08 日&#xff0c;当时应该是在用安装和使用Oracle&#xff0c;遇到一些问题&#xff0c;写下第一篇博客 Oracle存储过程常见问题及…

一、Mybatis 简介

本章概要 简介持久层框架对比快速入门&#xff08;基于Mybatis3方式&#xff09; 1.1 简介 https://mybatis.org/mybatis-3/zh/index.html MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投G…

【解决方案】 无法将“pip“项识别为 cmdlet、函数、脚本文件

在当今的软件开发和运维领域&#xff0c;Python已经成为了一个不可或缺的工具。而pip&#xff0c;作为Python的包管理工具&#xff0c;更是Python生态系统中不可或缺的一部分。然而&#xff0c;有时候我们可能会遇到一个令人困扰的问题&#xff1a;无法将“pip”项识别为cmdlet…

zookeeper 与eureka区别

CAP定理 在分布式系统的发展中&#xff0c;影响最大的莫过于CAP定理了&#xff0c;是分布式系统发展的理论基石。 2000年&#xff0c;加州大学的计算机科学家 Eric Brewer提出了CAP猜想 2002 年&#xff0c;麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP 猜…

c++实现支持动态扩容的栈(stack)

1.在栈容量满时自动扩容: 支持自动扩容栈实现: // // myStack.hpp // algo_demo // // Created by Hacker X on 2024/1/9. //#ifndef myStack_hpp #define myStack_hpp #include <stdio.h> #include <string.h> //栈实现 //1.入栈 //2.出栈 //3.空栈 //4.满栈 …

栈的模拟实现

栈的模拟实现 一:什么是栈二:IStack 接口三:MyStack类:1:push(int x):2:pop()3:peek()4:size(),empty(),full() 三:四:栈的时间复杂度: 一:什么是栈 栈是以先进后出(后进先出)的形式来组织数据结构 比如: 先装入的子弹后射出,后装入的子弹先射出,这就是一种典型的栈. 二:ISta…

扩展欧几里得算法总结

知识概览 裴蜀定理&#xff1a;对于任意正整数a&#xff0c;b&#xff0c;一定存在非零整数x&#xff0c;y&#xff0c;使得 而且(a, b)是a和b能凑出来的最小的正整数。 通过扩展欧几里得算法可以求得裴蜀定理中x和y的值&#xff0c;x和y的通解为 &#xff0c; 例题展示 扩展欧…