【CUDA】Branch Divergence and Unrolling Loop

目录

一、避免分支发散

1.1 并行规约问题

1.2 并行规约中的发散

二、UNrolling Loops


一、避免分支发散

控制流有时依赖于 thread 索引。同一个warp中,一个条件分支可能导致性能很差。通过重新组织数据获取模式可以减少或避免 warp divergence。具体问题查看下面这篇文章:

【CUDA】Warp解析-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/GG_Bruse/article/details/143772619

1.1 并行规约问题

若要计算一个数组N个元素的和,使用CPU编程实现十分容易

int sum = 0;
for (int i = 0; i < N; ++i)sum += array[i]

若数组中的元素非常多,应用并行计算可以大大提高效率。鉴于加法交换律等性质,这个求和过程可以以元素的任意顺序来进行:

  • 将输入数组切割成很多小的块
  • 用 thread 来计算每个块的和
  • 对这些块的结果再求和得最终结果

数组的切割主旨是:用 thread 求数组中按一定规律配对的的两个元素和,然后将所有结果组合成一个新的数组,然后再次求配对两元素和,多次迭代,直到数组中只有一个结果

比较直观的两种实现方式是:

  1. Neighbored pair:每次迭代都是相邻两个元素求和
  2. Interleaved pair:按一定跨度配对两个元素

对于N个元素的数组,该过程需N - 1次求和、log_{2}N步。InterleavedPair的跨度是半个数组长度

上述讲的这类问题术语为 reduction problem。Parallel reduction(并行规约)是指迭代减少操作,是并行算法中非常关键的一种操作

1.2 并行规约中的发散

Neighbored pair

在这个kernel中,有两个global memory array,一个用来存放数组所有数据,另一个用来存放部分和。所有 block 独立的执行求和操作

#include <iostream>
#include <cuda_runtime.h>
#include <cuda_runtime_api.h>
using namespace std;// 1 << 28 == 268435456
__global__ 
void reduceNeighbored(int* inputData, int *outputData, unsigned int N) 
{unsigned int threadIndex = threadIdx.x; // 0 - 511unsigned int index = blockIdx.x * blockDim.x + threadIdx.x; // 0 - 524287, 512, 0 - 511 即 0 - 268435455int* iData = inputData + blockIdx.x * blockDim.x; // inputData + (0 - 268434944) 即指向每个block的起始位置if (index >= N) return;// stride = (1 - 511) 1, 2, 4, ..., 500for (int stride = 1; stride < blockDim.x; stride *= 2) {if ((threadIndex % (2 * stride)) == 0) // 每次取threadIndex为偶数的threadiData[threadIndex] += iData[threadIndex + stride];__syncthreads();}if (threadIndex == 0) outputData[blockIdx.x] = iData[0]; // 放入计算出的数据 
}void GPU_ReduceNeighbored(int* dInputData, int* dOutputData, int size, dim3 grid, dim3 block)
{reduceNeighbored<<<grid, block>>>(dInputData, dOutputData, size);
}

因为无法让所有的block同步,所以最后将所有block的结果送回 host 进行串行计算

主函数代码:

void GPU_ReduceNeighbored(int* inputData, int *outputData, int N, dim3 grid, dim3 block);long long Seconds()
{// 获取当前时间点auto now = std::chrono::high_resolution_clock::now();// 将时间点转换为以毫秒为单位的时间间隔auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());// 获取毫秒数return duration.count();
}int main(int argc, char** argv)
{// set up deviceint device = 0;cudaDeviceProp deviceProp;cudaGetDeviceProperties(&deviceProp, device);printf("%s starting reduction at device %d : %s\n", argv[0], device, deviceProp.name);cudaSetDevice(device);// initialization int size = 1 << 28;printf("Array Size : %d\n", size);int blockSize = 512;dim3 block(blockSize, 1);dim3 grid((size + block.x - 1) / block.x, 1);printf("grid %d , block %d\n", grid.x, block.x);// allocate host memorysize_t bytes = size * sizeof(int);int* hInputData = (int*) malloc(bytes);int* hOutputData = (int*) malloc(grid.x * sizeof(int));int* tmp = (int*) malloc(bytes);for(int i = 0; i < size; ++i) hInputData[i] = (int)(rand() & 0xFF); // 屏蔽最大两个字节memcpy(tmp, hInputData, bytes);// allocate device memoryint* dInputData = nullptr;int* dOutputData = nullptr;cudaMalloc((void **) &dInputData, bytes);cudaMalloc((void **) &dOutputData, grid.x * sizeof(int));int iStart, iElaps;// kernel: reduceNeighboredcudaMemcpy(dInputData, hInputData, bytes, cudaMemcpyHostToDevice);cudaDeviceSynchronize();iStart = Seconds();GPU_ReduceNeighbored(dInputData, dOutputData, size, grid, block);cudaDeviceSynchronize();iElaps = Seconds() - iStart;cudaMemcpy(hOutputData, dOutputData, grid.x * sizeof(int), cudaMemcpyDeviceToHost);long long gpuSum = 0;for (int i = 0; i < grid.x; i++) gpuSum += hOutputData[i];printf("gpu Neighbored elapsed %d ms gpuSum: %lld <<<grid %d block %d>>>\n", iElaps, gpuSum, grid.x, block.x);// free host memoryfree(hInputData);free(hOutputData);// free device memorycudaFree(dInputData);cudaFree(dOutputData);// reset devicecudaDeviceReset();return 0;
}

if ((tid % (2 * stride)) == 0) 该表达式只对偶数ID的线程为true,所以其导致很高的 divergent warps。第一次迭代只有偶数ID的线程执行了指令,但是所有线程都要被调度;第二次迭代,只有四分之一的 thread 是 active 的,但所有 thread 都要被调度。可以重新组织每个线程对应的数组索引来强制ID相邻的thread来处理求和操作

__global__ 
void reduceNeighboredLess(int* inputData, int* outputData, unsigned int N) 
{unsigned int threadIndex = threadIdx.x; // 0 - 511unsigned int index = blockIdx.x * blockDim.x + threadIdx.x; // 0 - 524287, 512, 0 - 511 即 0 - 268435455int* iData = inputData + blockIdx.x * blockDim.x; // inputData + (0 - 268434944) 即指向每个block的起始位置if(index >= N) return;// stride = (1 - 511) 1, 2, 4, ..., 500for (int stride = 1; stride < blockDim.x; stride *= 2) {int idx = 2 * stride * threadIndex; // 2 * (1 - 500) * (0 - 511)if (idx < blockDim.x) iData[idx] += iData[idx + stride];__syncthreads();  }if (threadIndex == 0) outputData[blockIdx.x] = iData[0];
}void GPU_ReduceNeighboredLess(int* dInputData, int* dOutputData, int size, dim3 grid, dim3 block)
{reduceNeighboredLess<<<grid, block>>>(dInputData, dOutputData, size);
}

int index = 2 * stride * tid; 因为步调乘以了2,下面的语句使用block的前半部分thread来执行求和:if (index < blockDim.x)

对于一个有512个 thread 的 block 而言,前八个 warp 执行第一轮 reduction,剩下八个 warp 什么也不干;第二轮,前四个 warp 执行,剩下十二个什么也不干。就不存在divergence了(divergence只发生于同一个warp)。但最后还是会有divergence,因为这个时候需要执行 threads 已经凑不够一个 warp 了

Interleaved pair

 Interleaved Pair模式的初始步调是block大小的一半,每个thread处理相隔半个block的两个数据

__global__ 
void reduceInterleaved(int *inputData, int *outputData, unsigned int N) 
{unsigned int threadIndex = threadIdx.x;unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;int *iData = inputData + blockIdx.x * blockDim.x;if(index >= N) return;for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {if (threadIndex < stride)iData[threadIndex] += iData[threadIndex + stride];__syncthreads();}if (threadIndex == 0) outputData[blockIdx.x] = iData[0];
}void GPU_ReduceInterleaved(int* dInputData, int* dOutputData, int size, dim3 grid, dim3 block)
{reduceInterleaved<<<grid, block>>>(dInputData, dOutputData, size);
}

步调被初始化为block大小的一半:for (int stride = blockDim.x / 2; stride > 0; stride >>= 1)

下面的语句使得第一次迭代时,block的前半部分thread执行相加操作,第二次是前四分之一,以此类推:if (tid < stride)

二、UNrolling Loops

loop unrolling(取消循环)是用来优化循环减少分支的方法,该方法就是把本应在多次 loop 中完成的操作,尽量压缩到一次 loop。循环体展开程度称为loop unrolling factor(循环展开因子),loop unrolling对顺序数组的循环操作性能有很大影响

for (int i = 0; i < 100; ++i) {a[i] = b[i] + c[i];
}

如下重复一次循环体操作,迭代数目将减少一半:

for (int i = 0; i < 100; i += 2) {a[i] = b[i] + c[i];a[i+1] = b[i+1] + c[i+1];
}   

从高级语言层面是无法看出性能提升的原因的,需从 low-level instruction 层面去分析,第二段代码循环次数减少了一半,而循环体两句语句的读写操作的执行在CPU上是可以同时执行互相独立的,所以相对第一段,第二段性能要好

Unrolling 在CUDA编程中意义更重。目标是通过减少指令执行消耗,增加更多的独立指令来提高性能。这样就会增加更多的并行操作从而产生更高的指令和内存带宽(bandwidth)。也就提供了更多的 eligible warps 来帮助 hide instruction / memory latency

在前文的 reduceInterleaved 中,每个block处理一部分数据,将这些数据称为 dataBlock

下面的代码是 reduceInterleaved 的修正版本,每个 block 都是以两个 dataBlock 作为源数据进行操作(前文中,每个 block 处理一个 dataBlock)。这是一种循环分区:每个 thread 作用于多个 dataBlock,并且从每个 dataBlock 中取出一个元素处理

__global__ 
void reduceUnrolling(int *inputData, int *outputData, unsigned int N) 
{unsigned int threadIndex = threadIdx.x;unsigned int index = blockIdx.x * blockDim.x * 2 + threadIdx.x;int *iData = inputData + blockIdx.x * blockDim.x * 2;// unrolling 2 data blocksif (index + blockDim.x < N) inputData[index] += inputData[index + blockDim.x];__syncthreads();for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {if (threadIndex < stride) iData[threadIndex] += iData[threadIndex + stride];__syncthreads();}if (threadIndex == 0) outputData[blockIdx.x] = iData[0];
}  void GPU_ReduceUnrolling(int* dInputData, int* dOutputData, int size, dim3 grid, dim3 block)
{reduceUnrolling<<<grid.x / 2, block>>>(dInputData, dOutputData, size);
}

注意下面的语句,每个 thread 从相邻的 dataBlock中取数据,这一步实际上就是将两个 dataBlock规约成一个:if (idx + blockDim.x < n) g_idata[idx] += g_idata[idx+blockDim.x];

global array index 也要相应的调整,因为,相对之前的版本,同样的数据,只需要原来一半的 thread 就能解决问题。要注意的是,这样做也会降低warp或block的并行性(因为thread减少):

由于每个block处理两个data block,所以需要调整grid的配置:

reduceUnrolling2<<<grid.x / 2, block>>>(d_idata, d_odata, size);

同一个 thread 中若能有更多的独立的 load/store 操作,会产生更好的性能,因为这样做 memory latency 能够更好的被隐藏

__syncthreads是用来同步block内部thread的。在reduction kernel中,其被用来在每次循环中保证所有 thread 的写 global memory 的操作都已完成,这样才进行下一阶段的计算

当kernel进行到只需要少于或等32个thread(即一个warp)呢?由于使用的SIMT模式,warp内的thread 是有一个隐式的同步过程的。最后几次迭代可以用下面的语句展开:

if (tid < 32) {volatile int *vmem = idata;vmem[tid] += vmem[tid + 32];vmem[tid] += vmem[tid + 16];vmem[tid] += vmem[tid + 8];vmem[tid] += vmem[tid + 4];vmem[tid] += vmem[tid + 2];vmem[tid] += vmem[tid + 1];
}

warp unrolling避免了__syncthreads同步操作,因为这一步本身就没必要

注意volatile修饰符,其告诉编译器每次执行赋值时必须将 vmem[tid] 的值 store 回 global memory。若不这样,编译器或 cache 可能会优化读写global/shared memory。有了这个修饰符,编译器就会认为这个值会被其他 thread 修改,从而使得每次读写都直接去 memory 而不是去 cache 或者 register

__global__ void reduceUnrollWarps8(int *inputData, int *outputData, unsigned int N) 
{unsigned int threadIndex = threadIdx.x;unsigned int index = blockIdx.x * blockDim.x * 8 + threadIdx.x;int *iData = inputData + blockIdx.x * blockDim.x * 8;// unrolling 8if (index + 7*blockDim.x < N) {int a1 = inputData[index];int a2 = inputData[index + blockDim.x];int a3 = inputData[index + 2 * blockDim.x];int a4 = inputData[index + 3 * blockDim.x];int b1 = inputData[index + 4 * blockDim.x];int b2 = inputData[index + 5 * blockDim.x];int b3 = inputData[index + 6 * blockDim.x];int b4 = inputData[index + 7 * blockDim.x];inputData[index] = a1 + a2 + a3 + a4 + b1 + b2 + b3 + b4;}__syncthreads();for (int stride = blockDim.x / 2; stride > 32; stride >>= 1) {if (threadIndex < stride) iData[threadIndex] += iData[threadIndex + stride];__syncthreads();}// unrolling warpif (threadIndex < 32) {volatile int *vmem = iData;vmem[threadIndex] += vmem[threadIndex + 32];vmem[threadIndex] += vmem[threadIndex + 16];vmem[threadIndex] += vmem[threadIndex + 8];vmem[threadIndex] += vmem[threadIndex + 4];vmem[threadIndex] += vmem[threadIndex + 2];vmem[threadIndex] += vmem[threadIndex + 1];}if (threadIndex == 0) outputData[blockIdx.x] = iData[0];
}           void GPU_ReduceUnrolling8(int* dInputData, int* dOutputData, int size, dim3 grid, dim3 block)
{reduceUnrollWarps8<<<grid.x / 8, block>>>(dInputData, dOutputData, size);
}

若在编译时已知了迭代次数,就可以完全把循环展开。Fermi 和 Kepler 每个 block 的最大 thread 数目都是1024,文中的 kernel 的迭代次数都是基于 blockDim 的,所以完全展开循环是可行的

__global__ 
void reduceCompleteUnrollWarps8(int *inputData, int *outputData, unsigned int N) 
{unsigned int threadIndex = threadIdx.x;unsigned int index = blockIdx.x * blockDim.x * 8 + threadIdx.x;int *iData = inputData + blockIdx.x * blockDim.x * 8;// unrolling 8if (index + 7 * blockDim.x < N) {int a1 = inputData[index];int a2 = inputData[index + blockDim.x];int a3 = inputData[index + 2 * blockDim.x];int a4 = inputData[index + 3 * blockDim.x];int b1 = inputData[index + 4 * blockDim.x];int b2 = inputData[index + 5 * blockDim.x];int b3 = inputData[index + 6 * blockDim.x];int b4 = inputData[index + 7 * blockDim.x];inputData[index] = a1 + a2 + a3 + a4 + b1 + b2 + b3 + b4;}__syncthreads();if (blockDim.x>=1024 && threadIndex < 512) iData[threadIndex] += iData[threadIndex + 512];__syncthreads();        if (blockDim.x>=512 && threadIndex < 256) iData[threadIndex] += iData[threadIndex + 256];__syncthreads();if (blockDim.x>=256 && threadIndex < 128) iData[threadIndex] += iData[threadIndex + 128];__syncthreads();if (blockDim.x>=128 && threadIndex < 64) iData[threadIndex] += iData[threadIndex + 64];__syncthreads();// unrolling warpif (threadIndex < 32) {volatile int *vsmem = iData;vsmem[threadIndex] += vsmem[threadIndex + 32];vsmem[threadIndex] += vsmem[threadIndex + 16];vsmem[threadIndex] += vsmem[threadIndex + 8];vsmem[threadIndex] += vsmem[threadIndex + 4];vsmem[threadIndex] += vsmem[threadIndex + 2];vsmem[threadIndex] += vsmem[threadIndex + 1];}if (threadIndex == 0) outputData[blockIdx.x] = iData[0];
}                void GPU_ReduceCompleteUnrollWarps8(int* dInputData, int* dOutputData, int size, dim3 grid, dim3 block)
{reduceCompleteUnrollWarps8<<<grid.x / 8, block>>>(dInputData, dOutputData, size);
}

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

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

相关文章

WIN系统解决小喇叭红色叉号的办法

WIN系统解决小喇叭红色叉号的办法 WIN系统提示无音频设备&#xff0c;无法播放声音&#xff0c;重装驱动无法解决 写在前面 前段时间搞了套6750GRE&#xff0c;用了两三个月&#xff0c;老是掉驱动&#xff0c;后面折腾了一下子&#xff0c;终于是不掉了。突然&#xff0c;某…

免费S3客户端工具大赏

首发地址&#xff08;欢迎大家访问&#xff09;&#xff1a;S3免费客户端工具大赏 1. S3 GUI GitHub地址&#xff1a;https://github.com/aminalaee/s3gui 简介&#xff1a;S3 GUI 是一款基于 Flutter 构建的免费开源 S3 桌面客户端&#xff0c;支持桌面、移动和网络平台。 特…

uniapp 购物弹窗组件 (微信小程序)

效果图&#xff0c;暂时只适应单规格&#xff0c;居中弹出和下方弹出&#xff0c;如需求不满足&#xff0c;请自行修改代码 &#xff08;更新于24/11/15) 居中显示效果 下方弹出效果 html <template><view class"" v-if"show":class"mod…

力扣-Mysql-1811 - 寻找面试候选人(中等)

一、题目来源 1811. 寻找面试候选人 - 力扣&#xff08;LeetCode&#xff09; 二、数据表结构 表: Contests -------------------- | Column Name | Type | -------------------- | contest_id | int | | gold_medal | int | | silver_medal | int | | bronze_medal | …

【C语言】volatile 防止编译的时候被优化

volatile 易变的 volatile是 C 和 C 中的一个类型修饰符&#xff0c;用于指示编译器该变量可能在程序之外被更改&#xff0c;因此不应对其进行优化。这在涉及硬件寄存器、信号处理或多线程编程时非常有用。 如果你做过单片机开发&#xff0c;你肯定写过这样的代码&#xff1a;…

makefile速通

makefile速通 文章目录 makefile速通1.基础显式规则隐含规则%*通配符 赋值 伪目标CFLAGS 2.函数wildcardpatsubst 3.项目实例 1.基础 显式规则 目标文件&#xff1a;依赖文件 [TAB] 指令隐含规则 % 任意* 所有通配符 符号含义$^所有依赖文件$所有目标文件$<所有依赖文…

面向服务的软件工程——巨详细讲解商务流程建模符号 (BPMN),一篇章带你入门BPMN!!!(week1)

文章目录 一、前言二、重点概念三、BPMN元素讲解流对象1.活动任务(Task)子流程(sub-process)多实例活动连接对象序列流消息流关联泳道Artifacts数据对象组(Group)事件(Events)启动事件中间事件结束事件边界事件边界事件1边界事件2小疑问?网关参考文献:一、前言 在我们…

模拟实现~简易通讯录

一.前言 今天给小伙伴们分享的是运用结构体以及指针等相关C语言知识实现一个简易版的通讯录。咱们写的通讯录的功能主要包括添加及删除联系人&#xff0c;修改联系人信息&#xff0c;显现所有联系人&#xff0c;查找已添加联系人&#xff0c;以及对联系人进行排序&#xff0c;…

0成本添加访问级监控

互联网的安全感这个概念源于阿里。顾名思义&#xff0c;让互联网的用户对于web产品能够产生足够的信任和依赖。特别是涉及到用户资金交易的站点&#xff0c;一次严重的用户资料泄露就可以彻底毁掉你的品牌。 然而当前阶段除了bat大部分互联网行业的企业对于网络安全给的重视都…

分布式系统稳定性建设-性能优化篇

分布式系统稳定性建设-性能优化篇 系统稳定性建设是系统工程的核心内容之一。以下是一些重要的方面: 架构设计: 采用模块化、松耦合的架构设计,以提高系统的可扩展性和可维护性。合理划分系统功能模块,降低单个模块的复杂度。定义清晰的接口和数据交换标准,确保各模块之间协调…

Web端高效BIM 3D可视化引擎HOOPS Communicator技术解析!

HOOPS Communicator是一款简单而强大的工业级高性能3D Web可视化开发包&#xff0c;专注于Web端工程图形渲染。采用了先进的流式加载方式&#xff0c;并支持服务端和客户端渲染&#xff0c;是可以在云端进行部署和无缝集成的新技术平台。 灵活且易于部署&#xff0c;可在以工程…

C/C++实现tcp客户端和服务端的实现(从零开始写自己的高性能服务器)

目录 tcp客户端通信流程 tcp客户端设计 1、创建通信对象 2、链接服务器 3、发送数据 / 读取数据 4、关闭通信 tcp服务端设计 1、创建通信对象 2、绑定服务器地址信息 3、设置服务器为监听模式 4、接收客户的链接请求 编写tcp客户端和服务端&#xff0c;实现双向通信…

C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程

引言 C 标准模板库&#xff08;STL&#xff09;提供了一组功能强大的容器类&#xff0c;用于存储和操作数据集合。不同的容器具有独特的特性和应用场景&#xff0c;因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C 的开发者来说&#xff0c;了解这些容…

快速上手并使用Muduo库

Muduo muduo库是基于主从reactor模型的高性能服务器&#xff08;高并发服务器&#xff09;框架 reactor模型&#xff1a;基于事件触发的模型&#xff08;基于epoll进行IP事件监控&#xff09; 主从reactor模型&#xff1a;将IO事件监控有进行进一步的层次划分 主reactor&#x…

深入解析【C++多态】:探索面向对象编程中的动态绑定与行为多样性和多态的核心概念与应用实践

&#x1f31f;个人主页&#xff1a;落叶 &#x1f31f;当前专栏: C专栏 目录 多态的概念 多态的定义及实现 实现多态还有两个必须重要条件 虚函数 虚函数的重写/覆盖 多态场景的⼀个选择题 虚函数重写的⼀些其他问题 协变(了解进行) 析构函数的重写 override 和 final关…

React Native Mac 环境搭建

下载 Mac 版Android Studio 下载 安装 JDK 环境 Flutter 项目实战-环境变量配置一 安装 Node.js 方式一 通过Node.js 官网下载 下载完成后点击安装包进行安装 安装完成

【Word】一键批量引用论文上标——将正文字体改为上标格式

【Word】一键批量引用论文上标——将正文字体改为上标格式 写在最前面Word一键批量引用论文上标技巧分享核心思路&#xff1a;Word 替换功能 通配符步骤详解1. 打开 Word 替换功能2. 输入通配符模式3. 设置替换格式为上标4. 批量替换 实际效果展示技巧扩展 &#x1f308;你好呀…

深入探索Python数据可视化:自定义颜色映射、标签与进阶技巧

目录 一、自定义颜色映射&#xff08;Cmap&#xff09; 1. 内置Cmap类型 2. 使用内置Cmap 3. 自定义Cmap 二、标签添加 1. 在散点图上添加标签 2. 在折线图上标记关键点 3. 在柱状图上添加标签 三、进阶技巧 1. 多图形布局 2. 添加图例 3. 3D数据可视化 四、总结 …

【Java SE】数据库连接池

数据库连接池是一个管理数据库连接的容器。它的主要作用是分配和管理数据库连接&#xff0c;允许应用程序重复使用现有的连接&#xff0c;而不是每次都重新建立新的连接。此外&#xff0c;连接池会释放那些空闲时间超过最大限制的连接&#xff0c;从而避免因未及时释放连接而造…

FastAPI重载不生效?解决PyCharm中Uvicorn无法重载/重载缓慢的终极方法!

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 重载缓慢 📒📝 问题概述🚨 相关原因📝 解决方案一📝 解决方案二📝 解决方案三📝 解决方案四⚓️ 相关链接 ⚓️📖 介绍 📖 在使用FastAPI开发时,reload=True 本应让你在修改代码后自动重启服务,提升开发效率…