Files
Knowledge/1 使用/Linux/极其高效的命令行工具.md
T

363 lines
21 KiB
Markdown

去年换服务器的时候我干了件事:没有直接把旧环境整个搬过去,而是从一台干干净净的机器开始,谁让我用得难受了,我才装谁。
这个过程比我想的有意思。头十分钟我装了四个东西,接下来两天陆陆续续又装了几个,然后就基本停了。而我旧机器上那一堆当年兴冲冲装的、名字都挺酷的工具,一多半我压根没想起来。
那份记录后来变成了一个叫 setup.sh 的文件,现在二十三行。这篇讲的就是这二十三行,以及它们各自是因为什么进来的。
我不太想写成一份"神器大全"。那种清单我以前收藏过很多,收藏完就没再打开。真正有用的信息不是"有哪些工具",是"什么样的疼痛值得用工具去治,以及它是怎么治的"。所以下面每一个我都会说清楚:我因为什么装的它,它具体解决了什么,怎么用。
命令我都会翻译成人话,没怎么碰过终端的人也能看懂在干嘛。
先说一句丑话:这里头有几个是近几年才流行的新工具,不同发行版的安装方式和名字略有差异,你真要装的时候查一下当下的官方说明。我给的是这些工具解决什么问题,这个是不会过期的。
## 头十分钟装的那四个
新机器上最先让我难受的,永远是同一件事:我知道那个东西在,但我找不到它。
一个跑了半年的项目,几十个实验目录,里面藏着日志、配置、checkpoint、临时脚本。你脑子里只有一个模糊的印象:那个改过学习率的配置文件,好像在某个 exp_0 什么的目录里。
系统自带的 find 能干这事,但它的写法对人非常不友好:
find . -name "*config*" -type f
要引号,要 -name,要 -type f,少一个就报错或者给你一堆你不想要的东西。而且它会一头钻进 .git 目录和虚拟环境里,给你翻出几千条垃圾结果。
第一个装的是 fd。同样的事:
fd config
完了。它默认忽略隐藏目录和 .gitignore 里写明不用管的东西,所以吐出来的结果是干净的。想只看 Python 文件加个 -e py:
fd -e py train
意思是:名字里带 train 的 py 文件在哪。
第二个是 ripgrep,命令名是 rg。这个是找文件内容的,对应系统自带的 [grep](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=2&q=grep&zhida_source=entity)。
rg "lr=" exp_*/config.yaml
意思是:在这些配置文件里,把含有 lr= 的行都找出来,顺便告诉我是哪个文件的第几行。
它比 grep 快很多,而且同样会自动跳过那些不该搜的目录。我第一次在整个项目目录下 rg 一个函数名、瞬间就出结果的时候,是真的愣了一下。以前用 grep -r 搜同样的东西,我习惯是敲完回车就去接杯水。
第三个是 fzf,如果只让我留一个,我留它。
它干的事听起来很平淡:给你一个模糊搜索的选择框。你输入几个不连续的字母,它从一堆候选里实时筛。它的价值在于,它能接在任何东西后面。
装完之后最直接的变化是,按下 Ctrl-R 翻历史命令,从原来那种一下一下往上按的痛苦,变成了输入三个字母就把三个月前那条又长又复杂的命令捞出来。我上一篇讲命令行的时候说过,命令是可以被写下来的东西,而 fzf 让"写下来的东西"真正变得可以被随时找回。这两件事凑一块儿威力才完整。
还能这么用:
vim $(fzf)
先弹出选择框让你搜文件,选中哪个就用编辑器打开哪个。
杀掉一个进程也不用先 ps 再抄 PID 了:
kill -9 $(ps aux | fzf | awk '{print $2}')
意思是:把所有进程列出来给我搜,我选中一行,把它的进程号取出来,杀掉。
第四个是 zoxide。装完之后你用 z 代替 cd:
z exp3
它会记住你常去哪些目录,你只需要给个片段,它带你跳过去。不用再敲那种十几层的长路径。这个东西的收益特别隐形,你装的时候觉得"就这?",一个月后回到没装它的机器上,你会明显地烦。
这四个是一类东西,它们不给你新能力,它们只是让"找"这件事从一件需要动脑的事变成一个反射动作。这种改善听起来不性感,但它节省的是你一天里被打断几十次的那种细碎的注意力。
## 服务器磁盘满了那次,我装了 ncdu
有一天集群提示我空间不够,训练起不来。
我第一反应是 du -sh * 一层层往下找,找了十几分钟,眼睛都花了。那次之后我装了 ncdu。
ncdu ~/
它会扫一遍,然后给你一个可以上下键走进走出的界面,每个目录后面标着占了多大、按大小排好序。哪个目录胖,一眼就看见,还能直接在里面按 d 删掉。
我那次进去三层就找到了元凶:一个我早就放弃的实验,每个 epoch 存一个 checkpoint,存了四十多个,一个两个多G。
顺手提一下 [dust](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=dust&zhida_source=entity),它是 du 的替代品,直接画出一棵带条形图的树,适合"我就想扫一眼谁最胖"这种需求。ncdu 适合排查,dust 适合扫视。
还有 duf,看各个分区还剩多少,比 df -h 那一堆对不齐的数字好读。这三个都属于装了不用天天想起、但出事那天能救你的东西。
多说一句我踩过的坑:磁盘报满的时候,别只看文件大小,还要看是不是被删掉的文件还被进程占着。你 rm 掉一个日志,但写它的程序还在跑,空间不会释放。这时候用 lsof 找它:
lsof | grep deleted
意思是:列出所有被打开的文件,找出那些"已经被删了但还被占着"的。我第一次遇到这个的时候完全懵了,明明删了几十个G,df 一看一点没变。
## 训练跑起来之后,你需要看清它在干嘛
这块是我最有体感的,因为跑多模态的实验,大部分时间就是等,而等的时候你需要知道它到底活着还是已经僵了。
第一个,gpustat 或者 [nvtop](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=nvtop&zhida_source=entity)。
nvidia-smi 大家都会,但它一屏东西太多,看一次要找半天。gpustat 是一行一张卡,谁占了、占了多少显存、利用率多少,清清楚楚:
gpustat -i
那个 -i 是持续刷新。nvtop 更进一步,给你一个带曲线的界面,像 [htop](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=htop&zhida_source=entity) 但是给显卡用的。你能一眼看出利用率是稳定在高位,还是在那儿一顿一顿地跳。
这个真的有用,不是花架子。利用率一顿一顿的,通常意味着显卡在等数据,瓶颈在数据加载而不是在计算。这个信号如果你看不见,你会白白多等几个小时,还以为是模型本来就慢。
第二个,ts,来自一个叫 moreutils 的工具包。
它干的事一句话说完:给流水一样的日志,每行前面加个时间戳。
python train.py | ts '%F %T'
以前我的日志长这样:一堆 loss 数字,不知道哪一行是什么时候打的。出问题回头查,只能靠猜。加了 ts 之后,每一行前面都有精确到秒的时间,你能立刻算出每个 epoch 花了多久、卡在哪一步。
这个改动小到不值一提,但它把我的日志从"一堆数字"变成了"一份有时间轴的记录"。查问题的时候差别巨大。
moreutils 里还有一个更冷门的,叫 [sponge](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=sponge&zhida_source=entity)。它解决一个特别隐蔽的坑:
sort file.txt > file.txt
这条看起来合理,其实会把文件清空,因为输出重定向会先把文件截断,而此时 sort 还没读完。换成:
sort file.txt | sponge file.txt
sponge 会先把内容全吸进去,等上游结束了再一次性写回。我是被坑过之后才知道有这么个东西的,那个文件不重要,但那种"我明明只是排个序,你怎么给我清空了"的荒谬感,记了很久。
第三个,pv。
传一个几十G的数据集、解压一个大压缩包,系统默认什么都不告诉你,你只能盯着光标看它转,完全不知道是快好了还是刚开始。
pv data.tar.gz | tar xzf -
加了 pv,你就有了进度条、速度和剩余时间。它本质上是插在管道中间的一个观察窗,任何数据流过它,它就报告一下流了多少。
## 数据处理:这几个能省掉大量临时脚本
搞机器学习,一半时间在跟数据的格式较劲。以前我遇到点需求就写个 Python 小脚本,写完就扔,下次再写。后来发现好多需求根本不值得开一个 py 文件。
jq,处理 JSON 的。这个可能不算冷门,但用得深的人不多。
jq '.results[] | .score' out.json
意思是:把 results 这个列表里每一项的 score 拿出来。加个 -r 可以去掉引号,方便接给别的命令。
我最常用的场景是:一堆评测输出的 json,我想看某个指标的分布。以前是写脚本,现在是一行,而且能直接接到后面排序、算平均。
[shuf](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=shuf&zhida_source=entity),这个是系统自带的,但我很少见人提,而它对做机器学习的人来说简直是量身定做:
shuf -n 1000 huge.jsonl > sample.jsonl
意思是:从这个巨大的文件里随机抽一千行出来。
调试数据处理流程的时候,你不需要跑全量,你需要一个有代表性的小样本。用 head 取前一千行是有偏的,因为数据往往按某种顺序排过。shuf 一行解决,而且它对大文件的处理效率很好。
split,也是自带的,把大文件切块:
split -l 100000 big.jsonl part_
每十万行切一个,分块之后可以并行处理。
[column -t](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=column+-t&zhida_source=entity),自带的排版小工具,冷门到我怀疑一半人不知道它存在:
cat result.txt | column -t
它会把用空格分隔的内容对齐成整齐的列。我那些手工输出的实验结果,加这一下之后从一团乱麻变成一张表,看错数字的概率明显降低。
sd,替代 sed 做简单替换的。sed 的语法我每次用都要查:
sd 'lr=0.001' 'lr=0.0005' config.yaml
就这么直观,前面是找什么,后面是换成什么。复杂的场合 sed 还是更强,但九成的日常替换,sd 足够,而且不容易写错。
处理 CSV 我用 miller,命令名 mlr:
mlr --csv stats1 -a mean,max -f loss data.csv
意思是:算这个 csv 里 loss 这一列的均值和最大值。
不过这个我用得不算深,我基本只用统计和筛选那几个功能,它更复杂的能力我没吃透,不敢多讲。
## 让重复的事自己发生
这一组是我认为收益最高、但知道的人最少的一类。
entr,监视文件变化然后自动执行命令。
fd -e py | entr -r python train.py
意思是:所有 py 文件,只要有任何一个被我保存了,就重新跑一次训练。那个 -r 是先杀掉上一次的进程再重启。
这个东西改变了我调试的节奏。以前是改代码、切窗口、回车、看结果、切回去改。现在是改代码、保存,眼睛都不用离开编辑器,旁边那个窗口自己就跑起来了。写测试的时候尤其明显:
fd -e py | entr pytest tests/
保存即测试。这个循环紧到什么程度,直接决定了你调试时的心态,松一点你就容易走神,紧一点你就一直在状态里。
[GNU parallel](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=GNU+parallel&zhida_source=entity),把串行的事变成并行。
parallel -j 4 python eval.py --ckpt {} ::: ckpt/*.pt
意思是:对每一个 checkpoint 文件都跑一次评测,同时跑四个。那个 {} 是占位符,代表当前这个文件。
我以前干这事是写个 for 循环一个个跑,二十个 checkpoint 排队等着。现在四个一起上,时间除以四。要注意别开太多把显存或者内存挤爆,-j 后面那个数字要看着机器给。
[hyperfine](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=hyperfine&zhida_source=entity),给命令做基准测试。
hyperfine 'python loader_a.py' 'python loader_b.py'
它会各跑很多次,给你平均耗时、标准差,还有谁快多少倍。
我用它比较过两种数据加载的写法。之前判断哪个快,我是自己 time 一下,跑两三次,凭感觉。但单次测量的波动其实很大,我不止一次被随机波动骗过,以为某个改动有效,后来发现是噪声。hyperfine 给的是带方差的结论。
这一点其实挺重要:做科研的人应该对"单次测量"天然警惕,但轮到自己写代码调性能的时候,反而经常凭一次运行下结论。我自己犯过这个错,还挺尴尬的。
[direnv](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=direnv&zhida_source=entity),进入目录自动加载环境变量。
在项目目录里放一个 .envrc,写上这个项目要用的显卡、路径、密钥,cd 进来自动生效,cd 出去自动失效。
export CUDA_VISIBLE_DEVICES=2
我用它主要是为了防一件事:忘了自己上一次把显卡设成了哪张,结果新实验一头撞到别人正在用的卡上。这种事发生一次就够让人不好意思了。
timeout,自带的,给命令设一个上限:
timeout 300 python probe.py
超过五分钟就掐掉。写探测脚本或者跑不确定会不会卡死的东西时,加上它,你就不会回来发现某个进程僵在那儿占了一晚上资源。
flock,防止同一个任务重复执行:
flock -n /tmp/mytask.lock python task.py
如果上一次还在跑,这一次就直接放弃。定时任务里这个特别有用,不然上一轮没跑完下一轮又起来了,两个一起写同一个文件,场面会很难看。
## 在共用服务器上活下来的几个
实验室的机器是大家共用的,这一组是关于怎么当个不讨人嫌的人,顺便别把自己搞死。
[nice 和 ionice](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=nice+%E5%92%8C+ionice&zhida_source=entity)。
nice -n 19 ionice -c3 python preprocess.py
意思是:这个数据预处理,把它的 CPU 优先级和磁盘优先级都降到最低。它会慢一点,但它不会拖慢别人正在跑的训练。
我第一次意识到该这么做,是有次我起了一个吃满 CPU 的预处理,隔壁同门的训练速度掉了三成,他还以为是自己代码的问题,查了半天。那次挺不好意思的。这两个命令加起来不到二十个字符,成本几乎为零,但它是一种态度。
lsof,找出是谁占着东西。
lsof -i :8888
意思是:8888 这个端口被谁占了。启不了 Jupyter 的时候第一个敲它。
显卡上有僵尸进程占着显存但 nvidia-smi 看不出是谁的时候:
fuser -v /dev/nvidia*
它会列出所有正在用显卡的进程。找到自己那个僵掉的,杀了就行。别人的别乱动,这个我得强调一下,共用机器上手快一时爽,后面很尴尬。
[mosh](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=mosh&zhida_source=entity),替代 ssh 的连接方式。
它最大的价值是:你合上笔记本、换个网络、地铁上断线,连接还在。ssh 在这些情况下会断,断了你正在前台跑的东西就没了。mosh 会自己缓存和重连,你重新打开电脑,光标还在那儿等你。
配合 [tmux](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=tmux&zhida_source=entity) 用最稳妥。tmux 保证程序活着,mosh 保证你回来得顺畅。
[trash-cli](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=trash-cli&zhida_source=entity),给 rm 一个回收站。
trash-put old_ckpt/
它把东西移到回收站而不是直接抹掉。我装它的原因不太光彩:我用通配符删过一批文件,范围比我以为的大,几天的训练结果跟着一起没了。
那天下午我坐在工位上,把那条命令反复看了很多遍,想搞清楚自己到底怎么写错的。其实很简单,就是手快。
我没有把 rm 直接 alias 成 trash-put,因为那会让我在别人的机器上产生错误的肌肉记忆,那更危险。我的做法是:删单个明确的东西照常用 rm,批量删、带通配符的一律先 ls 看一眼,重要的用 trash-put。
[croc](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=croc&zhida_source=entity),两台机器之间直接传文件。
croc send data.zip
它给你一个短语,在另一台机器上敲 croc 加那个短语,就开始传了,不用配置密钥、不用管中间有没有跳板机。临时给同门传个模型权重,比来回折腾 scp 路径快得多。
## 你机器上已经装好了、但你没用过的
这一段不用装任何东西,全是自带的,而且冷门得离谱。
comm,比较两个已排序的列表:
comm -23 all.txt done.txt
意思是:哪些在 all 里但不在 done 里。断点续跑的时候,这一条就能算出"还有哪些没处理",不用写脚本。
tac,把文件倒着输出。看日志找最近的报错特别顺手:
tac log.txt | rg -m1 "Error"
意思是:从后往前找,遇到第一个 Error 就停。日志几百万行的时候,这比从头搜快得多。
xargs,把上游的输出变成下游命令的参数:
fd -e ckpt | xargs -I{} ls -lh {}
它是把两个不搭的命令粘起来的胶水,值得单独花十分钟搞懂。
script,把整个终端会话录下来:
script session.log
之后你敲的和屏幕上出现的所有东西都会写进那个文件。我用它记过一次很麻烦的环境配置过程,后来照着复盘,把那份记录整理成了脚本。
还有两个纯粹的手指效率,免费的:
!! 代表上一条命令。忘了加 sudo 的时候,sudo !! 。
!$ 代表上一条命令的最后一个参数。刚 mkdir 了一个长路径,下一句 cd !$ 直接进去。
最后一个,tldr。这不是自带的,但值得作为这一段的结尾:
tldr tar
man 手册是给专家做参考的,又长又劝退。tldr 给你的是五六个最常用的例子。tar 的参数我到现在都记不全,每次都是 tldr 一下抄一条。
不背参数这件事,不丢人。你需要记住的是"有这么个东西能干这件事",具体怎么写,查。
## 现在说点扫兴的
写到这儿我得把另一面讲清楚,不然这篇就是一份不负责的种草清单。
第一,别把肌肉记忆绑在别人机器上没有的工具上。
我认识一个人,把 ls 全 alias 成了新工具,cat 换掉,cd 换掉,配置得极其漂亮。结果他一上没有权限装东西的集群,整个人就废了,连最基本的操作都要卡一下。
我的规矩是:新工具用新名字,不覆盖原来的命令。fd 就是 fd,不动 find。你在任何一台裸机上都得能干活,这是底线。
第二,很多服务器你没有 sudo。
好消息是上面这些新工具大多是单文件的,可以直接下载扔进自己家目录的 bin 里,不需要管理员权限。[conda](https://zhida.zhihu.com/search?content_id=792766250&content_type=Answer&match_order=1&q=conda&zhida_source=entity) 和 pip 也能装不少。真装不上的,就退回自带工具,前面那一段"已经装好了"的内容就是为这种情况准备的。
第三,这才是我真正想说的那句。
我大二那年是个装备党。收藏了几十篇神器推荐,配置文件搞得花里胡哨,终端打开是一屏漂亮的信息面板,给同学演示的时候挺有面子。
但那些工具我一个都没真正用起来。因为它们不是从我的疼痛里长出来的,是从别人的清单里抄来的。
后来某个阶段我意识到一件不太好意思承认的事:我那段时间之所以那么热衷于折腾工具,是因为实验做不出来。改配置有即时的、确定的成就感,而实验没有。捣鼓终端主题的那两个小时,我感觉自己在"提升效率",实际上我是在躲。
想通这个之后我给自己定了个规矩,一直用到现在:一个工具要进 setup.sh,得先有一次真实的狼狈。磁盘满了、日志没时间戳、文件误删了、显卡被自己撞了。疼过一次,再去找工具,那个工具就会活下来。
反过来,某天我又想装个什么新东西,先问自己一句:它在解决什么疼?如果答不上来,那大概不是工具的问题,是我今天不太想面对手上那件难的事。
这个自问帮我省下的时间,比上面所有工具加起来还多。
## 如果只让你装五个
真要挑,我的顺序是:fzf、ripgrep、fd、ncdu、entr。
前三个改善的是每天几十次的细碎动作,收益天天都在;ncdu 是出事那天救你;entr 会实实在在改变你调试的节奏。
跑深度学习的话,再加 gpustat 和 ts,这两个是这个行当的专属刚需。
判断一个工具值不值得进你的清单,标准我只有一条:它必须对应一次你真实卡住过的时刻。你想不起那个时刻,就先别装,等它来找你。
那份 setup.sh 现在躺在我的仓库里,二十三行,每行前面我都写了一句注释,写的不是这个工具是什么,而是我为什么装它。翻那个文件,比翻任何一份工具清单都有意思,因为它记的其实不是工具,是我这几年具体在哪些地方栽过。
写完这篇我看了一眼,今天的实验还在跑,进度条走了六成。