ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大模型训练loss spike全解析:成因、排查与解决

大模型训练loss spike全解析:成因、排查与解决 训练大模型最怕什么不是训得慢而是眼看着loss曲线稳得不行突然某一个step直接拉出一条陡峭的尖峰。我在一次7B参数模型的中期训练里就撞上过loss从2.1一路降到1.6附近结果step 13500时突然一个spike飙到9.2之后三个epoch都缓不过来最后只能回滚checkpoint重新来。这种问题在LLM训练中非常典型几乎每个大模型训练团队都会遇到。这篇文章我会把loss spike的真正成因、排查思路、解决策略以及面试时这个题该怎么答一次性讲透。不管你是正在做大模型训练的工程师还是在准备LLM相关的面试这篇文章都值得看完。核心就一句话loss spike不可怕可怕的是你不知道它为什么出现也不知道下一步该干什么。1. 先搞清楚loss spike到底是什么为什么值得单独开一篇1.1 现象界定正常的抖动 vs 真正的spike先说清楚一个概念。很多新手把loss曲线上的任何波动都叫spike这是不对的会导致误判和瞎调参。正常的训练中loss本身会有小范围抖动尤其是batch size不大、数据分布比较杂的时候每个batch的loss天然会有零点几的波动。这属于正常现象不需要任何处理。真正的spike要满足两个特征幅度异常大往往是正常水平的3倍以上比如一个训练中稳定在1.5~2.0的loss突然变成8.0甚至几十持续时间短通常只有一个或少数几个step但也可能连锁反应导致loss持续高位。另一个容易混淆的情况是eval loss和train loss之间的差异。有时候train loss正常但eval loss在某个节点突然涨上去这不叫spike这通常是过拟合的征兆或者是训练和推理时数据分布不一致比如dropout、padding策略不同。真正的training loss spike指的是训练过程中loss本身突然离群。我之前遇到过一个小规模模型训练步与步之间的loss波动从0.3到0.6不等肉眼看着曲线也毛糙但整体趋势是下降的。后来有个同事非要去调学习率结果反而把收敛搞崩了。所以第一步是确认这到底算不算一个需要处理的spike只有满足异常幅度大、位置不可解释这两个条件才值得投入精力去查。1.2 spike的危害链不只是数字变丑这么简单loss spike本身只是一个现象它真正可怕的是后续连锁反应。首先是权重破坏。如果你的优化器是Adam这种带状态的spike意味着这一step的梯度异常大参数会朝着一个错误方向迈出一大步。虽然Adam会对梯度做标准化但当spike足够大时参数更新量仍然可能远超正常水平模型权重被“顶”到一个坏区域后续训练很难自己走出来。其次是后续step的连锁放大。大模型训练普遍使用learning rate scheduler很多是cosine或warmup后衰减。如果spike出现在lr还很高的时候错误更新会被放大如果spike出现在lr已经衰减到很低的阶段模型可能需要极长时间才能恢复。更麻烦的是EMA指数移动平均版本的模型如果被污染整个评估结果全废。再算一笔账。现在训练一个7B模型假设用了64张A100每张卡算力成本大概2美元一小时按主流云厂商区间估一个step如果32秒spike导致你回滚500个step就意味着大约17分钟×64卡的算力白白浪费。遇到严重情况回滚上千步再加上反复实验排查的时间单次事故成本几千美元是很正常的。所以这不是一个小问题值得认真对待。2. 剖析根因loss spike背后的五大“嫌疑人”2.1 数据侧问题脏数据、重复样本、超长序列数据问题是我在实际排查中遇到频率最高的“元凶”之一尤其是大模型预训练阶段的数据内容丰富但也泥沙俱下。最典型的情况是标签错误。对于有监督的微调场景一条样本query, response里response完全答非所问或者标注让模型去预测一个和输入无关的目标这个样本的loss天然就比其他样本高几个数量级。在预训练场景中某些网页抓取片段包含大量乱码、超长重复字符或无法打印的控制符号模型在这些位置的预测难度极高loss被瞬间拉高。另一个隐蔽的情况是“超长序列截断”导致的spike。大模型预训练通常设置固定的seq_len比如2048或4096长文档会被截断或拼接。如果某两条被强行拼接的样本在边界处语义断层严重模型在这个位置上其实是在做无意义的预测loss异常高。这类样本的比例如果不加控制积累到一定程度单个batch里只要出现一两条就足以让loss曲线上冒尖。我个人的经验是遇到spike后优先从这个角度排查。具体做法后面会讲但排序上数据侧问题永远应该排在前面——因为它不需要看懂复杂优化器原理排查成本最低命中率却很高。2.2 优化器与学习率Adam参数没配好埋雷的典型方式数据没问题那就看优化器。在大模型训练中AdamW是绝对的主流选择。但AdamW有几个参数是需要认真对待的learning rate、beta1、beta2、epsilon、weight_decay。其中任何一个设置不当都可能成为spike的温床。最常见的组合拳是“warmup阶段没问题、lr峰值期崩了”。Warmup的设计思路是让优化器的状态一阶矩、二阶矩估计先稳定建立起来再逐步提升到目标lr。如果warmup步数设置过短比如本来应该几千步结果只配了200步lr冲得太猛优化器一阶矩还没稳定遇到一个相对大的梯度就容易失控出现spike。另一个被很多人忽略的是epsilon。Adam的epsilon是加到分母上防止除零的理论上只要比0足够小就行。但实践中如果你用的是混合精度训练epsilon设成1e-8这种极小的值在低精度下会导致数值稳定性下降偶尔出现极大的更新量。我们后来在多个训练任务里把epsilon从1e-8提高到1e-6spike频率明显下降。PS这里顺带说一句我不建议一上来就调整beta1、beta2这些参数除非你非常清楚自己在做什么。beta2从0.999改成0.95会显著改变优化轨迹很容易引入新的不稳定因素。2.3 混合精度与数值稳定性fp16的loss scale失控是重灾区大模型一旦上规模几乎必然要用混合精度训练。而fp16混合精度恰恰是loss spike的高发区。原理上fp16的表示范围有限最大值约65504比fp32小太多。为了防止梯度下溢underflow训练框架会用一个动态loss scaler如果检测到梯度过小就把loss乘上一个放大系数比如从1.0逐步放大到1024甚至更高再反向传播最后在更新前把梯度除以这个系数。这个机制整体设计得不错但它的动态调整逻辑依赖“最近若干步内是否出现inf/nan”来判断是否调整scale。当某个step的梯度突然出现inf无限大时scaler会立即跳回一个较小的值这一瞬间优化器看到的是异常放大的梯度参数更新被严重污染loss随即spike。还有一个很反直觉的情况按理说fp16溢出应该表现为loss变成NaN或inf但我实际见过的很多spike并不是NaN而是正常的数字只是很大。原因是溢出后的梯度在部分参数上变成了inf但由于模型存在残差连接、LayerNorm等结构inf经过归一化后有可能被“拉回”到一个有限值于是loss从NaN变成了一个极大的数值看起来就是一个有限但异常的spike。如果你只在日志里过滤NaN和inf很可能漏掉这种情况。相比之下bf16因为动态范围更大和fp32一致溢出问题会好很多这也是为什么现在大规模预训练基本都切到bf16的原因。如果你的spike经常出现且和loss scale相关优先考虑把混合精度模式从fp16切到bf16。2.4 分布式训练层面通信异常和数据shuffle不一致当你的训练从单机单卡扩展到多机多卡时新的变量又来了。数据并行场景下每个rank进程持有不同的数据分片每个step算完各自的梯度然后通过AllReduce把梯度同步、求平均再做参数更新。这个链路里有两个典型的spike触发点。第一个是通信异常。NCCL在节点间通信时如果网络出现抖动、超时甚至重新建立连接可能导致某个rank的梯度没有完整参与聚合其他rank得到的是一个“残缺”的平均梯度方向和幅度都异常随即出现spike。这种spike往往伴随通信日志中的警告如NCCL timeout、sock connect failed等。第二个是数据shuffle策略不一致。如果你的训练脚本在恢复checkpoint重训时没有正确恢复RNG随机数生成器状态或者数据分片用的是线程局部seed但没有做到全局一致不同rank拿到的数据不再对齐。结果就是一个batch里的模型看到的数据在另一个batch里被重复或遗漏梯度同步时出现N对不上、数据不配对的情况loss也可能异常。这类问题在单机上很难复现因为单机的NCCL走的是共享内存速度快、稳定性高问题不暴露。我建议做分布式排查时首先确认“单卡/单机能不能复现”这个关键实验——如果单机不崩、多机才崩那大概率就是通信或数据并行一致性的问题。2.5 模型与初始化attention logit 饱和、放大因子失效最后说模型结构层面的问题。相比前几个原因这类问题一旦存在通常从训练早期就会开始间歇性出现spike而不是训练到中途才突然发生。最典型的场景是Attention层中QK^T的值过大导致softmax饱和。在标准的Transformer中attention logit通常要除以sqrt(d_k)做缩放如果这一步被实现错误、或者后续加了额外的scale参数但初始化不当logit的绝对值会偏大softmax输出趋于one-hot反向传播时梯度会不稳定进而导致loss突然升高。另一种是DeepNorm/残差连接的比例问题。GPT类模型在堆叠很多层时每一层的残差更新如果scale太大深层网络容易在某个step激活值爆炸。这类问题在模型规模或层数改变后容易暴露。这类和模型结构强相关的问题有一个特点通常会在小规模试跑时出现但被忽略或者是在更换backbone、调整上下文长度后突然出现。排查时建议关注“是否换了结构之后才开始出现spike”这个时间节点。3. 拿到spike之后五步定位法快速锁定真凶3.1 第一步记录spike发生的step回溯训练日志遇到spike第一件事不是改代码而是尽量留下“案发现场”。我的习惯是训练脚本里全程打印至少每10步记录一次step、loss、lr、grad norm和loss scale这样spike出现后能立刻回溯。多数情况下训练框架都会周期性保存checkpoint结合日志文件你要做的第一步是确认spike发生在哪个step确认spike前后的lr数值确认spike前后的grad norm变化确认spike前后loss scale是否发生跳变确认spike是否同时出现在所有rank还是只有某一个rank。如果你在训练前没有记录这些指标那这次spike大概率只能哑巴吃黄连。这也是为什么我强烈建议所有训练脚本内置grad norm和loss scale的日志输出。一段简单的Python脚本可以帮你快速画出这些指标的变化import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(train_log.csv) spike_step df.loc[df[loss].idxmax(), step] fig, axes plt.subplots(2, 2, figsize(14, 8)) axes[0, 0].plot(df[step], df[loss]) axes[0, 0].axvline(spike_step, colorred, linestyle--) axes[0, 0].set_title(loss) axes[0, 1].plot(df[step], df[lr]) axes[0, 1].axvline(spike_step, colorred, linestyle--) axes[0, 1].set_title(lr) axes[1, 0].plot(df[step], df[grad_norm]) axes[1, 0].axvline(spike_step, colorred, linestyle--) axes[1, 0].set_title(grad_norm) axes[1, 1].plot(df[step], df[loss_scale]) axes[1, 1].axvline(spike_step, colorred, linestyle--) axes[1, 1].set_title(loss_scale) plt.tight_layout() plt.show()画完这张图很多问题已经能看出来了。比如grad_norm先爆那大概率是梯度爆炸loss_scale骤降那大概率是fp16溢出grad_norm正常但loss涨了那大概率是数据样本问题。3.2 第二步交叉验证梯度范数、loss scale与lr判断异常类型画完曲线后可以按照下表做一次快速分类观察到的现象可能原因下一步动作grad_norm在spike前暴涨梯度爆炸检查lr是否过高、是否缺梯度裁剪loss_scale在spike前骤降fp16溢出检查混合精度配置考虑bf16grad_norm正常但loss单步跳高数据样本问题定位触发样本lr恰好在峰值附近学习率过冲降低lr峰值、增加warmup所有rank同时spike数据分布或优化器问题检查数据shuffle与全局batch组成只有单个rank spike通信异常或该rank数据有问题检查NCCL日志与rank_id对应数据这个表格不是严格的诊断树但它能帮你快速缩小范围。我在实战中最常用的是交叉看grad_norm和loss_scale这两个指标。如果后者没有明显变化那就优先在数据层面找问题如果在spike前grad_norm已经连续多步上涨那就是优化器或lr的锅。3.3 第三步按固定seed复现锁定触发batch如果是数据样本问题下一步就是把它抓出来。做法是固定全局seed包括torch、numpy、random、cuda的seed把训练从头或从spike前几百步重新跑一遍。由于随机性被完全控制理论上同一位置会再次出现spike。在这个复现过程中将每个batch的样本ID和对应的loss都记录下来找到触发spike的那个batch。找到之后把这个batch单独拿出来人工检查里面是否包含异常样本比如重复字符、超长序列、乱码文本、错误标签等。识别出问题样本后有两种处理方式直接从训练数据中删除如果你担心删除后会破坏数据分布可以把这个样本的loss权重调低或者在采样器里限制它被重复采到的次数。清理掉问题样本后接着从spike前的checkpoint重新开始训练。这个操作我在多个任务里验证过基本都能把loss曲线恢复到正常形态。3.4 第四步做对照实验逐项排除变量如果前三步都没定位到明确原因那就得做对照实验了。严格控制变量逐个开关影响因子固定seed关闭混合精度纯fp32跑500步看spike是否还出现固定seed将lr降为原来的1/10跑500步看spike是否还出现固定seed过滤掉数据中最长的那1%样本跑500步看spike是否还出现固定seed去掉梯度裁剪跑500步看spike是否还出现。每组实验只改一个变量用同样的评估指标对比。实测下来80%以上的spike都能在四组对照实验里被复现或排除。剩下那20%属于多因素耦合比如lr处于峰值 某些异常样本 bf16精度损失恰好叠加这类问题定位成本很高但通常通过组合方案降低lr 清洗数据就能解决不必一定找到单一“真凶”。4. 落地方案出现spike后怎么止损怎么防复发4.1 立刻止损回滚checkpoint还是继续硬扛遇到spike你的第一反应应该是判断能不能救回来。判断标准很简单看接下来100~200步内loss有没有回落到正常区间。如果回落到接近spike前的水平那说明模型还没被彻底污染可以选择继续训练如果loss持续在高位震荡或者后面又出现第二次spike那就别犹豫直接回滚到spike前最近的checkpoint。一个重要原则回滚后不要直接以原lr继续跑。要在你spike前lr的基础上乘以一个折扣系数我常用0.1~0.3。等loss稳定下降一段时间后再通过lr scheduler慢慢升回去。否则同样的配置很容易在原位置再次踩雷。4.2 优化器与学习率策略把warmup和峰值调“软”一点在预防侧我建议优先检查以下四个参数warmup步数占训练总步数的比例。大模型预训练建议至少1%~2%比如10万步的训练warmup至少1000~2000步lr峰值。如果你用的是cosine衰减峰值lr建议参考论文或同规模模型的经验值不要一味加大7B模型常见峰值在1e-4到3e-4之间Adam的epsilon。强烈建议从1e-8改成1e-6这个改动成本极低但对spike的抑制效果明显是否开启梯度裁剪。几乎所有大模型训练框架都建议开启阈值常用1.0或模型grad norm均值的三四倍。注意用global norm裁剪而不是per-parameter norm裁剪否则多参数模型下裁剪效果会被稀释。如果你用的是HuggingFace Transformers Trainer可以在TrainingArguments里直接配置from transformers import TrainingArguments training_args TrainingArguments( output_dir./checkpoints, learning_rate2e-4, warmup_ratio0.02, adam_epsilon1e-6, max_grad_norm1.0, fp16True, # 或 bf16True logging_steps10, save_steps500, )如果你用的是DeepSpeed则可以在deepspeed config里加{ optimizer: { type: AdamW, params: { lr: 2e-4, eps: 1e-6, weight_decay: 0.1 } }, gradient_clipping: { enabled: true, max_grad_norm: 1.0 }, fp16: { enabled: true, loss_scale: 0, initial_scale_power: 16 } }其中initial_scale_power设成16意味着初始loss scale为2^1665536这对于多数中大规模模型是个相对安全的起点。4.3 数据治理从源头降低spike概率如果你的训练数据来源比较复杂尤其是从互联网抓取的海量文本建议在预处理阶段加上这几道工序过滤异常字符控制符号、乱码、无法解码的字节序列直接剔除重复文本检测超长重复的句子比“aaaaa”这种更隐蔽的是整段文本重复做一道MinHash或SimHash去重长度过滤对超出预设最大长度的样本不要简单暴力截断最好按语义边界比如段落、句子切断减少截断位置的语义断裂质量打分清洗用困惑度或分类器给样本质量打分去掉低分样本。此外如果你用的是预训练超大数据集我建议训练过程中写一个“异常样本检测器”定期计算每条样本的loss如果某类样本持续贡献异常高的loss把它列出来人工审查一次。这比事后回溯成本更低。4.4 混合精度与数值稳定性该换bf16就换bf16前面已经说了fp16的loss scale机制是spike高发原因之一。如果你有条件比如A100、H100、Ascend或4090等支持bf16的硬件强烈建议使用bf16混合精度。它没有动态scale机制表示范围同fp32从原理上规避了loss scale跳变这一类spike。但要注意bf16精度低在梯度极小的场景下可能丢失信息。我的经验是如果模型中小梯度占比很高可以考虑在关键模块比如embedding层、最后的分类头保持fp32计算。很多框架已经提供了“bf16 fp32 master weight”的混合方案可以优先试这个。还有一个偏保守的方案是打开gradient checkpointing。它通过重计算来省显存会带来大约20%~30%的额外计算开销但好处是batch size可以开得更大模型在每个step看到的样本方差变小训练稳定性反而提升。算力不是特别紧张的话这个方法也值得考虑。4.5 自动告警与checkpoint策略不靠人盯靠机制人肉监控loss曲线时间长了必出纰漏。更可靠的方式是写一个自动检测脚本实时读取训练日志当loss值超过“滚动均值的N倍标准差”或“绝对值超过设定阈值”时自动发告警同时自动执行回滚。我实践过的一个简单检测逻辑是这样的import numpy as np def is_spike(loss_history, threshold_ratio3.0, min_steps20): if len(loss_history) min_steps 1: return False recent loss_history[-min_steps:] mean np.mean(recent) std np.std(recent) if std 1e-8: return False current loss_history[-1] return current mean threshold_ratio * std配合shell任务在训练脚本外层加一个循环每30秒检查一次日志一旦判定spike就自动杀掉训练进程并调用命令回滚到指定checkpoint同时发送告警。这套机制不复杂但能替你省下几小时的夜间值班时间。关于checkpoint策略也很简单保存频率不能太疏。大模型checkpoint体积大保存可能耗时但spike回滚需要你有一个“足够近”的检查点。我通常的做法是每500~1000步保存一次保留最近5~10份磁盘压力可接受的前提下尽量频繁一点。5. 面试视角回答“loss spike怎么办”的高分姿势5.1 面试官到底在考察什么这个题在LLM面试里出现频率很高但它并不是一个纯粹的技术填空题。面试官想通过你的回答看出三层能力第一层对训练不稳定现象是否有基本认知能不能说出两个以上可能原因第二层是否具备系统性排查的工程思维遇到问题是否知道先看什么、再看什么、最后怎么办第三层是否踩过真实训练的大坑回答里能不能带出细节比如loss scale、grad norm、NCCL通信、数据样本之类的具体术语和场景。如果你的回答只是“调低学习率”或者“加梯度裁剪”那大概率会被面试官追问“还有呢”“你怎么判断是哪种原因”“你实际遇到过吗”一旦被追到死角回答就会露怯。5.2 推荐回答框架从现象到止损到排查再到预防根据我的带人经验给出一套比较完整的回答逻辑第一步先定义现象区分正常波动和真正的spike体现你对训练曲线的敏感度。 第二步说止损策略回滚checkpoint、降低lr、在下一个spike出现时自动告警。 第三步按概率排序讲原因数据问题、优化器/lr、混合精度、分布式通信、模型结构这五个方向各说一两句关键判断方法不需要全展开。 第四步讲排查手段最核心的两个观测指标是grad norm和loss scale再配合固定seed复现、对照组实验定位根因。 第五步讲预防机制数据清洗、warmup与lr峰值设置、bf16替代fp16、梯度裁剪、自动检测与告警。按照这个顺序答基本能覆盖面试官想听的全部要点。这里有一个加分项主动说出“我实际遇到过一次最后的结论是……”哪怕只是你做过的小实验也比空谈公式和理论强得多。5.3 加分项口头复盘模板展示真实工程经验如果你需要一个口头复盘模板可以这样组织“我之前训练一个7B模型的时候loss在13000步左右出现过一次spike。当时我的第一反应是看日志里的grad norm发现它先涨了好几倍说明是梯度问题而不是数据样本问题。接着我检查了当时的lr和epoch位置发现正好处于cosine schedule的峰值附近。进一步排查发现这个batch里有一个很长的异常样本属于低质量网页对loss的贡献是其他样本的好几倍。最后我做的处理是回滚checkpoint、把lr峰值降低了一块、清洗了这批次数据后续的spike频率就大幅下降了。”这类话术听起来不复杂但它包含了完整的“现象→观测→判断→行动→结果”面试官很容易从中判断出你真的做过训练而不是只背过概念。5.4 容易踩的坑别在这些地方翻车不建议只说“调小lr”。面试官如果问“为什么调小lr有效”你需要讲清楚lr影响参数更新幅度的机制不建议把fp16和bf16混为一谈。要能解释bf16为什么更稳定动态范围大不需要loss scaling不建议说“spike后直接继续训练就行”。你需要立刻给出判断条件说明什么情况下能继续、什么情况下必须回滚不建议把梯度裁剪说成万能药。要知道裁剪只是把极端梯度压住如果根因是数据污染裁剪只能掩盖症状不能治本。6. 亲历复盘一次7B模型训练中的loss spike追踪6.1 事故现象一个意外出现的陡峭尖峰我当时在训练一个约7B参数的decoder-only模型数据是清洗过的中文英文混合语料训练目标是标准next token prediction。训练到约13000步时训练loss从2.1附近下降到了1.6附近曲线整体很平滑评估指标也在同步变好。结果在step 13500loss突然从1.62跳升到8.73紧接着的十几个step一直维持在4~6之间之后才缓慢回落到2附近但始终回不到之前1.6的水平。第一反应是“数据跑偏了”但因为这批数据是反复清洗过的所以又觉得不太可能。后面看日志才发现grad norm在spike前的约200步里从2.0附近缓慢爬升到4.0spike当步直接到了23.6。这就把问题明显指向了“梯度在快速积累”。6.2 排查过程对照组实验逐一证伪定位到梯度异常后我按照顺序做了几组实验第一组固定seed 低lr降为1/10重新跑。结果曲线回归平滑没有出现spike说明问题和lr过冲强相关。 第二组固定seed 原lr 过滤掉最长1%样本。spike仍然出现说明不完全是长样本单方面引发的。 第三组加梯度裁剪max_grad_norm1.0 原lr。spike出现的step推迟了几百步但最终还是出现了说明裁剪只能延缓不能根除。 第四组把Adam的epsilon从1e-8调到1e-6。spike概率明显下降但没有完全消失。四组实验下来综合判断是lr处于峰值期某些正常范围内但偏难的样本二者叠加导致梯度逐步积累最终在某一步超过临界点形成梯度爆炸式的spike。单独降lr或者单独清洗数据都不能完全解决问题但组合起来就很稳定。6.3 最终修复与效果最终我采用的方案是回滚到spike前500步的checkpoint而不是从头训练把cosine schedule的峰值lr从3e-4降到2e-4Adam的epsilon固定为1e-6开启max_grad_norm1.0的全局梯度裁剪对训练数据做了一遍困惑度过滤去掉最难的top 0.2%样本。调整后的训练继续跑了4万多步没有再次出现显著的loss spike最终收敛指标还略微优于之前。这次复盘让我对“不要试图用一个措施解决所有spike”的认知深了一层——训练稳定性是一个系统性工程不是某一项参数就能兜底的。7. 最后分享一点个人体会在训练大模型这件事上loss曲线本质上是数据、模型、优化器、分布式环境四者共同作用的“心电图”。loss spike就像一次室颤预警它不一定会毁掉整个训练但一定说明系统里某个环节不在最佳状态。我个人对spike的态度是不恐慌、先止损、再定位、后预防。这里的“不恐慌”不是不重视而是不要一看到loss拉高就盲目调参。先花10分钟看日志判断是哪一类问题比盲目改10个参数有用得多。最后再分享一个小技巧每次启动一个新训练任务前先花半天时间做一次“稳定性体检”。用一个较小的模型、较小的batch、较短的序列跑几百步观察grad norm、loss scale、lr几个指标的联动情况。如果小规模下就有异常那大规模训练前就应该先把这些雷排掉。这比训练中途去救火省时省力太多了。
返回列表