简明Pytorch分布式训练 — DistributedDataParallel 实践

上一次的Pytorch单机多卡训练主要介绍了Pytorch里分布式训练的基本原理,DP和DDP的大致过程,以及二者的区别,并分别写了一个小样作为参考。小样毕竟还是忽略了很多细节和工程实践时的一些处理方式的。实践出真知,今天(简单)写一个实际可用的 DDP 训练中样,检验一下自己对 DDP 的了解程度,也作为以后分布式训练的模板,逐渐完善。

文章目录

  • DDP 流程回顾
    • 一些前置
  • DDP 实践
    • 训练前
    • 训练时
    • 启动脚本
  • 更上一层楼
  • 结语
  • Reference

DDP 流程回顾

Pytorch单机多卡训练中主要从DDP的原理层面进行了介绍,这次从实践的角度来看看。

试问把大象装进冰箱需要几步?三步,先打开冰箱,再把大象装进去,最后关上冰箱门。那实践中DDP又分为几步呢?

  1. 开始训练/预测前的准备工作。
  2. 开始训练/验证!

看,DDP比把大象装进冰箱简单多了!

一些前置

虽然DDP很简单,但是得先了解一些前置知识。有一些DDP相关的函数是必须要了解的1,主要是和分布式通信相关的函数:

  • torch.distributed.init_process_group2。初始化分布式进程组。由于是单机多卡看的分布式,因此其实是将任务分布在多个进程中。
  • torch.distributed.barrier()3。用于进程同步。每个进程进入这个函数后都会被阻塞,当所有进程都进入这个函数后,阻塞解除,继续执行后续的代码。
  • torch.distributed.all_gather4。收集不同进程中的tensor。这个用到的比较多的,但有一点不太好理解。解释一下:某些变量是不同进程中都会有的,比如loss,如果要把各个进程中计算得到的loss汇总到一起,就需要进程间通信,把loss收集起来,all_gather干的就是收集不同进程中的某个tensor的事儿。
  • local_rank。节点上device的标识。在单机多卡的模式下,每个device的local_rank是唯一的,一般我们都在0号设备上操作。该参数不需要手动传参,在执行DDP时会自动设置该参数。当为非DDP时,该参数值为-1。
  • torch.nn.parallel.DistributedDataParallel5。在初始化好的分布式环境中,创建分布式的模型,负责。

好了,你已经具备DDP实践的主要前置知识了,一起开启DDP实践吧!

DDP 实践

训练前

训练前的准备工作,主要包含:

  • 初始化分布式进程组,使后续各个进程间能够通信。
  • 准备好数据和模型。
torch.cuda.set_device(args.local_rank)
device = torch.device("cuda", args.local_rank)
torch.distributed.init_process_group(backend="nccl", timeout=datetime.timedelta(seconds=3600))
args.device = deviceX, Y = make_classification(n_samples=25000, n_features=N_DIM, n_classes=2, random_state=args.seed)  # 每个进程都会拥有这样一份数据集
X = torch.tensor(X).float()
Y = torch.tensor(Y).float()train_X, train_Y = X[:20000], Y[:20000]
val_X, val_Y = X[20000:], Y[20000:]
train_dataset = SimpleDataset(train_X, train_Y)
val_dataset = SimpleDataset(val_X, val_Y)model = SimpleModel(N_DIM)
if args.local_rank not in (-1, 0):torch.distributed.barrier()
else:# 只需要在 0 号设备上加载预训练好的参数即可,后续调用DDP时会自动复制到其他卡上if args.ckpt_path is not None:model.load_state_dict(torch.load(args.ckpt_path))torch.distributed.barrier()

训练时

与单卡训练不同,DDP训练主要有三个改动地方:

  • 创建数据加载器时,需要创建特定的采样器,以保证每个进程使用的数据是不重复的
  • 创建分布式的模型。
  • 收集一些辅助信息时,例如loss、prediction等,需要all_gather将不同进程中的数据收集到一起。
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank], output_device=args.local_rank, find_unused_parameters=False)train_sampler = RandomSampler(train_dataset) if args.local_rank == -1 else DistributedSampler(train_dataset)
train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=args.batch_size)...def gather_tensors(target):"""target should be a tensor"""target_list = [torch.ones_like(target) for _ in range(torch.distributed.get_world_size())]torch.distributed.all_gather(target_list, target)ret = torch.hstack(target_list)return retfor epoch in train_iterator:if args.local_rank != -1:train_dataloader.sampler.set_epoch(epoch)  # 必须设置的!确保每一轮的随机种子是固定的!model.zero_grad()model.train()pbar = tqdm(train_dataloader, desc="Training", disable=args.local_rank not in [-1, 0])step_loss, step_label, step_pred = [], [], []for step, batch in enumerate(pbar):data, label = batchdata, label = data.to(args.device), label.to(args.device)prediction = model(data)loss = loss_func(prediction, label.unsqueeze(1))optimizer.zero_grad()loss.backward()optimizer.step()global_step += 1step_loss.append(loss.item())step_label.extend(label.squeeze().cpu().tolist())step_pred.extend(prediction.squeeze().detach().cpu().tolist())if step % args.logging_step == 0:if torch.distributed.is_initialized():  # 兼容非 DDP 的模式step_loss = torch.tensor(step_loss).to(args.device)step_label = torch.tensor(np.array(step_label)).to(args.device)step_pred = torch.tensor(np.array(step_pred)).to(args.device)step_loss = gather_tensors(step_loss)step_label = gather_tensors(step_label).squeeze().cpu().numpy()step_pred = gather_tensors(step_pred).squeeze().cpu().numpy()

单卡训练时,收集一些辅助信息是很方便的,因为所有的数据都在同一进程中,所以不需要同步操作。在DDP模式下,每个进程使用不同的数据训练,为了记录训练过程的信息以及计算指标等情况,需要把所有数据的结果收集起来,这就需要用到all_gather了。

启动脚本

一种常用的DDP启动方式:

#! /usr/bin/env bash
node_num=4# DDP 模式
CUDA_VISIBLE_DEVICES="0,1,2,3" python -m torch.distributed.launch --nproc_per_node=$node_num --master_addr 127.0.0.2 --master_port 29502 ddp-demo.py \
--do_train \
--do_test \
--epochs 3 \
--batch_size 128 \
--lr 5e-6 \
--output_dir ./test_model \
--seed 2024

python -m torch.distributed.launch即把torch.distributed.launch这个模块作为一个脚本运行,后面接的 --nproc_per_node=$node_num --master_addr 127.0.0.2 --master_port 29502 ddp-demo.py都作为这个模块的参数,关于该模块的参数以及含义,可以参考:https://github.com/pytorch/pytorch/blob/main/torch/distributed/launch.py。

更上一层楼

果然,DDP训练比把大象装进冰箱要简单多了。

当然,这主要归功于Pytorch封装的好,还有很多细节是值得我们深究的。以下是一些我觉得可以进一步探索的:

  • 分布式训练时不同的后端之间的区别,比如gloompinccl。主要是加深对多进程通信的了解。
  • torch.nn.parallel.DistributedDataParallel内部的细节,比如梯度的同步过程及涉及到的算法等。
  • DistributedSampler是如何实现不同device读取不同的数据的。这一块其实原理很简单,源码也不多。
  • 多机多卡的实践6

结语

单卡或DP训练相比,DDP启动多个进程,每个进程读取对应的数据,每个step单独训练,能够大大提高显卡的利用率和降低训练时间。

初次接触DDP还是很容易让人茫然的,如果有过多进程的经验的会更好理解其中的一些概念以及分布式训练的流程。

目前写的这个demo虽然比较简单,但常用的内容都涉及到了,后续可以在这个版本上继续完善。

附完整代码:

import os
import time
import random
import logging
import argparse
import datetime
from tqdm import tqdm, trangeimport numpy as np
from sklearn import metrics
from sklearn.datasets import make_classificationimport torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
from torch.utils.data.distributed import DistributedSamplerimport torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDPN_DIM = 200
logging.basicConfig(format='[%(asctime)s] %(levelname)s - %(name)s - %(message)s',datefmt='%m-%d %H:%M:%S',level=logging.INFO)
logger = logging.getLogger("DDP")class SimpleModel(nn.Module):def __init__(self, input_dim):super(SimpleModel, self).__init__()self.fc = nn.Linear(input_dim, 1)def forward(self, x):return torch.sigmoid(self.fc(x))class SimpleDataset(Dataset):def __init__(self, data, target):self.data = dataself.target = targetdef __len__(self):return len(self.data)def __getitem__(self, idx):return self.data[idx], self.target[idx]def set_seed(args):seed = args.seedrandom.seed(seed)np.random.seed(seed)torch.manual_seed(seed)if args.n_gpu > 0:torch.cuda.manual_seed_all(seed)def parse_args():parser = argparse.ArgumentParser()## DDP:从外部得到local_rank参数。从外面得到local_rank参数,在调用DDP的时候,其会自动给出这个参数parser.add_argument("--local_rank", default=-1, type=int)parser.add_argument("--no_cuda", action="store_true", help="Whether to cuda.")parser.add_argument("--seed", default=2024, type=int)parser.add_argument("--ckpt_path", default="./ddp-outputs/checkpoint-100/model.pt", type=str)parser.add_argument("--output_dir", default="./ddp-outputs/", type=str)parser.add_argument("--epochs", default=3, type=int)parser.add_argument("--lr", default=1e-5, type=float)parser.add_argument("--batch_size", default=2, type=int)parser.add_argument("--val_step", default=50, type=int)parser.add_argument("--save_step", default=10, type=int)parser.add_argument("--logging_step", default=2, type=int)parser.add_argument("--do_train", action="store_true", help="Whether to run training.")parser.add_argument("--do_test", action="store_true", help="Whether to run eval on the test set.")args = parser.parse_args()return argsdef gather_tensors(target):"""target should be a tensor"""target_list = [torch.ones_like(target) for _ in range(torch.distributed.get_world_size())]torch.distributed.all_gather(target_list, target)# ret = torch.cat(target_list, 0)ret = torch.hstack(target_list)return retdef train(args, model, train_dataset, val_dataset=None):if args.n_gpu > 1:model = torch.nn.DataParallel(model)model = model.to(args.device)if args.local_rank != -1:model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank], output_device=args.local_rank, find_unused_parameters=False)logger.info(f"After DDP, Rank = {args.local_rank}, weight = {model.module.fc.weight.data[0, :10]}")# 采样器train_sampler = RandomSampler(train_dataset) if args.local_rank == -1 else DistributedSampler(train_dataset)train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=args.batch_size)optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=args.lr)loss_func = nn.BCELoss().to(args.local_rank)total_steps = len(train_dataloader) * args.epochsglobal_step = 0train_iterator = trange(0, int(args.epochs), desc="Epoch", disable=args.local_rank not in [-1, 0])for epoch in train_iterator: #range(args.epochs):  # train_iterator:if args.local_rank != -1:train_dataloader.sampler.set_epoch(epoch)if args.local_rank in (-1, 0):logger.info('*' * 25 + f" Epoch {epoch + 1} / {args.epochs} " + '*'* 25)model.zero_grad()model.train()pbar = tqdm(train_dataloader, desc="Training", disable=args.local_rank not in [-1, 0])step_loss, step_label, step_pred = [], [], []for step, batch in enumerate(pbar):data, label = batchdata, label = data.to(args.device), label.to(args.device)prediction = model(data)loss = loss_func(prediction, label.unsqueeze(1))optimizer.zero_grad()loss.backward()optimizer.step()global_step += 1time.sleep(0.1)step_loss.append(loss.item())step_label.extend(label.squeeze().cpu().tolist())step_pred.extend(prediction.squeeze().detach().cpu().tolist())if step % args.logging_step == 0:if torch.distributed.is_initialized():  # 兼容非 DDP 的模式step_loss = torch.tensor(step_loss).to(args.device)step_label = torch.tensor(np.array(step_label)).to(args.device)step_pred = torch.tensor(np.array(step_pred)).to(args.device)step_loss = gather_tensors(step_loss)step_label = gather_tensors(step_label).squeeze().cpu().numpy()step_pred = gather_tensors(step_pred).squeeze().cpu().numpy()if args.local_rank in (-1, 0):# logger.info(f"Gathered loss = {step_loss}")# logger.info(f"Gathered label = {step_label}")# logger.info(f"Label shape = {step_label.shape}, Pred = {step_pred}, {prediction.shape}")if not all(step_label == 1) and not all(step_label == 0):auc = metrics.roc_auc_score(step_label, step_pred)# logger.info(f"Step AUC = {auc:.5f}")pbar.set_description(f"loss={step_loss.mean():>.5f}, auc={auc:.5f}")step_loss, step_label, step_pred = [], [], []if step % args.val_step == 0 and val_dataset is not None:test_val(args, model, val_dataset)if global_step % args.save_step == 0 and global_step > 0 and args.local_rank in [-1, 0]:save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")if not os.path.exists(save_path):os.makedirs(save_path)model_to_save = model.module if hasattr(model, 'module') else modeltorch.save(model_to_save.state_dict(), os.path.join(save_path, "model.pt"))torch.save(optimizer.state_dict(), os.path.join(save_path, "optimizer.pt"))def test_val(args, model, dataset):# 采样器test_sampler = RandomSampler(dataset) if args.local_rank == -1 else DistributedSampler(dataset)test_dataloader = DataLoader(dataset, sampler=test_sampler, batch_size=args.batch_size)ground_truth = []prediction = []pbar = tqdm(test_dataloader, desc="Testing", disable=args.local_rank not in [-1, 0])with torch.no_grad():for step, batch in enumerate(pbar):data, label = batchdata, label = data.to(args.device), label.to(args.device)pred = model(data)time.sleep(0.05)ground_truth.extend(label.cpu().squeeze().tolist())prediction.extend(pred.cpu().squeeze().tolist())ground_truth = torch.tensor(ground_truth).to(args.device)prediction = torch.tensor(prediction).to(args.device)if args.local_rank != -1:  # 等待所有进程预测完,再进行后续的操作torch.distributed.barrier()if torch.distributed.is_initialized():  # 由于每张卡上都会有 prediction 和 ground_truth,为了汇集所有卡上的信息,需要 torch.distributed.all_gatherground_truth = gather_tensors(ground_truth)prediction = gather_tensors(prediction)if args.local_rank in [-1, 0]:print(f"GT : {ground_truth.shape}\tPred : {prediction.shape}")auc = metrics.roc_auc_score(ground_truth.detach().cpu(), prediction.detach().cpu())logger.info(f"Test Auc: {auc:.5f}")def main(args):if args.local_rank == -1 or args.no_cuda:device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")args.n_gpu = torch.cuda.device_count()else:torch.cuda.set_device(args.local_rank)device = torch.device("cuda", args.local_rank)torch.distributed.init_process_group(backend="nccl", timeout=datetime.timedelta(seconds=3600))args.n_gpu = 1args.device = deviceif args.local_rank in (-1, 0):logger.info('*' * 66)for k, v in vars(args).items():print(f"########## {k:>20}:\t{v}")logger.info('*' * 66)set_seed(args)model = SimpleModel(N_DIM)if args.local_rank not in (-1, 0):logger.info(f"Barrier in {args.local_rank}")torch.distributed.barrier()  # barrier 用于进程同步。每个进程进入这个函数后都会被阻塞,当所有进程都进入这个函数后,阻塞解除。else:if args.ckpt_path is not None:logger.info(f"Load pretrained model in {args.local_rank}")model.load_state_dict(torch.load(args.ckpt_path))logger.info(f"Barrier in {args.local_rank}")torch.distributed.barrier()logger.info(f"After barrier in {args.local_rank}")logger.info(f"Before DDP, Rank = {args.local_rank}, weight = {model.fc.weight.data[0, :10]}")X, Y = make_classification(n_samples=25000, n_features=N_DIM, n_classes=2, random_state=args.seed)  # 每个进程都会拥有这样一份数据集X = torch.tensor(X).float()Y = torch.tensor(Y).float()# 通过这个可以看到,在不同进程上的 X 的 id 是不同的,但是内容是一致的logger.info(f"Rank = {args.local_rank}, id(X) = {id(X)}, Y[:20] = {Y[:20]}")  train_X, train_Y = X[:20000], Y[:20000]val_X, val_Y = X[20000:], Y[20000:]train_dataset = SimpleDataset(train_X, train_Y)val_dataset = SimpleDataset(val_X, val_Y)if args.do_train:train(args, model, train_dataset, val_dataset)if args.do_test:test_val(args, model, val_dataset)if __name__ == "__main__":args = parse_args()main(args)

在这里插入图片描述

Reference


  1. DISTRIBUTED COMMUNICATION PACKAGE - TORCH.DISTRIBUTED. ↩︎

  2. 官方文档:torch.distributed.init_process_group. ↩︎

  3. 官方文档:torch.distributed.barrier. ↩︎

  4. 官方文档:torch.distributed.all_gather. ↩︎

  5. 官方文档-torch.nn.parallel.DistributedDataParallel. ↩︎

  6. Multi Node PyTorch Distributed Training Guide For People In A Hurry. ↩︎

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

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

相关文章

记 SpringBoot 使用@RequestBody 接收不到参数

POST请求,前端传的参数名字跟后端规定的参数一样。但是通过RequestBody注解接收的参数始终为NULL! //实体类中属性没有用驼峰命名 private String SubscribeID; /*** 标题*/ private String Title;解决方案: 1、字段上使用JsonProperty(valu…

深入理解数据结构(1):复杂度详解

文章主题:复杂度详解🌱所属专栏:深入理解数据结构📘作者简介:更新有关深入理解数据结构知识的博主一枚,记录分享自己对数据结构的深入解读。😄个人主页:[₽]的个人主页🔥…

面试宝典:深入剖析golang 反射在orm模型中的应用

在 Go 语言中,反射(Reflection)是一种强大的机制,它允许程序在运行时检查和修改自身的结构和行为。在 ORM(Object-Relational Mapping,对象关系映射)模型中,反射被广泛应用于将数据库中的表记录映射到 Go 语言的结构体实例,以及将结构体实例的数据持久化到数据库中。以…

【微服务】OpenFeign+Sentinel集中处理远程调用异常

文章目录 1.微服务基本环境调整1.对10004模块的application.yml调整2.启动nacos以及一个消费者两个提供者3.测试1.输入http://localhost:8848/nacos/index.html 来查看注册情况2.浏览器访问 http://localhost:81/member/nacos/consumer/get/13.结果 2.使用OpenFeign实现微服务模…

EtherCAT主站SOEM -- 22 -- Wireshark抓取并分析EtherCAT数据

EtherCAT主站SOEM -- 22 -- Wireshark抓取并分析EtherCAT数据 0 QT-SOEM视频预览及源代码下载:0.1 QT-SOEM视频预览0.2 QT-SOEM源代码下载1.Wireshark下载及安装2.Wireshark抓取EtherCAT数据2.1 我抓取的数据包3.Wireshark过滤EtherCAT数据3.1 筛选EtherCAT数据的COE数据:3.2…

C# comboBox

在C#中,ComboBox 是一个常用的控件,它允许用户从下拉列表中选择一个项目,或者输入自定义的文本(取决于 ComboBox 的 DropDownStyle 属性设置)。ComboBox 控件通常用于显示一系列固定的选项,让用户能够快速地…

2024年北京通信展|北京国际信息通信展览会|北京PT展

2024年北京通信展|北京国际信息通信展览会|北京PT展 2024年中国国际信息通信展览会(PTEXPO),是由工业和信息化部主办的ICT行业盛会,自1990年创办以来,已成功举办31届,是反映信息通信行业发展最新成果的重要…

Mysql 常用语句及用法记录

一、mysql简介-常用命令: MySQL是一种关系型数据库管理系统,它提供了许多命令和用法来管理和操作数据库。以下是一些常用的MySQL命令及其用法: 1. 连接数据库: mysql -u username -p 用于连接到MySQL服务器,其中usern…

【Java数据结构】关于栈的操作出栈,压栈,中缀表达式,后缀表达式,逆波兰表达式详解

🔥个人主页:努力学编程’ 🔥内容管理:java数据结构 上一篇文章我们讲过了java数据结构的链表,对于链表我们使用了它的一些基本操作,完成了扑克牌小游戏的操作,如果你感兴趣的话,点…

MATLAB 自定义均值滤波 (53)

MATLAB 自定义均值滤波 (53) 一、算法介绍二、算法实现1.原理2.代码一、算法介绍 均值滤波,是一种常见的点云平滑算法,改善原始点云的数据质量问题,MATLAB自带的工具似乎不太友好,这里提供自定义实现的点云均值滤波算法,具体效果如下所示: 均值滤波前: 均值滤波后:…

Pycharm选择使用Anaconda环境中的Pytorch 失败解决办法之一

前几日想要复现一篇论文,结果给配的台式机完全禁不住,老是报溢出,慢都没事,溢出就很难受了,因此想用自己笔记本的GPU来训练。 安装以后遇到一个问题: Anaconda里创建了环境,安装好了对应pytor…

RPC--远程调用

通信调用 程序A(加密) 程序B 内存共享 (本地RPC) 发送窗口信息 (本地RPC) --长度有限制 串口通讯 com口 --浏览器不开串口... 通讯管道(防止多开) (本地RPC) --对我们不可见. 网络 TCP/IP (远程RPC) --good! 浏览器(, ws) <--- 任意语言开发的软件 --任意语言控制浏览器. 注…

Linux基础知识

文章目录 一、入门命令&#xff1a;1.find 条件 要查找的文件满足的条件&#xff08;从当前目录开始查找&#xff09;&#xff1a;2.locate 文件名&#xff1a;3.lear CTRL L &#xff08;清除终端窗口&#xff09;与cat&#xff08;打印输出文件内容&#xff09;&#xff1a…

CSS3新增的语法(一)

1. CSS3 新增长度单位 rem根元素字体大小的倍数&#xff0c;只与根元素字体大小有关。vw 视口宽度的百分之多少------10vw 就是视口宽度的10% 。vh 视口高度的百分之多少 ------10vh 就是视口高度的10% 。vmax 视口宽高中大的那个的百分之多少。&#xff08;了解即可&#xff…

[TS面试]TS中使用Union Types时注意事项?

TS中使用Union Types时注意事项? 属性和方法的访问? 只能访问共有属性或方法 function getLength(something: string | number):number{return something.length // wrong, 因为number 类型时候没有 .length }function getString(something: string | number):string{retur…

网络性能提升10%,ZStack Edge 云原生超融合基于第四代英特尔®至强®可扩展处理器解决方案发布

随着业务模式的逐渐转变、业务架构逐渐变得复杂&#xff0c;同时容器技术的兴起和逐渐成熟&#xff0c;使得Kubernetes、微服务等新潮技术逐步应用于业务应用系统上。 为了充分释放性能、为业务系统提供更高效的运行环境&#xff0c;ZStack Edge 云原生超融合采用了第四代英特尔…

ROS传感器图像转换

ros通过摄像头来获得图片&#xff0c;传感器数据类型为sensor_msgs中的Image&#xff0c;具体的数据类型组成&#xff1a; sensor_msgs/Image Documentationhttp://docs.ros.org/en/api/sensor_msgs/html/msg/Image.html但是我们一般使用opencv对图像进行处理&#xff0c;所以…

elementui 导航菜单栏和Breadcrumb 面包屑关联

系列文章目录 一、elementui 导航菜单栏和Breadcrumb 面包屑关联 文章目录 系列文章目录前言一、elementui 导航菜单栏和Breadcrumb 面包屑怎么关联&#xff1f;二、实现效果三、实现步骤1.本项目演示布局2.添加面包屑2.实现breadcrumbName方法3.监听方法4.路由指配5.路由配置…

FastAPI+React全栈开发15 让我们构建一个展示API

Chapter03 Getting Started with FastAPI 15 Let’s Build a showcase API FastAPIReact全栈开发15 让我们构建一个展示API REST APIs are all about cycles of HTTP requests and responses, it is the engine that powers the web and is implemented in every web framew…

全国青少年软件编程(Python)等级考试三级考试真题2023年12月——持续更新.....

青少年软件编程&#xff08;Python&#xff09;等级考试试卷&#xff08;三级&#xff09; 分数&#xff1a;100 题数&#xff1a;38 一、单选题(共25题&#xff0c;共50分) 1.一个非零的二进制正整数&#xff0c;在其末尾添加两个“0”&#xff0c;则该新数将是原数的&#xf…