MNIST DDP 分布式数据并行
Distributed Data Parallel
转自我的个人博客:https://shar-pen.github.io/2025/05/04/torch-distributed-series/3.MNIST_DDP/
The difference between DistributedDataParallel and DataParallel is: DistributedDataParallel uses multiprocessing where a process is created for each GPU, while DataParallel uses multithreading.
By using multiprocessing, each GPU has its dedicated process, this avoids the performance overhead caused by GIL of Python interpreter.
- DDP vs DP 的并发模式
-
DDP使用的是多进程multiprocessing
- 每个 GPU 对应一个独立的 Python 进程。
- 各 GPU/进程之间通过通信(比如 NCCL)同步梯度。
- 进程之间可以并行,每个进程独占一个 GPU,自由度高、效率高。
-
DP 使用的是多线程(multithreading)
- 一个 Python 主进程控制多个线程,每个线程对应一个 GPU 上的模型副本。
- 所有线程共享同一个 Python 解释器(主进程中的 GIL 环境)。
- 在多线程环境下,同一时刻只能有一个线程执行 Python 字节码
- GIL 的性能问题
Python 有个限制叫 GIL(Global Interpreter Lock):
- 在 Python 中,进程之间可以并行,线程之间只能并发。
- 在多线程环境下,同一时刻只能有一个线程执行 Python 字节码。
- 这意味着虽然多个线程运行在不同 GPU 上,但只要你涉及到 Python 层的逻辑(如 forward 调度、数据调度),就会被 GIL 限制,造成瓶颈。
DDP 的多进程模式就天然绕开了 GIL,每个进程有独立的 Python 解释器和 GIL,不会互相争抢锁。所以执行速度更快、效率更高、更适合大模型和多 GPU 并行。
To use DistributedDataParallel on a host with N GPUs, you should spawn up N processes, ensuring that each process exclusively works on a single GPU from 0 to N-1.
总结下,DDP 用多进程给每个 GPU 配一个独立的进程,这样就不用多个线程去抢 Python 的 GIL,避免了 DataParallel 由于多线程带来的性能开销。
分布式数据并行时,模型(model parameters)/优化器(optimizer states)每张卡都会拷贝一份(replicas),在整个训练过程中 DDP 始终在卡间维持着模型参数和优化器状态的同步一致性;
DDP 将 batch input,通过 DistributedSampler split & 分发到不同的 gpus 上,此时虽然模型/optimizer 相同,但因为数据输入不同,导致 loss 不同,反向传播时计算到的梯度也会不同,如何保证卡间,model/optimizer 的同步一致性,之前 DP 用的 parameter server,而它的问题就在于通信压力都在 server,所以 DDP 对这方面的改进是 ring all-reduce algorithm,将 Server 上的通讯压力均衡转到各个 Worker 上
注意有两个核心概念:
- All to one:reduce
- one to All:broadcast
方法名 | 通信结构 | 通信路径 | 数据流向 | 聚合策略 | 通信瓶颈位置 | 通信效率 | 适合场景 |
---|---|---|---|---|---|---|---|
Parameter Server | 中心化(星型) | 所有 Worker ⇄ PS | 上传全部梯度 → 聚合 → 下发参数 | PS 聚合 | PS 带宽和计算压力 | ❌ 低(集中式瓶颈) | 小规模训练,原型实验 |
Tree All-Reduce | 层次化(树型) | 节点间按树结构上传/下传 | 层层上传聚合 → 再层层广播 | 层次加和 & 广播 | 上层节点(树根) | ✅ 中( log N \log N logN 轮次) | 多机多卡,合理拓扑连接 |
Broadcast + Reduce | 两阶段(集中) | 所有 → 主节点(reduce) → 所有 | 所有上传 → 中心聚合 → 广播下发 | 单节点聚合 | 主节点 | ❌ 低 | 小规模单机多卡 |
Ring All-Reduce | 环形(对称) | 相邻节点之间点对点传输 | 均匀传递/聚合,每轮处理一块数据 | 分块加和 & 拼接 | 无集中瓶颈 | ✅✅ 高(带宽最优) | 大规模 GPU 并行,主流方案 |
Parameter Server(PS)和 Broadcast + Reduce 在通信机制上本质相似,区别只在于:
- PS 是显式设计了专门的“参数服务器”角色;
- Broadcast + Reduce 是“隐式指定”某个节点承担聚合与广播任务。
ring all-reduce:
- Reduce-scatter:首先将 gradient 分为 n 块,在第 i 轮 (0<= i < n-1),每个 gpu j 把 第 (i+j) % n 块的数据传给下一个 gpu (j+1 % n),即每个 gpu 都把自己一个块给下一个做加法,在 n 轮结束后,每个 gpu 上都有一个块是完整的聚合了所有不同 gpu 的 gradient。
- All-gather: 将每个 gpu 上的完整聚合后的 gradient 依次传给下一个 gpu,再传递 n-1 次就使所有 gpu 的每块 gradient 都是完整聚合的数据。
虽然传递的数据量还是和 PS 一样,但传输压力平均到每个 gpu 上,不需要单个 worker 承担明显大的压力。
概念/参数名 | 中文含义 | 含义解释 | 示例(2节点 × 每节点4GPU) |
---|---|---|---|
world | 全局进程空间 | 指整个分布式系统中参与训练的所有进程总和 | 2 节点 × 4 GPU = 8 个进程 |
world_size | 全局进程数 | world 中的进程总数,参与通信、同步、梯度聚合的总 worker 数 | 8 |
rank | 全局进程编号 | 当前进程在 world 中的唯一编号,范围是 [ 0 , world_size − 1 ] [0, \text{world\_size} - 1] [0,world_size−1] | 第1节点是 0~3,第2节点是 4~7 |
node | 物理节点/机器 | 实际的服务器或物理机,每个节点运行多个进程,通常对应一台机器 | 2台服务器(假设每台4 GPU) |
node_rank | 节点编号 | 当前节点在所有节点中的编号,通常用于标识不同机器 | 第1台是 0,第2台是 1 |
local_rank | 本地GPU编号 | 当前进程在所在节点上的 GPU 编号,绑定 cuda(local_rank) | 每台机器上分别为 0~3 |
简洁点,world 代表所有服务器上的 gpu,rank 代表 world 视角下的 gpu 编号;node 代表某个具体的服务器,node_rank 代表 world 视角下的 node 编号,local_rank 代表 node 视角下的 gpu 编号。
引入 DDP 相关库
import os
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader# 以下是分布式相关的
import torch.multiprocessing as mp
from torch.utils.data.distributed import DistributedSampler # 分发数据集
from torch.nn.parallel import DistributedDataParallel as DDP # 用 DDP 封装 module 以支持分布式训练
from torch.distributed import init_process_group, destroy_process_group # 初始化和销毁进程组,一个 process 代表一个 gpu 进程
ddp 对原始代码的修改
参数 | 作用说明 |
---|---|
MASTER_ADDR | 指定 主节点(rank=0 所在节点)的 IP 地址或主机名,作为所有进程连接的“服务器” |
MASTER_PORT | 指定主节点上用于通信监听的端口号,所有进程都通过这个端口进行连接与协调 |
为什么只需要指定主节点的地址和端口?所有进程必须“集合”在一起组成一个通信组(process group);这个过程需要一个 协调者,就像组织会议需要一个人发出会议链接一样;PyTorch DDP 把这个协调角色交给 rank == 0 的进程(主节点);其它进程只需要“知道去哪找这个协调者”就能完成初始化。
主节点负责协调组网,在 DDP 初始化时,所有节点主动连接主节点,每个节点都会告知主节点自己的地址和端口,主节点收集所有其他进程的网络信息,构建全局通信拓扑,将通信配置信息广播回每个进程,包括每个 rank 要连接哪些 peer,这样每个进程就可以进行后续的双向传输,而不再依赖主节点作为中转。
主节点(rank 0) | 工作节点(rank 1,2,…) |
---|---|
在 MASTER_PORT 启动一个监听服务(如 TCP server) | 主动连接 MASTER_ADDR:MASTER_PORT |
监听并接受连接,记录加入者信息 | 与主节点握手,注册自己的 rank 、地址等 |
构建通信拓扑,如 Ring 或 NCCL 分组等 | 一旦接入,就获得组网配置,与其他 worker 点对点通信 |
ddp 初始化和销毁进程
def ddp_setup(rank, world_size):"""Args:rank: Unique identifier of each processworld_size: Total number of processes"""# rank 0 processos.environ["MASTER_ADDR"] = "localhost"os.environ["MASTER_PORT"] = "12355"# nccl:NVIDIA Collective Communication Library # 分布式情况下的,gpus 间通信torch.cuda.set_device(rank)init_process_group(backend="nccl", rank=rank, world_size=world_size)
DDP 会在每个 GPU 上运行一个进程,每个进程中都有一套完全相同的 Trainer 副本(包括 model 和 optimizer),各个进程之间通过一个进程池进行通信。
ddp 包装 model
训练函数不需要多大的修改,使用 DistributedDataParallel 包装模型,这样模型才能在各个进程间同步参数。包装后 model 变成了一个 DDP 对象,要访问其参数得这样写 self.model.module.state_dict()
运行过程中单独控制某个进程进行某些操作,比如要想保存 ckpt,由于每张卡里都有完整的模型参数,所以只需要控制一个进程保存即可。需要注意的是:使用 DDP 改写的代码会在每个 GPU 上各自运行,因此需要在程序中获取当前 GPU 的 rank(gpu_id),这样才能对针对性地控制各个 GPU 的行为。
class Trainer:def __init__(self,model: torch.nn.Module,train_data: DataLoader,optimizer: torch.optim.Optimizer,gpu_id: int,save_every: int, ) -> None:self.gpu_id = gpu_idself.model = model.to(gpu_id)self.train_data = train_data self.optimizer = optimizerself.save_every = save_everyself.model = DDP(model, device_ids=[gpu_id]) # model 要用 DDP 包装一下def _run_batch(self, source, targets):self.optimizer.zero_grad()output = self.model(source)loss = F.cross_entropy(output, targets)loss.backward()self.optimizer.step()def _run_epoch(self, epoch):b_sz = len(next(iter(self.train_data))[0])print(f"[GPU{self.gpu_id}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")self.train_dataloader.sampler.set_epoch(epoch) # 注意需要在各 epoch 入口调用该 sampler 对象的 set_epoch() 方法,否则每个 epoch 加载的样本顺序都不变for source, targets in self.train_data:source = source.to(self.gpu_id)targets = targets.to(self.gpu_id)self._run_batch(source, targets)def _save_checkpoint(self, epoch):ckp = self.model.state_dict()PATH = "checkpoint.pt"torch.save(ckp, PATH)print(f"Epoch {epoch} | Training checkpoint saved at {PATH}")def train(self, max_epochs: int):for epoch in range(max_epochs):self._run_epoch(epoch)if self.gpu_id == 0 and epoch % self.save_every == 0:self._save_checkpoint(epoch)
在程序入口初始化进程池;在程序出口销毁进程池
def main(rank: int, world_size: int, save_every: int, total_epochs: int, batch_size: int):# 初始化进程池ddp_setup(rank, world_size)# 进行训练dataset, model, optimizer = load_train_objs()train_data = prepare_dataloader(dataset, batch_size)trainer = Trainer(model, train_data, optimizer, rank, save_every)trainer.train(total_epochs)# 销毁进程池destroy_process_group()
DistributedSampler
构造 Dataloader 时使用 DistributedSampler 作为 sampler,这个采样器可以自动将数量为 batch_size 的数据分发到各个GPU上,并保证数据不重叠。理解是可以是这样的,但实际是根据 rank 让每个 gpu 能索引到的数据不一样,每个 gpu 上也是有重复的 Dataloader 的,但每个gpu 上 rank 设置不同,Dataloader sample 先根据 shuffle 打乱顺序,再控制不同 rank 能索引到的数据,以实现类似分发的效果。
Rank 0 sees: [4, 7, 3, 0, 6] Rank 1 sees: [1, 5, 9, 8, 2]
def prepare_dataloader(dataset: Dataset, batch_size: int):return DataLoader(dataset,batch_size=batch_size,pin_memory=True,shuffle=False, # 设置了新的 sampler,参数 shuffle 要设置为 False sampler=DistributedSampler(dataset) # 这个 sampler 自动将数据分块后送个各个 GPU,它能避免数据重叠)
set_epoch(epoch) 用于设置当前训练 epoch,以确保在分布式训练中 每个进程对数据的打乱顺序一致,从而保证每个 rank 分到的数据是互不重叠且可复现的。
当 DistributedSampler 的 shuffle=True 时,它在每个 epoch 会用 torch.Generator().manual_seed(seed) 生成新的随机索引顺序。
但:
- 如果不调用 set_epoch(),每个进程将使用相同的默认种子;
- 会导致每个 epoch 每个进程打乱后的样本索引相同 → 重复取样,每个 epoch 的训练数据都一样 → 训练不正确!
你确实可以不手动设置 rank 和 world_size,因为 DistributedSampler 会自动从环境变量中获取它们。
如果你不传入 rank 和 num_replicas,PyTorch 会调用:
- torch.distributed.get_world_size() # 获取 world_size
- torch.distributed.get_rank() # 获取当前进程 rank
import torch
from torch.utils.data import Dataset, DataLoader, DistributedSampler# 自定义一个简单的数据集:返回 [0, 1, ..., n-1]
class RangeDataset(Dataset):def __init__(self, n):self.data = list(range(n))def __len__(self):return len(self.data)def __getitem__(self, idx):return self.data[idx]# 模拟两张卡(进程)下的样本访问情况,并支持 set_epoch
def simulate_distributed_sampler(n=10, world_size=2, num_epochs=2):dataset = RangeDataset(n)for epoch in range(num_epochs):print(f"\nEpoch {epoch}")for rank in range(world_size):# 设置 shuffle=True 并调用 set_epochsampler = DistributedSampler(dataset,num_replicas=world_size,rank=rank,shuffle=True,)sampler.set_epoch(epoch) # 关键:确保每轮不同但在所有 rank 一致dataloader = DataLoader(dataset, batch_size=1, sampler=sampler)data_seen = [batch[0].item() for batch in dataloader]print(f"Rank {rank} sees: {data_seen}")simulate_distributed_sampler(n=10, world_size=2, num_epochs=2)
Epoch 0
Rank 0 sees: [4, 7, 3, 0, 6]
Rank 1 sees: [1, 5, 9, 8, 2]Epoch 1
Rank 0 sees: [5, 1, 0, 9, 7]
Rank 1 sees: [6, 2, 8, 3, 4]
multiprocessing.spawn 创建多卡进程
使用 torch.multiprocessing.spawn 方法将代码分发到各个 GPU 的进程中执行。在当前机器上启动 nprocs=world_size 个子进程,每个进程执行一次 main() 函数,并由 mp.spawn 自动赋值第一个参数(目的是执行 nprocs 个进程,第一个参数为 0 ~ nprocs-1)。
def start_process(i):# Each process is assigned a file to write tracebacks to. We# use the file being non-empty to indicate an exception# occurred (vs an expected shutdown). Note: this previously# used a multiprocessing.Queue but that can be prone to# deadlocks, so we went with a simpler solution for a one-shot# message between processes.tf = tempfile.NamedTemporaryFile(prefix="pytorch-errorfile-", suffix=".pickle", delete=False)tf.close()os.unlink(tf.name)process = mp.Process(target=_wrap,args=(fn, i, args, tf.name),daemon=daemon,)process.start()return i, process, tf.nameif not start_parallel:for i in range(nprocs):idx, process, tf_name = start_process(i)error_files[idx] = tf_nameprocesses[idx] = process
可以执行执行以下代码,它展现了 mp 创建进程的效果
import torch.multiprocessing as mpdef run(rank, message):print(f"[Rank {rank}] Received message: {message}")if __name__ == "__main__":world_size = 4 # 启动 4 个进程(模拟 4 个GPU)mp.spawn(fn=run,args=("hello world",), # 注意是 tuple 格式nprocs=world_size,join=True)
效果为:
[Rank 0] Received message: hello world
[Rank 3] Received message: hello world
[Rank 2] Received message: hello world
[Rank 1] Received message: hello world
# 利用 mp.spawn,在整个 distribution group 的 nprocs 个 GPU 上生成进程来执行 fn 方法,并能设置要传入 fn 的参数 args
# 注意不需要传入 fn 的 rank 参数,它由 mp.spawn 自动分配
import multiprocessing as mp
world_size = torch.cuda.device_count()
mp.spawn(fn=main, args=(world_size, args.save_every, args.total_epochs, args.batch_size), nprocs=world_size
)!CUDA_VISIBLE_DEIVES=0,1 python mnist_ddp.py
torchrun
torchrun 是 PyTorch 官方推荐的分布式训练启动工具,它用来 自动管理多进程启动、环境变量传递和通信初始化,替代早期的 torch.distributed.launch 工具。
- 它帮你在每个 GPU 上自动启动一个训练进程;
- 它设置好 DDP 所需的环境变量(如 RANK, WORLD_SIZE, LOCAL_RANK, MASTER_ADDR 等);
- 它会自动将这些参数传递给你的脚本中的 torch.distributed.init_process_group()。
torchrun == python -m torch.distributed.launch --use-env
参数名 | 类型 | 说明 |
---|---|---|
--nproc_per_node | int | 每台机器上启动的进程数(默认值为 1) |
--nnodes | int | 总节点(机器)数 |
--node_rank | int | 当前节点编号(范围:0 ~ nnodes-1 ) |
--rdzv_backend | str | rendezvous 后端(默认 c10d ,一般不改) |
--rdzv_endpoint | str | rendezvous 主地址和端口,格式如 "localhost:29500" |
--rdzv_id | str | 作业唯一标识,默认 "default" |
--rdzv_conf | str | 可选的 kv 参数,用逗号分隔,如 "key1=val1,key2=val2" |
--max_restarts | int | 失败时最多重启次数(默认 3) |
--monitor_interval | float | monitor 进程检查的间隔(秒) |
--run_path | str | 若脚本是模块路径形式,比如 my_module.train ,则用此代替 script |
--tee | str | 控制日志输出,可选值为 "stdout" 或 "stderr" |
--log_dir | str | 日志输出目录(默认当前目录) |
--redirects | str | 重定向日志,可选:all , none , rank ,如 all:stdout |
--no_python | flag | 若已是 Python 脚本(不用再次 python 调用),可加这个 flag |
以上的 rendezvous 是每个进程通过 rendezvous 找到主节点,然后加入。之后的通信阶段用 backend, 即 NCCL,在 init_process_group 设置。
最常见的几个参数的用法是
torchrun \--nproc_per_node=4 \--nnodes=1 \--node_rank=0 \--rdzv_endpoint=localhost:29500 \your_script.py
对比下是否使用 torchrun 时的行为差别
两种 DDP 启动模式的关键区别
对比项 | 不使用 torchrun (手动) | 使用 torchrun (推荐方式) |
---|---|---|
启动方式 | 使用 mp.spawn(fn, ...) | 使用 torchrun --nproc_per_node=N |
rank , world_size 设置方式 | 手动传入(通过 spawn 的参数) | 自动由 torchrun 设置环境变量 |
主节点地址 / 端口 | 你必须手动设置 MASTER_ADDR/PORT | torchrun 会自动设置这些环境变量 |
是否需控制进程数量 | 手动使用 spawn 创建 | 自动由 torchrun 创建 |
是否读取环境变量 | ❌ 默认不会 | ✅ 自动从环境变量中读取(如 RANK , LOCAL_RANK ) |
脚本能否直接运行(python train.py ) | ❌ 通常不行,需要多进程协调 | ✅ 支持直接 torchrun train.py 运行 |
是否适用于多机 | ❌ 手动处理跨节点逻辑 | ✅ 内建 --nnodes , --node_rank , 可跨机运行 |
init_process_group()
的行为
情况 | 说明 |
---|---|
手动传 rank 和 world_size | 常用于 mp.spawn 场景(你在代码里传了参数) |
不传,内部读取环境变量 | 如果你用的是 torchrun ,环境变量如 RANK 、WORLD_SIZE 自动设置了 |
不传又没用 torchrun | ❌ 报错:因为 init_process_group 找不到必要的通信信息 |
当你运行:
torchrun --nproc_per_node=4 --rdzv_endpoint=localhost:29500 train.py
它在后台自动设置了以下环境变量(对每个进程):
RANK=0 # 每个进程唯一编号
WORLD_SIZE=4 # 总进程数
LOCAL_RANK=0 # 当前进程在本节点内的编号
MASTER_ADDR=localhost
MASTER_PORT=29500
而 init_process_group(backend="nccl")
会自动从这些环境变量中解析配置,无需你显式传入。
非 torchrun 完整代码
import os
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import datasets, transforms
from time import time
import argparse# 对 python 多进程的一个 pytorch 包装
import torch.multiprocessing as mp
# 用于收集一些用于汇总的数据
import torch.distributed as dist
# 这个 sampler 可以把采样的数据分散到各个 CPU 上
from torch.utils.data.distributed import DistributedSampler # 实现分布式数据并行的核心类
from torch.nn.parallel import DistributedDataParallel as DDP # DDP 在每个 GPU 上运行一个进程,其中都有一套完全相同的 Trainer 副本(包括model和optimizer)
# 各个进程之间通过一个进程池进行通信,这两个方法来初始化和销毁进程池
from torch.distributed import init_process_group, destroy_process_group def ddp_setup(rank, world_size):"""setup the distribution process groupArgs:rank: Unique identifier of each processworld_size: Total number of processes"""# MASTER Node(运行 rank0 进程,多机多卡时的主机)用来协调各个 Node 的所有进程之间的通信os.environ["MASTER_ADDR"] = "localhost" # 由于这里是单机实验所以直接写 localhostos.environ["MASTER_PORT"] = "12355" # 任意空闲端口init_process_group(backend="nccl", # Nvidia CUDA CPU 用这个 "nccl"rank=rank, world_size=world_size)torch.cuda.set_device(rank)class ConvNet(nn.Module):def __init__(self):super(ConvNet, self).__init__()self.features = nn.Sequential(nn.Conv2d(1, 32, 3, 1),nn.ReLU(),nn.Conv2d(32, 64, 3, 1),nn.ReLU(),nn.MaxPool2d(2),nn.Dropout(0.25))self.classifier = nn.Sequential(nn.Linear(9216, 128),nn.ReLU(),nn.Dropout(0.5),nn.Linear(128, 10))def forward(self, x):x = self.features(x)x = torch.flatten(x, 1)x = self.classifier(x)output = F.log_softmax(x, dim=1)return output# 自定义Dataset
class MyDataset(Dataset):def __init__(self, data):self.data = datadef __len__(self):return len(self.data)def __getitem__(self, idx):image, label = self.data[idx]return image, labelclass Trainer:def __init__(self,model: torch.nn.Module,train_data: DataLoader,optimizer: torch.optim.Optimizer,gpu_id: int,save_every: int,) -> None:self.gpu_id = gpu_idself.model = model.to(gpu_id)self.train_data = train_dataself.optimizer = optimizerself.save_every = save_every # 指定保存 ckpt 的周期self.model = DDP(model, device_ids=[gpu_id]) # model 要用 DDP 包装一下def _run_batch(self, source, targets):self.optimizer.zero_grad()output = self.model(source)loss = F.cross_entropy(output, targets)loss.backward()self.optimizer.step()# 分布式同步 lossreduced_loss = loss.detach()dist.all_reduce(reduced_loss, op=dist.ReduceOp.SUM)reduced_loss /= dist.get_world_size()return reduced_loss.item()def _run_epoch(self, epoch):b_sz = len(next(iter(self.train_data))[0])print(f"[GPU{self.gpu_id}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")self.train_data.sampler.set_epoch(epoch) # 在各个 epoch 入口调用 DistributedSampler 的 set_epoch 方法是很重要的,这样才能打乱每个 epoch 的样本顺序total_loss = 0.0num_batches = 0for source, targets in self.train_data: source = source.to(self.gpu_id)targets = targets.to(self.gpu_id)loss = self._run_batch(source, targets)total_loss += lossnum_batches += 1avg_loss = total_loss / num_batchesif self.gpu_id == 0:print(f"[GPU{self.gpu_id}] Epoch {epoch} | Avg Loss: {avg_loss:.4f}")def _save_checkpoint(self, epoch):ckp = self.model.module.state_dict() # 由于多了一层 DDP 包装,通过 .module 获取原始参数 PATH = "checkpoint.pt"torch.save(ckp, PATH)print(f"Epoch {epoch} | Training checkpoint saved at {PATH}")def train(self, max_epochs: int):for epoch in range(max_epochs):self._run_epoch(epoch)# 各个 GPU 上都在跑一样的训练进程,这里指定 rank0 进程保存 ckpt 以免重复保存if self.gpu_id == 0 and epoch % self.save_every == 0:self._save_checkpoint(epoch)def prepare_dataset():transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))])train_data = datasets.MNIST(root = './mnist',train=True, # 设置True为训练数据,False为测试数据transform = transform,# download=True # 设置True后就自动下载,下载完成后改为False即可)train_set = MyDataset(train_data)test_data = datasets.MNIST(root = './mnist',train=False, # 设置True为训练数据,False为测试数据transform = transform,)test_set = MyDataset(test_data)return train_set, test_setdef load_train_objs():train_set, test_set = prepare_dataset() # load your datasetmodel = ConvNet() # load your modeloptimizer = optim.Adam(model.parameters(), lr=1e-3)return train_set, test_set, model, optimizerdef prepare_dataloader(dataset: Dataset, batch_size: int):return DataLoader(dataset,batch_size=batch_size,pin_memory=True,shuffle=False, # 设置了新的 sampler,参数 shuffle 要设置为 False sampler=DistributedSampler(dataset) # 这个 sampler 自动将数据分块后送个各个 GPU,它能避免数据重叠)def main(rank: int, world_size: int, save_every: int, total_epochs: int, batch_size: int):# 初始化进程池, 仅是单个进程 gpu rank 的初始化ddp_setup(rank, world_size)# 进行训练train_set, test_set, model, optimizer = load_train_objs()print(f"Train dataset size: {len(train_set)}")train_data = prepare_dataloader(train_set, batch_size)trainer = Trainer(model, train_data, optimizer, rank, save_every)trainer.train(total_epochs)# 销毁进程池destroy_process_group()def arg_parser():parser = argparse.ArgumentParser(description='MNIST distributed training job')parser.add_argument("--epochs", type=int, default=5, help="Number of training epochs")parser.add_argument("--batch_size", type=int, default=512, help="Batch size for training")parser.add_argument('--save_every', type=int, default=1, help='How often to save a snapshot')return parser.parse_args()r"""
README
执行命令: CUDA_VISIBLE_DEVICES=0,1 python mnist_ddp.py # 用 2 卡训练注意训练数据是60K条, 训练时输出:
[GPU0] Epoch 0 | Batchsize: 512 | Steps: 59
[GPU1] Epoch 0 | Batchsize: 512 | Steps: 59
512 * 59 = 30208 ~= 30K
排除掉有些 batch_size 不足的情况, 59个 batch 就是 30K, 两个 gpu 平分了数据"""if __name__ == "__main__":args = arg_parser()print(f"Training arguments: {args}")world_size = torch.cuda.device_count()print(f"Using {world_size} GPUs for training")# 利用 mp.spawn,在整个 distribution group 的 nprocs 个 GPU 上生成进程来执行 fn 方法,并能设置要传入 fn 的参数 args# 注意不需要 fn 的 rank 参数,它由 mp.spawn 自动分配mp.spawn(fn=main, args=(world_size, args.save_every, args.epochs, args.batch_size), nprocs=world_size)
启动代码
CUDA_VISIBLE_DEVICES=0,1 python mnist_ddp.py
torchrun 完整代码
import os
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import datasets, transforms
from time import time
import argparse# 对 python 多进程的一个 pytorch 包装
import torch.multiprocessing as mp
# 用于收集一些用于汇总的数据
import torch.distributed as dist
# 这个 sampler 可以把采样的数据分散到各个 CPU 上
from torch.utils.data.distributed import DistributedSampler # 实现分布式数据并行的核心类
from torch.nn.parallel import DistributedDataParallel as DDP # DDP 在每个 GPU 上运行一个进程,其中都有一套完全相同的 Trainer 副本(包括model和optimizer)
# 各个进程之间通过一个进程池进行通信,这两个方法来初始化和销毁进程池
from torch.distributed import init_process_group, destroy_process_group def ddp_setup():"""setup the distribution process groupArgs:rank: Unique identifier of each processworld_size: Total number of processes"""# 用torchrun 后台自动设置的环境变量init_process_group(backend="nccl")torch.cuda.set_device(int(os.environ['LOCAL_RANK']))class ConvNet(nn.Module):def __init__(self):super(ConvNet, self).__init__()self.features = nn.Sequential(nn.Conv2d(1, 32, 3, 1),nn.ReLU(),nn.Conv2d(32, 64, 3, 1),nn.ReLU(),nn.MaxPool2d(2),nn.Dropout(0.25))self.classifier = nn.Sequential(nn.Linear(9216, 128),nn.ReLU(),nn.Dropout(0.5),nn.Linear(128, 10))def forward(self, x):x = self.features(x)x = torch.flatten(x, 1)x = self.classifier(x)output = F.log_softmax(x, dim=1)return output# 自定义Dataset
class MyDataset(Dataset):def __init__(self, data):self.data = datadef __len__(self):return len(self.data)def __getitem__(self, idx):image, label = self.data[idx]return image, labelclass Trainer:def __init__(self,model: torch.nn.Module,train_data: DataLoader,optimizer: torch.optim.Optimizer,save_every: int,) -> None:self.gpu_id = int(os.environ['LOCAL_RANK']) # gpu_id 由 torchrun 自动设置self.model = model.to(self.gpu_id)self.train_data = train_dataself.optimizer = optimizerself.save_every = save_every # 指定保存 ckpt 的周期self.model = DDP(model, device_ids=[self.gpu_id]) # model 要用 DDP 包装一下def _run_batch(self, source, targets):self.optimizer.zero_grad()output = self.model(source)loss = F.cross_entropy(output, targets)loss.backward()self.optimizer.step()# 分布式同步 lossreduced_loss = loss.detach()dist.all_reduce(reduced_loss, op=dist.ReduceOp.SUM)reduced_loss /= dist.get_world_size()return reduced_loss.item()def _run_epoch(self, epoch):b_sz = len(next(iter(self.train_data))[0])print(f"[GPU{self.gpu_id}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")self.train_data.sampler.set_epoch(epoch)total_loss = 0.0num_batches = 0for source, targets in self.train_data: source = source.to(self.gpu_id)targets = targets.to(self.gpu_id)loss = self._run_batch(source, targets)total_loss += lossnum_batches += 1avg_loss = total_loss / num_batchesif self.gpu_id == 0:print(f"[GPU{self.gpu_id}] Epoch {epoch} | Avg Loss: {avg_loss:.4f}")def _save_checkpoint(self, epoch):ckp = self.model.module.state_dict() # 由于多了一层 DDP 包装,通过 .module 获取原始参数 PATH = "checkpoint.pt"torch.save(ckp, PATH)print(f"Epoch {epoch} | Training checkpoint saved at {PATH}")def train(self, max_epochs: int):for epoch in range(max_epochs):self._run_epoch(epoch)# 各个 GPU 上都在跑一样的训练进程,这里指定 rank0 进程保存 ckpt 以免重复保存if self.gpu_id == 0 and epoch % self.save_every == 0:self._save_checkpoint(epoch)def prepare_dataset():transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))])train_data = datasets.MNIST(root = './mnist',train=True, # 设置True为训练数据,False为测试数据transform = transform,# download=True # 设置True后就自动下载,下载完成后改为False即可)train_set = MyDataset(train_data)test_data = datasets.MNIST(root = './mnist',train=False, # 设置True为训练数据,False为测试数据transform = transform,)test_set = MyDataset(test_data)return train_set, test_setdef load_train_objs():train_set, test_set = prepare_dataset() # load your datasetmodel = ConvNet() # load your modeloptimizer = optim.Adam(model.parameters(), lr=1e-3)return train_set, test_set, model, optimizerdef prepare_dataloader(dataset: Dataset, batch_size: int):return DataLoader(dataset,batch_size=batch_size,pin_memory=True,shuffle=False, # 设置了新的 sampler,参数 shuffle 要设置为 False sampler=DistributedSampler(dataset) # 这个 sampler 自动将数据分块后送个各个 GPU,它能避免数据重叠)def main(save_every: int, total_epochs: int, batch_size: int):# 初始化进程池, 仅是单个进程 gpu rank 的初始化ddp_setup()# 进行训练train_set, test_set, model, optimizer = load_train_objs()print(f"Train dataset size: {len(train_set)}")train_data = prepare_dataloader(train_set, batch_size)trainer = Trainer(model, train_data, optimizer, save_every)trainer.train(total_epochs)# 销毁进程池destroy_process_group()def arg_parser():parser = argparse.ArgumentParser(description='MNIST distributed training job')parser.add_argument("--epochs", type=int, default=5, help="Number of training epochs")parser.add_argument("--batch_size", type=int, default=512, help="Batch size for training")parser.add_argument('--save_every', type=int, default=1, help='How often to save a snapshot')return parser.parse_args()r"""
README
执行命令: CUDA_VISIBLE_DEVICES=0,1 torchrun --nproc_per_node=2 mnist_ddp_torchrun.py
"""if __name__ == "__main__":args = arg_parser()print(f"Training arguments: {args}")world_size = torch.cuda.device_count()print(f"Using {world_size} GPUs for training")main(args.save_every, args.epochs, args.batch_size)
启动代码
CUDA_VISIBLE_DEVICES=0,1 torchrun --nproc_per_node=2 mnist_ddp_torchrun.py
相关文章:
MNIST DDP 分布式数据并行
Distributed Data Parallel 转自我的个人博客:https://shar-pen.github.io/2025/05/04/torch-distributed-series/3.MNIST_DDP/ The difference between DistributedDataParallel and DataParallel is: DistributedDataParallel uses multiprocessing where a proc…...
语音合成之十三 中文文本归一化在现代语音合成系统中的应用与实践
中文文本归一化在现代语音合成系统中的应用与实践 引言理解中文文本归一化(TN)3 主流LLM驱动的TTS系统及其对中文文本归一化的需求分析A. SparkTTS(基于Qwen2.5)与文本归一化B. CosyVoice(基于Qwen)与文本归…...
9.1.领域驱动设计
目录 一、领域驱动设计核心哲学 战略设计与战术设计的分野 • 战略设计:限界上下文(Bounded Context)与上下文映射(Context Mapping) • 战术设计:实体、值对象、聚合根、领域服务的构建原则 统一语言&am…...
如何配置光猫+路由器实现外网IP访问内部网络?
文章目录 前言一、网络拓扑理解二、准备工作三、光猫配置3.1 光猫工作模式3.2 光猫端口转发配置(路由模式时) 四、路由器配置4.1 路由器WAN口配置4.2 端口转发配置4.3 动态DNS配置(可选) 五、防火墙设置六、测试配置七、安全注意事…...
C++题题题题题题题题题踢踢踢
后缀表达式求值 #include<bits/stdc.h> #include<algorithm> using namespace std; string a[100]; string b[100]; stack<string> op; int la0,lb0; int main(){while(true){cin>>a[la];if(a[la]".") break;la;}for(int i0;i<la;i){if(…...
M. Moving Both Hands(反向图+Dijkstra)
Problem - 1725M - Codeforces 题目大意:给你一个有向图,起始点在1,问起始点分别与另外n-1个 点相遇的最短时间,无法相遇输出-1。 思路:反向建图,第一层建原图,第二层建反向图,两层…...
11、参数化三维产品设计组件 - /设计与仿真组件/parametric-3d-product-design
76个工业组件库示例汇总 参数化三维产品设计组件 (注塑模具与公差分析) 概述 这是一个交互式的 Web 组件,旨在演示简单的三维零件(如带凸台的方块)的参数化设计过程,并结合注塑模具设计(如开模动画)与公…...
智能座舱开发工程师面试题
一、基础知识类 简述智能座舱的核心组成部分及其功能 要求从硬件(如显示屏、传感器、控制器)和软件(操作系统、中间件、应用程序)层面展开,阐述各部分如何协同实现座舱的智能化体验。 对比 Android Automotive、QNX…...
【连载14】基础智能体的进展与挑战综述-多智能体系统设计
基础智能体的进展与挑战综述 从类脑智能到具备可进化性、协作性和安全性的系统 【翻译团队】刘军(liujunbupt.edu.cn) 钱雨欣玥 冯梓哲 李正博 李冠谕 朱宇晗 张霄天 孙大壮 黄若溪 在基于大语言模型的多智能体系统(LLM-MAS)中,合作目标和合…...
06.three官方示例+编辑器+AI快速学习webgl_animation_skinning_additive_blending
本实例主要讲解内容 这个Three.js示例展示了**骨骼动画(Skinning)和变形动画(Morphing)**的结合应用。通过加载一个机器人模型,演示了如何同时控制角色的肢体动作和面部表情,实现更加丰富的角色动画效果。 核心技术包括: 多动画混合与淡入…...
【Java学习日记36】:javabeen学生系统
ideal快捷键...
.Net HttpClient 使用请求数据
HttpClient 使用请求数据 0、初始化及全局设置 //初始化:必须先执行一次 #!import ./ini.ipynb1、使用url 传参 参数放在Url里,形如:http://www.baidu.com?namezhangsan&age18, GET、Head请求用的比较多。优点是简单、方便࿰…...
详解 Java 并发编程 synchronized 关键字
synchronized 关键字的作用 synchronized 是 Java 中用于实现线程同步的关键字,主要用于解决多线程环境下的资源竞争问题。它可以修饰方法或代码块,确保同一时间只有一个线程可以执行被修饰的代码,从而避免数据不一致的问题。 synchronized…...
《Go小技巧易错点100例》第三十二篇
本期分享: 1.sync.Map的原理和使用方式 2.实现有序的Map sync.Map的原理和使用方式 sync.Map的底层结构是通过读写分离和无锁读设计实现高并发安全: 1)双存储结构: 包含原子化的 read(只读缓存,无锁快…...
时序约束高级进阶使用详解四:Set_False_Path
目录 一、背景 二、Set_False_Path 2.1 Set_false_path常用场景 2.2 Set_false_path的优势 2.3 Set_false_path设置项 2.4 细节区分 三、工程示例 3.1 工程代码 3.2 时序约束如下 3.3 时序报告 3.4 常规场景 3.4.1 设计代码 3.4.2 约束场景 3.4.3 约束对象总结…...
每日定投40刀BTC(16)20250428 - 20250511
定投 坚持 《恒道》 长河九曲本微流,岱岳摩云起累丘。 铁杵十年销作刃,寒窗五鼓淬成钩。已谙蜀栈盘空险,更蓄湘竹带泪遒。 莫问枯荣何日证,星霜满鬓亦从头。...
C# 高效处理海量数据:解决嵌套并行的性能陷阱
C# 高效处理海量数据:解决嵌套并行的性能陷阱 问题场景 假设我们需要在 10万条ID 和 1万个目录路径 中,快速找到所有满足以下条件的路径: 路径本身包含ID字符串该路径的子目录中也包含同名ID 初始代码采用Parallel.ForEach嵌套Task.Run&am…...
【Java EE初阶 --- 多线程(初阶)】线程安全问题
乐观学习,乐观生活,才能不断前进啊!!! 我的主页:optimistic_chen 我的专栏:c语言 ,Java 欢迎大家访问~ 创作不易,大佬们点赞鼓励下吧~ 文章目录 线程不安全的原因根本原因…...
从InfluxDB到StarRocks:Grab实现Spark监控平台10倍性能提升
Grab 是东南亚领先的超级应用,业务涵盖外卖配送、出行服务和数字金融,覆盖东南亚八个国家的 800 多个城市,每天为数百万用户提供一站式服务,包括点餐、购物、寄送包裹、打车、在线支付等。 为了优化 Spark 监控性能,Gr…...
《Redis应用实例》学习笔记,第一章:缓存文本数据
前言 最近在学习《Redis应用实例》,这本书并没有讲任何底层,而是聚焦实战用法,梳理了 32 种 Redis 的常见用法。我的笔记在 Github 上,用 Jupyter 记录,会有更好的阅读体验,作者的源码在这里:h…...
Redis 缓存
缓存介绍 Redis 最主要三个用途: 1)存储数据(内存数据库) 2)消息队列 3)缓存 对于硬件的访问速度,通常有以下情况: CPU 寄存器 > 内存 > 硬盘 > 网络 缓存的核心…...
Apache Flink 与 Flink CDC:概念、联系、区别及版本演进解析
Apache Flink 与 Flink CDC:概念、联系、区别及版本演进解析 在实时数据处理和流式计算领域,Apache Flink 已成为行业标杆。而 Flink CDC(Change Data Capture) 作为其生态中的重要组件,为数据库的实时变更捕获提供了强大的能力。 本文将从以下几个方面进行深入讲解: 什…...
缓存(4):常见缓存 概念、问题、现象 及 预防问题
常见缓存概念 缓存特征: 命中率、最大元素、清空策略 命中率:命中率返回正确结果数/请求缓存次数 它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。 最大元素(最大空间):缓存中可以存放的最大元素的…...
实战项目6(09)
目录 任务场景一 【r1配置】 【r2配置】 【r3配置】 任务场景二 【r1配置】 【r2配置】 【r3配置】 任务场景三 【r1配置】 【r2配置】 【r3配置】 任务场景一 按照下图完成网络拓扑搭建和配置 任务要求:在…...
MySQL 数据库故障排查指南
MySQL 数据库故障排查指南 本指南旨在帮助您识别和解决常见的 MySQL 数据库故障。我们将从问题识别开始,逐步深入到具体的故障类型和排查步骤。 1. 问题识别与信息收集 在开始排查之前,首先需要清晰地了解问题的现象和范围。 故障现象: 数…...
MacOS Python3安装
python一般在Mac上会自带,但是大多都是python2。 python2和python3并不存在上下版本兼容的情况,所以python2和python3可以同时安装在一台设备上,并且python3的一些语法和python2并不互通。 所以在Mac电脑上即使有自带python,想要使…...
锁相放大技术:从噪声中提取微弱信号的利器
锁相放大技术:从噪声中提取微弱信号的利器 一、什么是锁相放大? 锁相放大(Lock-in Amplification)是一种用于检测微弱信号的技术,它能够从强噪声背景中提取出我们感兴趣的特定信号。想象一下在嘈杂的派对上听清某个人…...
机器学习总结
1.BN【batch normalization】 https://zhuanlan.zhihu.com/p/93643523 减少 2.L1L2正则化 l1:稀疏 l2:权重减小 3.泛化误差 训练误差计算了训练集的误差,而泛化误差是计算全集的误差。 4.dropout 训练过程中神经元p的概率失活 一文彻底搞懂深度学习&#x…...
基于神经网络的无源雷达测向系统仿真实现
基于神经网络的无源雷达测向系统仿真实现 项目概述 本项目实现了基于卷积神经网络(CNN)的无源雷达方向到达角(DOA)估计系统。通过深度学习方法,系统能够从接收到的雷达信号中准确估计出信号源的方向,适用于单目标和多目标场景。相比传统的DOA估计算法&…...
《用MATLAB玩转游戏开发》Flappy Bird:小鸟飞行大战MATLAB趣味实现
《用MATLAB玩转游戏开发:从零开始打造你的数字乐园》基础篇(2D图形交互)-Flappy Bird:小鸟飞行大战MATLAB趣味实现 文章目录 《用MATLAB玩转游戏开发:从零开始打造你的数字乐园》基础篇(2D图形交互…...
【C/C++】跟我一起学_C++同步机制效率对比与优化策略
文章目录 C同步机制效率对比与优化策略1 效率对比2 核心同步机制详解与适用场景3 性能优化建议4 场景对比表5 总结 C同步机制效率对比与优化策略 多线程编程中,同步机制的选择直接影响程序性能与资源利用率。 主流同步方式: 互斥锁原子操作读写锁条件变量无锁数据…...
linux 三剑客命令学习
grep Grep 是一个命令行工具,用于在文本文件中搜索打印匹配指定模式的行。它的名称来自于 “Global Regular Expression Print”(全局正则表达式打印),它最初是由 Unix 系统上的一种工具实现的。Grep 工具在 Linux 和其他类 Unix…...
【js基础笔记] - 包含es6 类的使用
文章目录 js基础js 预解析js变量提升 DOM相关知识节点选择器获取属性节点创建节点插入节点替换节点克隆节点获取节点属性获取元素尺寸获取元素偏移量标准的dom事件流阻止事件传播阻止默认行为事件委托 正则表达式js复杂类型元字符 - 基本元字符元字符 - 边界符元字符 - 限定符元…...
《Linux命令行大全(第2版)》PDF下载
内容简介 本书对Linux命令行进行详细的介绍,全书内容包括4个部分,第一部分由Shell的介绍开启命令行基础知识的学习之旅;第二部分讲述配置文件的编辑,如何通过命令行控制计算机;第三部分探讨常见的任务与必备工具&…...
补补表面粗糙度的相关知识(一)
表面粗糙度,或简称粗糙度,是指表面不光滑的特性。这个在机械加工行业内可以说是绝绝的必备知识之一,但往往也是最容易被忽略的,因为往往天天接触的反而不怎么关心,或者没有真正的去认真学习掌握。对于像我一样…...
Python实用工具:pdf转doc
该工具只能使用在英文目录下,且无法转换出图片,以及文本特殊格式。 下载依赖项 pip install PyPDF2 升级依赖项 pip install PyPDF2 --upgrade 查看库版本 python -c "import PyPDF2; print(PyPDF2.__version__)" 下载第二个依赖项 pip i…...
基于Dify实现对Excel的数据分析
在dify部署完成后,大家就可以基于此进行各种应用场景建设,目前dify支持聊天助手(包括对话工作流)、工作流、agent等模式的场景建设,我们在日常工作中经常会遇到各种各样的数据清洗、格式转换处理、数据统计成图等数据分…...
Win全兼容!五五 Excel Word 转 PDF 工具解决多场景转换难题
各位办公小能手们!今天给你们介绍一款超牛的工具——五五Excel Word批量转PDF工具V5.5版。这玩意儿专注搞批量格式转换,能把Excel(.xls/.xlsx)和Word(.doc/.docx)文档唰唰地变成PDF格式。 先说说它的核心功…...
java加强 -Collection集合
集合是一种容器,类似于数组,但集合的大小可变,开发中也非常常用。Collection代表单列集合,每个元素(数据)只包含1个值。Collection集合分为两类,List集合与set集合。 特点 List系列集合&#…...
BGP实验练习1
需求: 要求五台路由器的环回地址均可以相互访问 需求分析: 1.图中存在五个路由器 AR1、AR2、AR3、AR4、AR5,分属不同自治系统(AS),AR1 在 AS 100,AR2 - AR4 在 AS 200,AR5 在 AS …...
Nginx location静态文件映射配置
遇到问题? 以下这个Nginx的配置,愿意为访问https://abc.com会指向一个动态网站,访问https://abc.com/tongsongzj时会访问静态网站,但是配置之后(注意看后面那个location /tongsongzj/静态文件映射的配置)&…...
四、Hive DDL表定义、数据类型、SerDe 与分隔符核心
在理解了 Hive 数据库的基本操作后,本篇笔记将深入到数据存储的核心单元——表 (Table) 的定义和管理。掌握如何创建表、选择合适的数据类型、以及配置数据的读写方式 (特别是 SerDe 和分隔符),是高效使用 Hive 的关键。 一、创建表 (CREATE TABLE)&…...
每日脚本 5.11 - 进制转换和ascii字符
前置知识 python中各个进制的开头 二进制 : 0b 八进制 : 0o 十六进制 : 0x 进制转换函数 : bin() 转为2进制 oct() 转换为八进制的函数 hex() 转换为16进制的函数 ascii码和字符之间的转换 : chr(97) 码转为字符 …...
cookie和session的区别
一、基本概念 1. Cookie 定义:Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据(通常小于4KB),浏览器会在后续请求中自动携带该数据。作用:用于跟踪用户状态(如登录状态)、记…...
Kotlin Multiplatform--03:项目实战
Kotlin Multiplatform--03:项目实战 引言配置iOS开发环境配置项目环境运行程序 引言 本章将会带领读者进行项目实战,了解如何从零开始编译一个能同时在Android和iOS运行的App。开发环境一般来说需要使用Macbook,笔者没试过Windows是否能开发。…...
图形学、人机交互、VR/AR领域文献速读【持续更新中...】
(1)笔者在时间有限的情况下,想要多积累一些自身课题之外的新文献、新知识,所以开了这一篇文章。 (2)想通过将文献喂给大模型,并向大模型提问的方式来快速理解文献的重要信息(如基础i…...
opencascade.js stp vite 调试笔记
Hello, World! | OpenCascade.js cnpm install opencascade.js cnpm install vite-plugin-wasm --save-dev 当你不知道文件写哪的时候trae还是有点用的 ‘’‘ import { defineConfig } from vite; import wasm from vite-plugin-wasm; import rollupWasm from rollup/plugi…...
openharmony系统移植之gpu mesa3d适配
openharmony系统移植之gpu mesa3d适配 文章目录 openharmony系统移植之gpu mesa3d适配1. 环境说明2. gpu内核panfrost驱动2.1 使能panfrost驱动2.2 panfrost dts配置 3. buildroot下测试gpu驱动3.1 buildroot配置编译 4. ohos下mesa3d适配4.1 ohos下mesa3d编译调试4.1.2 编译4.…...
Java开发经验——阿里巴巴编码规范经验总结2
摘要 这篇文章是关于Java开发中阿里巴巴编码规范的经验总结。它强调了避免使用Apache BeanUtils进行属性复制,因为它效率低下且类型转换不安全。推荐使用Spring BeanUtils、Hutool BeanUtil、MapStruct或手动赋值等替代方案。文章还指出不应在视图模板中加入复杂逻…...
Linux中常见开发工具简单介绍
目录 apt/yum 介绍 常用命令 install remove list vim 介绍 常用模式 命令模式 插入模式 批量操作 底行模式 模式替换图 vim的配置文件 gcc/g 介绍 处理过程 预处理 编译 汇编 链接 库 静态库 动态库(共享库) make/Makefile …...