凌晨两点,一个批量任务的两千条样本全部返回了DONE,日志里一行红字都没有,我关了终端去睡觉。第二天业务方把结果接进去跑下游,反馈说有一批产出的图是纯黑的,还有几十条文本是空的。追回去看,那批任务确实"成功"了——进程退出码是 0,日志没报错,状态接口显示已完成,唯独产物本身是废的。这件事之后,我看 GPU 平台的眼光彻底变了:不再看谁的单卡跑分高、谁的单价便宜,而是看一个很土、但极其致命的东西——批量任务的结果完整性。这篇内容讲的就是我怎么把"稳定"这件事从一句体感描述,拆成能统计、能对比、能复现的几个指标,并用同一套批量任务在四家 GPU 服务商上做了横向对照,最后形成了自己的选型和验收流程。适合手上正在跑批量推理、批量数据处理、模型微调这类任务的同行看,尤其适合那些"任务量不大但每一条结果都不能错"的场景。
1. 批量任务里最贵的成本,从来不是显卡时长
1.1 一次静默错误是怎么把整批结果废掉的
我印象最深的一次,是一个两千条的 OCR 批处理任务。任务分成 20 个批次提交,每批 100 条,跑完之后脚本逐批检查返回状态,全部是成功。问题出在下游:业务方要把识别出的字段拼进一张报表,结果发现有一列整段整段地空着。我们回头去做逐条比对,发现有 130 多条的结果是空字符串,而它们的返回状态全是成功。原因很朴素——那批任务跑到中途,某台机器上的临时目录被清了一次,进程没崩,但写出的是空文件。
这件事的真实成本是多少?GPU 时长大概几十块钱,重跑一遍也就一小时。但我们三个人排查了将近三天,业务方的报表延期,下游还基于错误数据做了一轮分析,等于全部推倒。在批量场景里,一次静默错误的代价,是它后面所有依赖这份结果的环节之和,而不是它自己那点算力费。这就是为什么我把"结果完整性"放在选型的第一位,速度快慢、单价高低都往后排。
顺带说一个反直觉的结论:批量任务的规模越大,单个平台的"平均速度"越不重要。因为批量任务的时间构成里,排队、启动、环境初始化、失败重试占的比例很高,纯计算时间往往只占一半不到。一个平台如果单卡快 20%,但每次启动要等 15 分钟、失败率高出 1%,算总账反而是慢的。
1.2 我把"稳定"拆成四个能看见的层次
"稳定"这个词太模糊,必须拆开。我在实际排查里把它分成四层,每层都有不同的表现和不同的应对方式:
| 层次 | 典型表现 | 对结果完整性的影响 | 排查难度 |
|---|---|---|---|
| 硬件层 | 显卡掉卡、显存 ECC 报错、机器重启 | 任务直接中断,通常有报错 | 低,日志明显 |
| 调度层 | 实例被回收、抢占、超时被踢 | 任务断在半路,可能无报错 | 中,需要看平台侧事件 |
| 环境层 | 镜像版本漂移、驱动不一致、依赖缺失 | 结果不一致或精度异常 | 高,跨批次才暴露 |
| 接口层 | 状态接口延迟、返回码语义模糊 | 客户端误判成功 | 高,最容易被忽略 |
这四层里,最坑的是后两层。硬件层出问题,日志会喊;调度层出问题,你至少能看到实例没了;但环境层和接口层出问题,进程活得好好的,返回码漂漂亮亮,产物悄悄坏掉。我在后面第 4 章会专门讲这几个坑的具体表现。
补充一句,这四层的划分不是学术分类,纯粹是我自己排查时的经验总结。它的价值在于:当一批任务出问题时,你可以按这个顺序快速定位到底是"机器坏了"还是"我的判断逻辑坏了"。多数人第一反应是怀疑代码,其实有相当比例的问题出在接口层——你信了那个不该信的返回码。
1.3 这四家平台是怎么选出来的
需要说明清楚:我对比的四家(下面用平台 A、B、C、D 代称)并不是什么权威榜单,而是我当时手上正好有账号、且都能跑同一类任务的四家。选择标准很实际:能按量开实例、支持自定义镜像、提供批量提交接口、有公网可拉的结果存储。这四条是跑批量任务的底线,缺一条就很难做自动化。
四家的定位差异大致是这样:平台 A 主打按量弹性,实例来得快,价格波动大,部分资源是抢占性质的;平台 B 主打开箱即用,镜像和框架版本给得很全,启动速度快;平台 C 主打长期稳定,实例不怎么动,但排队明显;平台 D 介于中间,多卡互联做得比较顺,适合需要卡间通信的任务。
我必须在开头就声明一点:下面的所有数据都来自我自己的测试窗口,样本量有限,只反映那段时间那批任务的情况,不能代表任何平台的普遍水平。我写这些数字的目的是展示一套方法,而不是给谁下结论。你自己做选型时,一定要用你自己的任务去跑一遍,因为不同任务类型对平台的敏感点完全不同。
2. 结果完整性怎么量化:四个指标和一套埋点
2.1 完成率不等于返回码为 0 的比例
这是最容易搞错的地方。绝大多数人统计完成率的方式是数一数日志里有多少个成功标记,这个数字通常在 99% 以上,看起来很美,但毫无意义。我定义的完成率是:产物通过校验的条数 ÷ 提交的总条数。注意分子是"通过校验的产物",不是"返回成功的任务"。
这两者的差距有多大?在我的测试里,某平台按返回码统计的完成率是 99.6%,按产物校验统计的完成率只有 97.4%。中间那 2.2 个百分点,就是那些"跑完了但结果不能用"的样本。这个差值我单独起了个名字叫静默错误率,它是四个指标里我认为最重要的一个。
校验怎么做?不同类型的任务校验方式不一样,我列几个我常用的:
- 图像类任务:检查文件大小是否大于一个下限、能否正常解码、像素均值是否落在合理区间(纯黑纯白直接判废)
- 文本类任务:检查输出长度是否为零、是否命中预设的异常模式、是否包含乱码字符
- 数值类任务(embedding、打分等):检查是否含 NaN 或 Inf、向量模长是否在合理范围、维度是否匹配
- 文件类任务:检查文件头、检查能否被目标解析器打开
这些校验规则都是几行代码的事,但必须先写。没有校验规则的批量任务,等于闭着眼睛跑。我现在的习惯是,任何批量任务在写主逻辑之前,先把校验函数写好,因为校验函数会反过来约束主逻辑的输出格式,逼着我把产物结构设计清楚。
2.2 静默错误率:抽样复算才是真相
光靠格式校验还不够,因为格式正确但内容错误的情况依然存在。比如图像生成了但内容是噪声,文本生成了但语义完全跑偏。这种情况只能靠抽样复算。
我的做法是:每批任务完成后,按固定比例(我一般用 5%,最少 20 条)抽样,用同一份输入在一个"参照环境"里重跑一遍,然后比对结果。所谓参照环境,就是一台长期不动的单机实例,镜像版本锁死,只用来做基准。这个环境的唯一作用就是当标尺。
比对的方式要看任务性质。确定性任务(比如固定种子的推理、纯数值计算)可以要求逐字节一致,用哈希比对就行。非确定性任务(比如带随机性的采样生成)不能要求一致,这时候我用的是分布层面的指标,比如输出的统计特征是否在合理波动范围内,或者用一个小模型做语义相似度打分,低于阈值就报警。
这里有个经验:不要指望一次性把静默错误率降到零,要接受它存在,然后把它变成可观测的。我现在对静默错误的态度是,只要它低于某个阈值(我设的是 0.5%)且能被及时发现,就可以接受;超过阈值就必须查根因。追求零错误会导致过度校验,把批量任务的速度优势全吃掉。
2.3 跨批次一致性:同一个输入跑两遍结果一样吗
这个指标是我在一个坑里总结出来的。当时有一批任务分三天跑完,每天都用了平台上的"最新推荐镜像",结果三天的结果风格有肉眼可见的差异。查下来是镜像里某个依赖库的小版本在三天内更新了,数值精度有细微变化,累积到输出上就放大了。
所以我现在一定会测:同一份输入,在不同的时间点、用平台默认配置跑两次,结果的一致率是多少。这个指标不需要跑全量,几百条样本足够。一致性差的平台,意味着你不能把跨天、跨周的结果混在一起用,这会极大限制批量任务的调度灵活性。
对确定性任务,我用结果文件的哈希做比对,一致率 = 哈希相同的条数 ÷ 总条数。对非确定性任务,这个指标没法直接算,我会退一步看"结果的可接受率"——两次运行的结果分别用质检规则过一遍,看通过率是否稳定。比如第一次通过率 96%,第二次 91%,那说明环境在漂。
2.4 断点续跑损耗:中断之后能不能干净地接上
批量任务必然会被中断,这不是"会不会"的问题,是"什么时候"的问题。所以断点续跑的能力直接影响结果完整性。我统计的损耗有两个方向:一是重复计算量,中断恢复后有多少条被重跑了;二是遗漏量,有多少条既没跑成功也没进重试队列,被悄悄跳过了。
遗漏是最可怕的,因为它不会报错。我见过的情况是:任务用了一个"已完成列表"文件做进度记录,中断时这个文件正在写入,写了一半,恢复时读到半行,导致那一批的编号判断错位,中间一段直接被当成已完成跳过。这种问题在几千条规模下可能只丢几十条,肉眼根本看不出来。
我的应对方式是双记录:主进度用任务队列的状态(服务端记录),辅记录用产物文件的实际存在性(客户端扫描)。恢复时以产物为准,重新生成待跑列表。这样即使进度文件损坏,也能通过扫描产物目录重建。代价是每次恢复要扫一遍目录,几千条文件扫一遍不到十秒,完全值得。
2.5 埋点脚本:我把校验做成了一个独立步骤
校验逻辑不该混在业务代码里,我把它拆成独立的一步,输入是产物目录和一份清单文件,输出是一份结构化报告。这样换平台、换任务都不用改校验代码。
import json, hashlib, os from pathlib import Path def check_image(path: Path) -> tuple[bool, str]: if not path.exists(): return False, "missing" size = path.stat().st_size if size < 2048: return False, f"too_small:{size}" from PIL import Image try: with Image.open(path) as im: im.verify() with Image.open(path) as im: px = list(im.convert("L").getdata()) mean = sum(px) / len(px) if mean < 2 or mean > 253: return False, f"degenerate:mean={mean:.1f}" except Exception as e: return False, f"decode_error:{type(e).__name__}" return True, "ok" def run(manifest_path: str, out_dir: str, report_path: str): manifest = [l.strip() for l in open(manifest_path) if l.strip()] report = {"total": len(manifest), "pass": 0, "fail": 0, "items": []} for key in manifest: p = Path(out_dir) / f"{key}.png" ok, reason = check_image(p) digest = "" if ok: digest = hashlib.sha256(p.read_bytes()).hexdigest()[:16] report["pass" if ok else "fail"] += 1 report["items"].append({"key": key, "ok": ok, "reason": reason, "hash": digest}) report["pass_rate"] = report["pass"] / max(report["total"], 1) json.dump(report, open(report_path, "w"), ensure_ascii=False, indent=2) print(f"pass_rate={report['pass_rate']:.4f} fail={report['fail']}")这份报告的价值在于它可存档、可比对。每一轮测试我都留一份,后面看趋势的时候直接读 JSON 就行,不用翻日志。另外注意里面的hash字段,它是跨批次一致性的基础——同一个 key 在两轮报告里的 hash 相同,说明结果是确定的。
3. 实测过程:四家平台在同样任务上的表现
3.1 测试任务是怎么设计的
为了让对比有意义,我设计了一组覆盖不同资源特征的任务,而不是只跑一种:
| 任务编号 | 任务类型 | 单条资源特征 | 规模 | 关键敏感点 |
|---|---|---|---|---|
| T1 | 图像批处理(超分) | 显存中等、单条耗时稳定 | 2000 条 | 显存溢出、产物损坏 |
| T2 | 文本批处理(结构化抽取) | 显存低、单条耗时不均 | 2000 条 | 长尾请求超时、空输出 |
| T3 | 向量化计算 | 显存低、CPU 侧开销大 | 5000 条 | 环境一致性 |
| T4 | 小规模微调 | 需要多卡通信 | 3 轮 | 通信超时、结果条数缺失 |
每个任务在每家平台上跑 3 轮,分别安排在不同的三天,时间段也错开(一轮白天、一轮夜间、一轮周末),尽量覆盖不同负载情况。每一轮都完整走一遍"提交—校验—抽样复算—记录"的流程。四个平台加起来一共跑了 48 轮,前后花了大概三周。
这里有个细节值得说:我一律用自定义镜像,不用平台提供的预置环境。原因是预置环境的版本是平台控制的,会变,而我需要的是一个可控的基准。自定义镜像虽然说准备起来麻烦一点,但它把"环境层"这个变量给固定住了,剩下的差异才能归因到调度和接口上。如果你的团队没有维护镜像的能力,那就至少把依赖版本全部锁死在配置文件里,别用浮动版本号。
3.2 四家的对照结果
下面这组数字是我这三周的实际记录,再次强调样本量有限,只代表我的测试条件:
| 指标 | 平台 A | 平台 B | 平台 C | 平台 D |
|---|---|---|---|---|
| 产物校验完成率 | 97.4% | 99.1% | 99.9% | 98.2% |
| 静默错误率 | 0.8% | 2.6% | 0.1% | 1.1% |
| 跨批次一致率 | 96.0% | 88.0% | 99.5% | 92.0% |
| 断点续跑遗漏量 | 中(约 1.2%) | 低 | 极低 | 中 |
| 平均排队等待 | 短(3 分钟内) | 短(5 分钟内) | 长(30~50 分钟) | 中(10~20 分钟) |
| 平均单轮总耗时 | 快 | 最快 | 最慢 | 中 |
把这张表翻译成人话:
平台 A的问题集中在调度层。它弹性好、来得快,但部分资源是抢占性质的,我的 T4 任务在两轮里遇到过实例中途被回收。回收本身不可怕,可怕的是回收的时候客户端没有收到明确的失败信号,任务状态停在"运行中"很久才变成失败,我一开始的脚本把这段"运行中"当成了还在跑,白等了一个多小时。另外它的静默错误率 0.8%,我抽查下来主要原因是进程被中断时正在写产物文件,写出了截断的文件。
平台 B的启动速度和整体耗时是四家里最好的,但它的跨批次一致率只有 88%,这是它最明显的问题。我排查下来,主要是它默认推荐镜像的依赖版本在我测试的三周里有过两次小更新,导致同一输入在不同批次上的结果有细微差异,累积之后哈希就对不上了。静默错误率 2.6% 也偏高,主要出现在 T2 的长尾请求上——超时之后返回了一个空结果,但状态是成功。
平台 C的数据是四家里最漂亮的,完成率 99.9%、静默错误率 0.1%,但它也是我实际用起来最难受的一个——排队太久了。T1 那种 2000 条的任务,光排队就等了将近 40 分钟,占整轮耗时的比例相当高。这就回到我第 1 章说的那个结论:单看指标它是赢家,算总账未必。它适合那种"不急但要准"的任务,比如夜间批量、周期性报表。
平台 D属于各项都在中间,多卡通信做得比较顺,T4 是四家里跑得最稳的,但它也出现过两次通信超时导致结果条数少几条的情况,而且少的这几条不报错,是我校验的时候发现的。
3.3 差异背后的四个根因
数据只是表象,我更关心为什么。把三周的排查记录翻出来,差异基本能归到四个原因上:
第一个是中断时的处理方式。平台 A 的抢占式资源在回收时,给客户端的信号不是立即的失败,而是延迟一段时间才更新状态。这就导致客户端在"以为还在跑"的窗口里做不了任何补救。相比之下平台 C 的资源不动,压根没有这个问题。这个差异跟平台好坏无关,是资源类型决定的——任何抢占式资源都有这个特征,你只能通过客户端侧的心跳检测来弥补。
第二个是镜像版本的控制权。平台 B 为了体验,默认镜像会跟着上游更新,这对做实验很方便,对跑批量是灾难。我在测试期内观察到两次依赖变动,一次是某个数值库的小版本升级,一次是默认的精度策略调整。这两次变动在单条任务上完全看不出来,只有跨批次比对才会暴露。
第三个是多卡任务的收尾逻辑。分布式任务里,只要有一个进程退出,其他进程的行为取决于框架和平台配置。我遇到的"结果少几条"基本都是这个原因:主进程认为任务完成,但某个工作进程的数据还没落盘。这个坑跟平台关系不大,跟你的代码关系更大,第 4 章细讲。
第四个是状态接口的语义。四家平台的状态接口对"完成"的定义不完全一样,有的把"进程退出"当完成,有的把"产物上传完毕"当完成。如果你按前者的语义写客户端逻辑,用在后者的平台上,就会出现"我刚拿到完成信号就去读产物,但产物还没传完"的情况。这个差异造成的错误非常隐蔽,表现为随机几条产物读取失败或者读到不完整内容。
4. 只有跑批量才会暴露的五个坑
4.1 抢占式资源的"优雅退出"其实一点都不优雅
第一个坑:抢占式实例被回收时,平台通常会给你一段很短的缓冲时间,理论上你可以在这段时间里做收尾。但实际测试下来,这个缓冲时间非常不可靠,我见过缓冲时间不到 10 秒的情况,也见过根本没有通知直接消失的情况。
我的应对方式是彻底不相信缓冲时间:把单条任务做成"短事务",单条耗时控制在 5 分钟以内,每条任务开始前先写一个"进行中"标记文件,完成后写产物再删标记。恢复的时候扫描到标记文件但没有产物的,直接重跑。这样即使被硬中断,损失也就一条。
另一个细节:心跳检测要做在客户端,不要依赖平台的状态接口。我的做法是任务进程每隔 30 秒往一个固定路径写一次时间戳,客户端监控这个时间戳,超过 3 分钟没更新就判定实例失联,主动切到备用计划。这个机制救过我好几次,比等平台通知快得多。
4.2 环境漂移:最隐蔽的一致性杀手
第二个坑:环境漂移。这个坑的特点是它不会让你今天失败,只会让你这周和下周的结果不一致。而且因为每次都"成功",你不会去查。
判断方法很简单:同一份输入,隔一周再跑一次,比对哈希。哈希不一样,环境就动了。我做测试的时候,会专门准备 50 条"标尺样本",每轮测试都跑一遍这 50 条,把哈希记下来做趋势图。这条曲线平稳,说明环境稳定;一旦跳变,立刻去查镜像和依赖。
防御措施有三条,按有效性排序:一是用自定义镜像,把镜像 ID 写进任务配置里,每次提交都校验;二是把依赖版本全部写死到最细的补丁号,别用>=和^这类浮动写法;三是在任务开始时打印关键依赖的版本号到日志里,出问题时这是唯一的证据链。
4.3 产物写一半:文件系统层面的坑
第三个坑:产物写到一半。这个在图像和文件类任务上特别常见。原因通常是进程在写文件的过程中被中断,文件创建了但没有写完,从而产生了截断的文件。这种文件大小不为零,有些甚至能通过简单的存在性检查,只有真正去解码才会失败。
解决办法是原子写入:先写到临时文件名,写完并校验通过后,再重命名为最终文件名。重命名在大多数文件系统上是原子操作,这样就不会产生"半成品"文件。配合校验脚本检查文件大小下限和解码能力,基本能过滤掉这类问题。
# 原子写入的典型做法 tmp="out/${key}.${RANDOM}.tmp" final="out/${key}.png" write_result "$tmp" || { rm -f "$tmp"; exit 1; } [ -s "$tmp" ] || { rm -f "$tmp"; exit 1; } mv "$tmp" "$final"这段逻辑简单,但收益极大。我把它固化成了任务模板里的标准步骤,所有产物都走这条路。另外[ -s "$tmp" ]这个检查别省,文件存在但大小为 0 的情况比你想的多。
4.4 多卡并行的结果拼装错位
第四个坑:多卡任务的结果拼装。这个坑只在多卡场景下出现,但它造成的错误最难发现,因为结果条数是对的、格式是对的,只是内容和样本对不上。
典型的错法是这样的:主进程把 5000 条任务按某种规则切成 4 份分给 4 张卡,每张卡处理完后按自己的顺序写结果文件,主进程收集时按文件顺序拼接。由于各卡的完成顺序不确定,如果代码里用了"谁先完成谁先追加"的逻辑,输出顺序就和输入顺序错位了。你如果按行号去对应输入,会得到一堆看似正常、实则是别的样本的结果。
防御方式很简单:结果里必须自带主键,不要靠顺序对应。每条结果都带上输入的唯一 ID,拼装时按 ID 索引而不是按行号。同时加一步校验:拼装后的结果集合的 ID 集合,必须和输入集合完全相等,差一个都要报错。
in_ids = {l.split("\t")[0] for l in open("manifest.tsv")} out_ids = {json.loads(l)["id"] for l in open("results.jsonl")} missing = in_ids - out_ids extra = out_ids - in_ids assert not missing and not extra, f"missing={len(missing)} extra={len(extra)}"这三行断言我建议每个多卡任务都加上,成本极低,能挡住一整类问题。我见过的一个案例就是靠这个断言发现的:某次任务少了 3 条,但是因为总数没检查,一直到下游才发现。
4.5 超时重试导致的重复写入
第五个坑:重试重复写入。为了应对超时,很多人会给任务加重试机制,超时 10 分钟就重跑一次。问题在于,如果第一次并没有真正失败,只是响应慢,重试就会导致同一个样本被写两次。如果你的下游是"按文件名覆盖",那没影响;但如果下游是追加写,就会出现重复行。
处理这个问题有两个层次:一是产物写入用固定文件名加原子重命名,天然幂等;二是记录侧用"幂等键"去重,把样本 ID 加上重试次数以外的一个稳定标识做唯一键。我一般用样本 ID 直接做唯一键,重试的时候覆盖而不是追加。
还有一个更隐蔽的情况:两次重试几乎同时完成了,都在做重命名。这种竞态在批量任务里确实会遇到,概率不高但存在。稳妥的做法是重命名前先检查目标文件是否已存在且校验通过,存在就跳过。多一次检查,省一次数据事故。
5. 我现在的选型和验收流程
5.1 按任务特性分配,而不是押注一家
跑完这轮测试之后,我并没有得出"某家最好"的结论,因为这四家的差异本质上是资源类型和产品取向的差异,不是好坏。我现在的做法是按任务特性分:
| 任务特性 | 我的选择倾向 | 理由 |
|---|---|---|
| 结果要准、可以等 | 平台 C 这类不动的资源 | 一致性和完成率最好,代价是排队 |
| 赶时间、可容错 | 平台 A 这类弹性资源 | 来得快,但要自己补中断防护 |
| 依赖复杂、要开箱即用 | 平台 B 这类预置丰富的 | 启动快,但必须锁镜像 |
| 多卡通信密集 | 平台 D 这类互联顺的 | 通信稳定,但条数校验必须做 |
这套分配的底层逻辑是:把任务的容错能力和平台的特性对齐。越不能容忍错误的任务,越要放到确定性高的平台上,哪怕慢一点。反过来,做了充分幂等和校验的任务,就可以放到弹性资源上换速度。
我不建议把鸡蛋放一个篮子,也不建议为了"统一"强行用一家。统一带来的管理便利,远比不上一次数据事故的代价。真正需要统一的是校验流程和埋点格式,而不是资源来源。
5.2 一次两百块以内的验收流程
新平台要不要用,我有一套固定的验收流程,跑一遍大概几十到两百块钱,一两天内能出结论。顺序是这样的:
第一步,准备 200 条标尺样本,覆盖任务里的各种边界情况(最短的、最长的、最容易出错的)。这 200 条要固定不变,以后每次验收都用同一批。
第二步,在平台上跑三轮,分别安排在不同时段。每轮记录四个指标:产物校验完成率、静默错误率、跨批次一致率、断点续跑遗漏量。
第三步,做一次人为中断测试。跑到一半手动杀掉实例,然后恢复,看它能不能干净地接上,统计重复量和遗漏量。这一步很多人会跳过,但它最能暴露调度层的问题。
第四步,做一次环境比对。把任务开始时打印的依赖版本和环境信息收集起来,跟你的基准比对,确认没有漂移。
第五步,跑一次多卡任务(如果你的业务需要),重点验证结果条数和 ID 集合的完整性。
这五步跑完,一个平台能不能用基本就有答案了。我的判断阈值是:产物校验完成率低于 99%、或静默错误率高于 1%、或跨批次一致率低于 95%,就直接不用。这三个阈值不是拍脑袋来的,是根据我业务里能承受的返工量倒推的——低于这个线,返工的成本会超过换平台带来的收益。
5.3 上线之后还要盯的三个数
验收通过只是开始,长期使用还得有监控。我保留了三个数的日常跟踪:
一是每日完成率趋势。正常情况下它应该是一条平稳的高位线,一旦出现连续下滑,说明环境或者资源在变。我遇到过的情况是某周完成率从 99.5% 掉到 97%,查下来是平台侧某个区域的存储延迟升高,导致产物上传超时。
二是标尺样本哈希。每天跑一次那 200 条标尺样本,比对哈希。哈希变了就是环境漂移,立刻查。
三是重试率。重试率突然升高通常意味着资源层在抖动,虽然任务最终成功了,但耗时和成本都会上去。这个数能提前预警。
最后分享一个小技巧:把校验脚本做成能读历史报告的形式。每轮的报告都存成 JSON,我写了个小脚本把所有报告聚合成一张趋势表,一眼就能看出哪一周开始出问题。这个习惯帮我省了很多事后追溯的时间——出问题时不用重新跑,翻历史报告就能定位是哪个指标先动的。
我个人在实际操作中的体会是,选 GPU 平台这件事,最后拼的不是谁家卡好,而是你的校验体系有多扎实。校验做得好的团队,用哪家平台都能跑出可用的结果;校验做得差的,用最好的平台也会在某个深夜收到一堆废数据。平台是变量,你的流程才是常量。