news 2026/10/1 23:08:43

昇思MindSpore大模型训练评估与性能优化实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
昇思MindSpore大模型训练评估与性能优化实践指南

在我用昇思 MindSpore 做大模型训练的一年多时间里,被问得最多的两个问题,一个是“你怎么判断训练有没有跑好”,另一个是“为什么我的训练这么慢”。评估体系和性能优化,看起来是两个方向,实际是同一件事的两面:没有可靠的量化方式,优化就是瞎调;没有清晰的瓶颈定位,所有参数都像在碰运气。这篇文章就把我在 MindSpore 上做大模型训练时,从评估指标搭建到性能优化的完整实践梳理一遍,希望对正在折腾大模型训练的你有点实际帮助。

1. 评估体系:先搞清楚“训练得好”到底怎么量化

很多同学一上来就把眼光钉死在训练 loss 上,loss 降了就觉得万事大吉。大模型训练里这个习惯特别危险,因为 loss 下降并不能说明训练效率高、资源利用合理、模型没有在悄悄退化。我习惯把评估体系分成四个维度来做,每个维度各管一件事。

1.1 四个核心评估维度

第一个维度是训练效率,核心指标是 tokens/s 和 step time。tokens/s 衡量整个训练管线每秒能处理多少 token,这是大模型训练真正意义上的“吞吐”;step time 则是每个训练步的墙钟时间,它和 tokens/s 互为表里,看到 step time 突然拉长,第一反应应该是数据或通信出问题了。

第二个维度是收敛质量,也就是大家最熟悉的 loss 和困惑度(PPL)。但我会更关注 loss 曲线的形状,而不是单点数值。正常的大模型训练 loss 曲线应该是平滑下降、偶尔小抖,如果出现周期性的尖峰,往往是学习率策略或数据顺序出了问题,需要单独排查。

第三个维度是资源利用率。我习惯同时盯 GPU/Ascend 算力利用率、显存占用峰值、通信耗时占比这三项。算力利用率低说明计算没吃饱,通信占比高说明并行策略可能需要调整,显存贴着上限跑则随时有 OOM 风险。

第四个维度是稳定性,包括梯度范数、loss 是否出现 NaN、训练进程是否反复中断。大模型训练动辄跑几周,稳定性和前面三个维度同样重要,一条异常梯度就可能毁掉整个 checkpoint。

维度核心指标观测手段
训练效率tokens/s、step time训练日志、TimeMonitor
收敛质量loss 曲线、PPLTensorBoard、定期验证
资源利用算力利用率、显存、通信占比MindSpore Profiler、npu-smi/nvidia-smi
稳定性梯度范数、NaN、中断次数回调函数、日志告警

1.2 比 loss 更值得长期盯的 4 个指标

第一个是有效吞吐。只看 tokens/s 还不够,如果 tokens/s 很高但 loss 不降,说明数据被反复喂了没意义的重复序列,有效信息量其实很低。我会额外统计“每个 step 的 loss 下降幅度”,并且用这个幅度去反推数据质量。

第二个是梯度范数。梯度范数突然飙升,往往是学习率过大或数据中出现异常样本的征兆。我在训练脚本里加了一个自定义 Callback,每个 step 结束后把梯度范数打进日志,一旦发现梯度范数超过设定阈值,马上触发梯度裁剪或者降低学习率,而不是等 loss 飞了再手忙脚乱去救。

第三个是 loss 的 EMA 曲线。原始 loss 曲线噪声太大,直接看会让人焦虑。我做了一个滑动平均版本的 loss 曲线,每 50 步记录一次 EMA 值,用它来判断真正的收敛趋势,噪声就被过滤掉了。

第四个是显存占用曲线。大模型训练最怕的就是训练到一半突然 OOM。我每隔固定 step 记录一次显存峰值,并且观察显存是否随着训练时长缓慢增长,如果出现缓慢增长,基本可以断定存在显存碎片泄漏,需要检查 checkpoint 保存逻辑或者动态图算子缓存。

1.3 评估结果如何反哺训练决策

评估体系建好之后,不是拿来看的,是用来做决策的。我在实际训练中经常遇到这样的情况:有效吞吐连续 500 步没有提升,于是把 batch size 调大一倍,loss 曲线立刻有了明显下降。另一次是梯度范数频繁报警,我把 warmup 步数从 1000 拉到 2000,loss 的尖峰立刻减少。

验证集的 PPL 同样关键。我习惯每 1000 个 step 在验证集上算一次 PPL,如果训练 loss 在降、但验证 PPL 在升,就说明模型开始过拟合了,这时候要么增加数据多样性,要么调整正则化策略。把评估结果和训练超参形成闭环,这才叫真正的评估体系,而不是训练结束后才补一张测试报告。

2. 性能画像:动手优化前先定位瓶颈

性能优化最大的误区是一上来就调并行策略、换优化器,结果发现瓶颈根本不在那里。我做性能优化的第一步永远是先做性能画像,把整个训练过程拆开来看,时间到底花在哪里。

2.1 MindSpore Profiler 的正确打开方式

MindSpore 自带 Profiler 工具,可以在训练过程中采集算子耗时、数据处理耗时、通信耗时等关键数据。我这里以 GPU 环境为例,Ascend 环境的方式类似,只是底层采集的数据项名称略有差异。

import mindspore as ms from mindspore import Profiler ms.set_context(mode=ms.GRAPH_MODE, device_target='GPU') profiler = Profiler(output_path='./profiler_data') # 这里是正常的训练流程,跑几十个 step 后停止 # ... # 训练结束时调用 analyse 生成分析结果 profiler.analyse()

我使用 Profiler 的经验是:不要从头到尾开着它,因为它本身有性能开销,会影响采集数据的真实性。我通常只在完成一次稳定的训练启动后,额外跑 30~50 个 step 做性能采集,采集完立刻关闭,然后把profiler_data目录下的分析结果导出来看。

这个工具输出的核心信息有三块:一是各算子的平均耗时和调用次数,二是数据管线各环节的耗时占比,三是通信算子的耗时分布。拿到这三块数据,基本可以判断训练慢的原因到底在哪一层。

2.2 四类常见瓶颈的判断清单

把 Profiler 数据拉出来之后,我习惯按下面这张表去对号入座。这里的场景很典型:训练慢,但慢的原因千差万别。

表现可能原因优化方向
数据队列经常为空、GPU 算力利用率低数据加载太慢,预处理阻塞加大 num_parallel_workers、使用数据缓存、精简预处理逻辑
单个算子耗时特别突出计算瓶颈,或算子实现不高效混合精度、算子融合、替换等价高效算子
AllReduce 等通信算子占比高并行策略通信开销过大梯度融合、通信掩蔽、优化并行切分策略
显存峰值接近上限且偶尔 OOM显存容量瓶颈重计算、优化器状态切分、梯度累积、减小 batch

这里我想特别强调一个容易被忽略的现象:数据加载瓶颈。很多人用 MindSpore 时只注意了num_parallel_workers的大小,却忘了shuffle和map的顺序,导致数据预处理反复执行。数据管线的优化往往不花一分钱,却能带来 10% 到 30% 的训练提速,性价比极高。

2.3 基线数据记录是提速的第一步

每次做优化之前,我都会先记录一份完整的基线数据,包括 batch size、step time、tokens/s、算力利用率、显存峰值、通信占比。为什么这样做?因为性能优化是一个迭代过程,你改了一个参数,效果是好是坏,不能靠感觉,必须和基线对比。

我自己的基线记录表大概长这样:

项目数值
模型规模7B
并行策略数据并行 8 卡
batch size per device4
step time3.2s
tokens/s约 4200
算力利用率68%
显存峰值38.2GB / 40GB
通信耗时占比11%

记录完之后,每次只改一个变量,跑 50 个 step 再看数据。不要同时改 batch size 和并行策略,否则出了问题你根本不知道是哪个改动造成的。这条经验我踩过很多次坑,后来老老实实遵守,效率反而高了。

3. 大模型训练优化的五个关键动作

定位到瓶颈之后,接下来就是动手优化。我把自己在 MindSpore 上实践过、并且验证有效的优化动作按性价比排序,整理成五个关键动作。

3.1 数据管线:并行度和顺序都要对

数据管线是第一优先级,因为它最容易改、效果最直接。MindSpore 的GeneratorDataset和mindspore.dataset内置算子都支持多线程并行,我用的是类似下面的配置:

import mindspore as ms from mindspore import dataset as ds # source 可以是自定义生成器,也可以从文件读取 data = ds.GeneratorDataset(source, column_names=["input_ids", "attention_mask"]) data = data.shuffle(buffer_size=10000) data = data.map(operations=tokenize_op, num_parallel_workers=8) data = data.batch(batch_size=4, drop_remainder=True)

这里有一个我踩过的坑:map里的operations如果包含 Python 自定义函数,多线程并行度提得太高反而会引发 GIL 竞争,导致性能下降。所以我通常先把num_parallel_workers从 4 开始试,用 Profiler 观察数据队列空置率,逐步加到 8、16,找到最合适的值。

另外一个容易出问题的地方是shuffle和map的顺序。如果map里的操作很重,先shuffle再map会让每个线程对同一批数据反复做预处理;反过来先map再shuffle能利用缓存减少预处理次数。对于大模型这种数据量大、预处理逻辑复杂的场景,我基本都采用“先 map 后 shuffle”的顺序。

3.2 混合精度:最划算的提速手段

混合精度几乎是所有大模型训练的标配。MindSpore 里开启混合精度最直接的方式是在构造Model时指定amp_level,也可以用auto_mixed_precision接口对网络做转换。我常用的写法是这样的:

from mindspore import Model from mindspore import amp # 方式一:在 Model 中指定 model = Model(network, loss_fn=loss, optimizer=optimizer, amp_level="O2") # 方式二:先转换网络再训练 network = amp.auto_mixed_precision(network, amp_level="O2")

为什么要用 O2 而不是 O0 或 O1?O0 是全精度,基本没有提速效果;O1 是部分算子转半精度,速度提升有限;O2 是尽量多地把算子转成 FP16,只保留一批必须用 FP32 的算子,在大模型场景下收益最明显。

但 O2 不是万能的。我之前在一个 LLaMA 结构的模型上直接套 O2,结果训练中期 loss 开始震荡,排查了半天才发现是 LayerNorm 被转成了 FP16,精度不够导致数值不稳定。后来我把 LayerNorm 和最后的 Softmax 相关部分手动保留在 FP32,问题立刻消失。在 MindSpore 里可以通过回调或者自定义混合精度策略实现这一点,核心思路就一句话:哪些算子必须守住 FP32,要心里有数。

3.3 梯度累积与显存受限下的微批量策略

显存不够的时候,梯度累积是比强行减小 batch size 更聪明的方案。梯度累积的意思是:先把多个微批次的梯度算出来,累积到一起,再统一更新一次参数。它能在不增大显存占用的情况下,模拟更大的有效 batch size。

在 MindSpore 里配置梯度累积,我不同版本用过不同方式。以我目前常用的版本为例,可以在Model中指定gradient_accumulation_steps,也可以在优化器中设置累积步数:

# 有效 batch size = 4 * 8 = 32 model = Model(network, loss_fn=loss, optimizer=optimizer, amp_level="O2", gradient_accumulation_steps=8)

使用梯度累积要注意的是:BatchNorm 类算子在累积模式下统计分布会有偏差。好在大多数大模型结构里没有 BatchNorm,基本是 LayerNorm 或 RMSNorm,所以影响不大。另外梯度累积会让参数更新频率变低,收敛轨迹会有些变化,需要同步调整学习率,通常我会在累积步数变大时略微提高学习率。

微批量策略还有一个容易被忽视的细节:微批次大小不是越小越好。每个微批次太小,算子计算效率会断崖式下降,因为固定的 kernel 启动开销摊薄不过去。我一般建议微批次大小不要小于 1,也不要小于模型并行切分后的最小计算单元要求。具体多少合适,还得靠基线对比。

3.4 图模式与算子融合

MindSpore 有 PY Native 和 Graph 两种执行模式。PY Native 模式调试方便,但算子反复调度、Python 开销大;Graph 模式下,MindSpore 会把整个计算图做编译优化,算子调度、内存复用、融合都更好。跑大模型训练,我基本只用图模式:

ms.set_context(mode=ms.GRAPH_MODE, device_target="GPU")

图模式带来的提速非常明显,尤其是模型结构复杂、算子多的场景,提速幅度能到 20% 到 40%。但图模式也有代价:第一次编译要花不少时间,改动网络结构后还会触发重新编译。为了解决这个问题,我开启了编译缓存:

ms.set_context(enable_compile_cache=True)

编译缓存能把编译产物保存下来,第二次运行同样的网络结构时,直接加载缓存,省掉一大段编译时间。我在一个 7B 模型上,首次编译要花 90 多秒,开缓存之后第二次启动只花不到 20 秒,体验完全不一样。

算子融合这块,MindSpore 在 Graph 模式下会自动做一些融合,比如把连续的矩阵乘和激活函数融合成一个算子。但自动融合并不总是最优,我在实际项目里会手动把一些频繁调用的小算子组合成复合算子,减少 kernel 启动次数。比如把Linear + GELU写成一个自定义 Cell,既方便复用,也方便框架做融合优化。

3.5 分布式并行策略:从数据并行到流水线并行

分布式并行是大模型训练绕不开的话题。很多人一听模型有 10B 参数,就觉得必须上模型并行。其实先把数据并行做到极致,往往能解决大部分问题。

数据并行是最简单的并行策略,每张卡持有完整模型副本,只同步梯度。MindSpore 里配置数据并行非常直接:

ms.set_auto_parallel_context(parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True)

数据并行的主要开销在通信:每步都要做梯度 AllReduce。通信优化的关键是梯度融合,把大量小的梯度张量合并成几个大的梯度张量再通信,能明显降低通信次数。MindSpore 的优化器里通常有这个优化开关,或通过配置项控制,开启后通信耗时占比能从 15% 降到 8% 左右。

当模型大到单卡装不下的时候,就必须考虑算子级并行或流水线并行。我的经验是:先看显存瓶颈出在参数、梯度还是优化器状态,再决定切分策略。比如参数 7B、FP16 存储,光参数就要 14GB,再加上梯度和优化器状态,4 张卡未必够。这时候可以把 Embedding、Attention 的权重矩阵做行列切分,实现算子级并行。流水分线并行则是把网络按层切成多个 stage,每个 stage 放在不同设备上,设备间通过 pipeline 方式传递中间激活值,适合层数特别深的模型。这两种并行方式在 MindSpore 里都有相应配置,但复杂度比数据并行高不少,建议先在数据并行上把性能榨干,再考虑升级。

4. 实战案例:7B 模型在 8 卡环境上的优化全程

理论讲再多,不如看一个完整的优化过程。这里我用自己的一个 7B 规模 GPT 风格模型为例,训练环境是 8 张 40GB 显卡,任务是在约 300B token 的中文语料上做继续预训练。

4.1 初始状态与基线数据

这个模型刚搭建起来的时候,配置是数据并行 8 卡,每卡 batch size 4,FP32 全精度,PY Native 模式已经换成了 Graph 模式。初始跑起来的数据让我非常不满意:

指标初始值
step time3.2s
tokens/s约 4200
算力利用率68%
显存峰值38.2GB / 40GB
通信耗时占比11%
loss(1000 步后)2.18

显存离上限只剩 1.8GB,算力利用率只有 68%,说明 GPU 有大把时间在等待,不是计算打满的状态。先用 Profiler 做了 40 个 step 的性能画像,发现数据队列偶发空置,同时通信占比有 11%,整体优化空间集中在数据、精度和通信三个方向。

4.2 优化步骤与效果对比

我按“先改便宜的后改贵的”原则,分步做了五轮优化,每一轮都只改一个变量。

第一轮优化数据管线。把num_parallel_workers从 4 调到 8,同时调整了shuffle和map的顺序,让预处理结果可以被缓存复用。跑 50 个 step 后,数据队列空置率明显下降,step time 从 3.2s 微降到 3.1s。效果不大,但说明数据管线已经不是主要瓶颈了,这一步主要是排除干扰项。

第二轮开混合精度 O2。把model的amp_level改成 "O2",同时对 LayerNorm 做了 FP32 保护。这一步效果极其显著,step time 从 3.1s 直接降到 2.2s,tokens/s 提升到 6000 左右。算力利用率也升到了 78%。混合精度果然是大模型训练最划算的提速手段。

第三轮优化通信。开启梯度融合,把大量小的梯度合并通信,并配置了通信掩蔽,也就是在当前微批次计算的时候,同时进行上一步的梯度通信,让通信和计算重叠。step time 从 2.2s 降到 1.8s,通信占比从 11% 降到 6%。

第四轮调整梯度累积。我之前是纯数据并行,没有用梯度累积。为了进一步模拟更大的有效 batch,我用gradient_accumulation_steps=4,同时把每卡 batch size 微调为 2,这样有效 batch size 不变(2 × 4 × 8 = 64),但显存峰值降下来了,还剩下不少余量。这轮优化后显存占用从 38.2GB 降到 31.5GB。

第五轮把省下来的显存换成更大的 batch。每卡 batch size 从 2 调回 4,同时保持梯度累积 4 步,有效 batch size 达到 128。虽然梯度累积数变了之后单 step 微批次计算量加大,但吞吐整体仍然在涨,最终 step time 稳定在 1.9s 左右,tokens/s 达到约 7800。

五轮优化后的最终数据对比:

指标优化前优化后
step time3.2s1.9s
tokens/s约 4200约 7800
算力利用率68%85%
显存峰值38.2GB / 40GB35.6GB / 40GB
通信耗时占比11%6%
loss(1000 步后)2.182.21

loss 数值有轻微差异是因为有效 batch size 变大、优化轨迹变了,并不是模型变差,看趋势到 2000 步时已经追上并反超了。整个优化过程大概花了两天,换来的是接近 86% 的吞吐提升,这个投入产出比我觉得非常值。

4.3 关键代码梳理

把优化后训练脚本里的几个关键片段整理出来,方便你对照自己的配置。训练入口的 context 配置:

import mindspore as ms from mindspore import Model ms.set_context(mode=ms.GRAPH_MODE, device_target="GPU") ms.set_context(enable_compile_cache=True) ms.set_auto_parallel_context(parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True)

训练时用到的自定义 Callback,用来记录 step time 和梯度范数:

import time from mindspore.train import Callback class TrainMonitor(Callback): def __init__(self, log_every=50): super().__init__() self.log_every = log_every self.step_start_time = None def step_begin(self, run_context): self.step_start_time = time.time() def step_end(self, run_context): if self.step_start_time is None: return cost = time.time() - self.step_start_time cb_params = run_context.original_args() cur_step = cb_params.cur_step_num loss = cb_params.net_outputs if cur_step % self.log_every == 0: print(f"step {cur_step}, loss {loss:.4f}, " f"time {cost:.3f}s, tokens {tokens_per_step / cost:.0f}/s")

其中tokens_per_step是每步处理的 token 总数,在实际代码里根据 batch size 和序列长度提前算好即可。

验证集 PPL 的计算,我单独写了一个脚本,用一个简单的函数完成核心逻辑:

import mindspore as ms from mindspore import ops def compute_ppl(logits, labels): # logits: [batch, seq_len, vocab] # labels: [batch, seq_len] shift_logits = logits[:, :-1, :] shift_labels = labels[:, 1:] ce_loss = ops.cross_entropy( shift_logits.reshape(-1, shift_logits.shape[-1]), shift_labels.reshape(-1) ) return ops.exp(ce_loss.mean()).asnumpy().item()

PPL 我一般每 1000 步算一次,和训练 loss 放在一起看,避免出现训得越快越偏的情况。

5. 常见问题与排查技巧实录

在实际训练过程中,我遇到过不少让人抓狂的诡异问题。这里整理成速查表,再分享几个独家的避坑经验。

5.1 高频问题速查表

问题现象可能原因排查方向
loss 不降,甚至缓慢上升学习率过大、数据顺序异常、混合精度数值不稳看梯度范数曲线,确认是否频繁触发裁剪,检查 FP32 保护算子
step time 周期性飙升数据队列周期性空置,或 checkpoint 保存与训练重叠用 Profiler 看数据加载耗时变化,把 checkpoint 保存移到异步线程
GPU 利用率只有 50% 左右数据加载慢、通信等待、算子串行先看 Profiler 的 AI Core 空闲率,再分别查数据和通信
训练中途 OOM显存碎片、batch 过大、激活值缓存过多减小 batch、开启重计算、优化器切分、检查是否有张量泄漏
多卡训练 loss 不一致数据并行中每个卡数据分布不均,或梯度同步失效检查每个 step 的 loss 打印是否来自 rank 0,必须统一从主卡日志观察
图模式编译慢编译缓存未开启或网络结构频繁改动开启 enable_compile_cache,适当冻结不常变化的子图
梯度范数突然爆掉学习率步长跨度过大、数据异常样本设置梯度裁剪,同时把学习率 warmup 步数拉长

5.2 三个独家避坑经验

第一个经验:不要盲目加大 batch size。吞吐并不总是随着 batch 增大而线性上升,当 batch 大到一定程度,算子计算效率进入平台期,反而可能因为显存压力导致重计算频率上升,吞吐掉头向下。我每次调整 batch 后都会用 Profiler 看一次算子效率,只有算力利用率同步提升,这个调整才真正有效。

第二个经验:多卡训练时,日志输出和 loss 监控必须做收敛。在多卡环境下,每个 rank 都会打印自己的 loss,这些数值在数据并行下通常会接近,但不完全一样。如果打印顺序混乱,很容易误判为训练不稳定。我的做法是在自定义 Callback 里判断当前是不是 rank 0,只在主卡打印统计信息,其他 rank 全部静默。

第三个经验:性能优化一定要留足“回滚点”。每完成一轮有效优化,我都会保存一份当时的训练脚本和超参配置,标注好当时的 step time 和显存数据。这样如果后续优化反而让性能下降,可以快速回滚到上一个稳定状态,而不是靠记忆重写配置。这个习惯帮我节省了大量试错时间。

最后聊一点我自己的体会:评估体系和性能优化这套东西,单独拿出来看都很简单,难的是把它们结合到日常训练流程里,形成稳定的习惯。每改动一个参数前先问自己“我要解决什么问题,如何量化效果”,这比任何优化技巧都重要。希望这篇实践梳理能帮你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 23:07:41

基于SVM的中文文本分类实战:垃圾短信识别与TF-IDF特征工程

简介:基于SVM的中文文本分类项目,以垃圾短信识别为例,面向自然语言处理初学者与需要快速搭建文本分类基线的开发者。压缩包内含可直接运行的训练脚本、已训练好的支持向量机模型与TF-IDF向量化模型,并提供带标签的短信训练集和测试…

作者头像 李华
网站建设 2026/10/1 23:04:31

RWA赛道全景拆解:从真实世界资产代币化到项目评估实战指南

开场:当我听完一场 RWA 主题活动,看到的不仅是热度上周我以从业者身份参加了 Coinstore B.KU 主办的数字金融与 RWA 主题活动。说实话,去之前我对这类活动没抱太高期望——过去两年"真实世界资产代币化"几乎成了行业标配口号&#…

作者头像 李华
网站建设 2026/10/1 23:04:31

正则化本质:从数学惩罚到认知边界与物理约束

1. 正则化不是“加点惩罚”就完事:它本质是模型认知边界的主动划界“正则化”这三个字,在《深度学习》教材里往往被压缩成两页纸:L1/L2公式、Dropout示意图、早停流程图。我带过六届AI方向的实习生,几乎所有人第一次调参时都把正则…

作者头像 李华
网站建设 2026/10/1 23:04:27

C++ unordered_map底层原理与性能调优:哈希表、迭代器失效与冲突排查

写 C 写了几年之后,我发现不少人对std::unordered_map/unordered_set这套unordered_xxx容器的理解,一直停在一个很舒服但也很危险的结论上:底层是哈希表,所以增删查都是 O(1)。真到线上出现性能抖动、迭代器崩溃、遍历顺序“莫名其…

作者头像 李华
网站建设 2026/10/1 23:04:05

SSM框架+Java+MySQL:汽车租赁管理系统毕设完整开发路线

带了三届毕业生做毕设,每年临到5月都能看到同一种表情:代码跑起来了,但论文一个字没写。今年大概率也不会例外,尤其是选“汽车租赁管理系统”这类经典题目的同学——SSM Java MySQL,题目看着不难,真要动手…

作者头像 李华
网站建设 2026/10/1 23:03:10

Spring Boot 3.x接口文档实战:springdoc-openapi替代Springfox全解析

Spring Boot 3.x刚出来那阵子,几乎所有从2.x升上来的老团队都撞上过同一堵墙:以前项目里那个开箱即用的Swagger UI页面,升级后一访问就是空白页或者直接404。换了Springfox的新版本也不行,因为Springfox的维护基本停在了Spring Bo…

作者头像 李华