TT Lab
开始
学习 学习路径 课程

AI瘦身失败事件

你请求的执行提供程序,未必就是你拿到的

在 TT Lab 中继续学习

一句话总结

执行提供程序(EP)是用来请求的,而不是有保障的。即使放进一个不存在的 EP,也 不会报错,而是静默回退,必须去问会话,才能知道它实际拿到了什么。

为什么这是个问题

明明在列表里写了要用加速器,实际上却是在用 CPU 运行,这种事经常发生。问题在于,那时什么事都不会发生。不抛异常,退出码也是 0。只有一行警告从标准错误里一闪而过,在收集日志的地方,这一行就被淹没了。

在这个实验环境里亲自测一下就是这样。把根本不存在的 CUDAExecutionProvider 放在列表最前面,会话照样能创建,推理也能进行。去问 session.get_providers(),返回的只有一个 CPUExecutionProvider。如果编造一个完全不存在的名字放进去,会出现“Unknown Provider Type”的提示,同样落回到 CPU 上运行。

所以规则只有一条:不要相信请求列表,要去问会话。部署日志里该留下的,不是请求的列表,而是会话返回的列表。

EP 决定什么

EP 不是用来选择“这个模型在哪里运行”的装置,而是决定 每个节点由谁来承担 的装置。执行器按顺序遍历请求列表,逐个询问各个 EP“这个节点你能承担吗”,只把说愿意承担的节点交给它。剩下的节点交给下一个 EP,最终没人承担的,就由 CPU 承担。

所以一个模型会被切成多个片段,在不同的地方运行。片段与片段之间要传递值。如果所支持的算子列表在计算图的顺序上不断被截断,片段数就会增加,传递的次数也随之增加。

지원: MatMul 만               조각 5개 · 넘김 4번
  [MatMul] -> Add -> Relu -> [MatMul] -> Add
    EP        CPU    CPU        EP       CPU

列表的 顺序就是优先级。写在前面的提供程序先被询问,并拿走它说愿意承担的节点。所以同样的两个提供程序,只换一下书写顺序,分配也可能不同。而且 CPU 即使不写,也会跟在最后——在这个环境里只写一个 AzureExecutionProvider 来创建会话,实际的列表就是它加上 CPUExecutionProvider 两个。因为不能留下没人承担的节点。请求的列表与实际的列表不同,是 正常工作而不是错误,原因也在这里。

由此引出一个重要的结论:同一个模型放入同样的输入,不同的机器也可能得出不同的数。片段切分方式不同,计算顺序就不同,而浮点数加法一旦顺序改变,结果就会有细微的差别。所以“在我们的笔记本上是对的”很难作为依据。

层层叠加之后,误差会怎样

量化误差在每一层都会新产生,前一层产生的误差会成为下一层的输入。所以 误差会累积。不过实际测一下就会发现,它并不是每一层均匀增长的。在这个实验里测五层模型时,各层边界的相对误差依次是 0.099、0.074、0.063、0.214、0.167、0.369。时降时升,整体变成了 3.7 倍。

有下降的区间,是因为每一层的值的大小不同,激活函数会截掉一部分,缩放因子也是重新确定的。所以不能只说“每一层都会稍微变大”就算了,必须在每个层边界上测量并做成表,才能看出问题在哪里。如果在某一层比值跳到三倍,那一层就是收窄范围的候选。

误差增长,也不等于准确率就会同样崩溃。即使最后一个边界的相对误差是 0.37,决策发生改变的样本也可能寥寥无几。因为输出向量整体稍微偏移,和排名被颠倒,是两回事。所以分层误差表要 用来挑选该动哪里,是否部署则另外用任务指标来判断。如果想把这两件事合并成一个数字,两者都会变得模糊。

要测层边界,就得能看到中间张量。在 ONNX 计算图中,把想看的张量追加到计算图的输出列表里就行。不过在整数版本里,整数张量无法以 float 取出,所以把线性算子 接收输入的那一侧 的张量定为边界更安全。那个位置在两个计算图里都是 float。

现场必须做的事

下一项实验要做什么

把存在的 EP 和不存在的 EP 混在一起请求,亲自测出实际拿到了什么,并做一个模拟,在给定支持的算子列表时,数出计算图会被切成几段。然后把层边界张量取成计算图的输出,用同样的输入分别运行 FP32 版本和整数版本,测出每一层的误差,做成增长比值的表。最后,把同一个模型、同一个输入,只改变优化级别和线程数来运行,看结果是否会变。评分器每次都会用不同的模型、随机种子和支持列表,真正运行你的工具,并把同样的计算重做一遍来核对。