目录

  1. 波澜不惊
  2. 惊涛骇浪
  3. 风平浪静
  4. Reference
  5. END

波澜不惊

记不太清楚那天的天气了。我像往常一样起床上班地铁轰隆一小时。

到公司抓紧时间写好处理数据的代码,然后告诉运维帮我开 24 台 GPU 服务器。

“ip.txt”

没过一会儿,运维甩给我一个包含 24 个服务器 IP 的文件。

“我有一些依赖要安装,代码要放上去,还得配 aws,难不成我要一台一台连接上去,然后一台一台敲上去?”

我咨询了下运维有没有什么捷径可走,运维说 xshell。此时的我有点疑惑,因为我之前几乎没怎么用过 xshell,向来都是 VS Code 和 Windows Terminal。

下了一个 30 天试用版,地方也好找,“发送键到所有会话”,啪啪啪在一台机器上输入命令,命令马上复制到了其他机器,很快就部署完了。

开始启动 1000 核的 client,开个 htop 看到 client 这边波澜不惊,开个 nvtop 发现 server 那边也是波澜不惊,只不过是高水位的波澜不惊。

Client htop。情况类似,忽略内存情况,CPU 占用非常低,毕竟不是 CPU-bound 任务。Client htop。情况类似,忽略内存情况,CPU 占用非常低,毕竟不是 CPU-bound 任务。
Server nvtop。情况类似,GPU 打得满满的。Server nvtop。情况类似,GPU 打得满满的。

我去接杯水,看着他们波澜不惊,我心里也波澜不惊了。

惊涛骇浪

由于我是处理完一批上传一批结果的,所以最终全部处理完之后,需要合并一下结果。单看每一份文件似乎没啥问题,但是合并完去重之后发现,非常多重复的,理论上来说是不可能的,肯定是哪里出了问题。

于是我回去翻代码,看看是哪里写重了还是循环里变量重复使用还是怎么回事,然后我把注意力放到了下面的这段代码上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
texts = ['Here', 'are', 'some', 'texts']
ids = ['Here', 'are', 'text', 'ids']
batch_size = 2
lines = []
for i in range(0, len(texts), batch_size):
batch_texts = texts[i : i+batch_size]
try:
batch_probs = parser.parse(batch_texts)
batch_probs = ['\t'.join(map(str, probs)) for probs in batch_probs]
except Exception as e:
logger.error(f"{e}")
batch_probs = ['\t'.join(['None'] * 5)] * len(batch_texts)
lines.extend([f"{id_}\t{probs}" for id_, probs in zip(ids, batch_probs)])
upload_s3(lines)

注意最后一行的 zip,第一个是每个样本对应的 id,shape 为 (len(texts),) ;第二个是样本对应的概率 shape 为 (batch_size, num_labels)

所以问题就来了,len(texts) != batch_size ,也就是说 zip 的两个参数长度不等。那么此时 Python 会怎么办?

取短舍长。

当较短的参数消耗完时,迭代就会停止:

1
2
>>> list(zip(range(3), ['fee', 'fi', 'fo', 'fum']))
[(0, 'fee'), (1, 'fi'), (2, 'fo')]

而不是抛出异常,甚至 warning 也不会抛出。所以运行时你不会发现任何问题。这就很危险了。

幸好 Python 社区也注意到了这个问题,2020 年 Brandt Bucher 发了一个 PEP 618,提议为 zip 函数增加一个参数 strict 以进行长度检查。该 PEP 最终通过并合并在了 3.10 版本中。所以如果你是用的是 >=3.10 的版本并且想要两个参数完全相等,那么可以指定 strict=True 来强制限定,如果长度不等则会抛出 ValueError 异常:

1
2
3
4
>>> list(zip(range(3), ['fee', 'fi', 'fo', 'fum'], strict=True))
Traceback (most recent call last):
...
ValueError: zip() argument 2 is longer than argument 1

所以,我得改下代码重新跑,再花一次 24 台 GPU 服务器的钱。虽然钱不是我出,但是还是觉得挺愧疚。

风平浪静

由于各种原因,切版本是不太现实的。所以直接将 ids 改为 batch_ids 即可。但为了以后一旦写错让程序抛出异常,我还是在 zip 前手动加了一个 assert 来进行长度检查。以后在涉及 zip 的地方,一定要多加检查,使用 assert 或者 strict=True 来显式抛出异常。Python 的默认行为也太容易出错了,我觉得至少得抛出个 warning 吧。

所以,zip,顾名思义,拉链。拉链的两边长度不等时,根据生活经验,你只能拉到较短一边的尽头。Python 中的 zip 同理,如果 A 和 B 长度不等,那么 zip(A, B) 的结果长度就是 min(A, B)。这不是问题。

问题在于,这是静默发生的。

算是一个自己不小心挖的坑吧,修复 bug,再次运行,回归风平浪静。

Reference

END