如何在共享的 Linux 服务器上合理使用资源
我目前在外校的网络安全实验室线上实习,实验室有一台共享的 Linux 服务器,我至今为止的所有工作几乎都是在这台服务器上完成的。
尽管有一定的 Linux 使用经验,但是我还是在整个实习过程中遇到了比较多的问题。究其原因,使用一台属于自己的 Linux 本地机器,和作为一个普通用户访问共享的 Linux 服务器,这之间还是有较大的区别的。
这篇文章里,我想分享下我使用服务器中用到的一些工具,遇到的问题以及解决方案。
SSH 连接工具#
我找不出任何不推荐 VS Code Remote-SSH 插件的理由。
只需要简单的配置,你就可以使用本地的 VS Code 终端编辑远程服务器上的文件,同时,所有的插件和配置都能方便地同步到远程服务器上,体验上和本地写代码并没有区别。
最重要的是,Github Copilot 也能在远程服务器上方便地访问。使用过 AI Copilot 后,我很难回到没有 AI 插件的环境了,由俭入奢易,由奢入俭难啊。
并行化工具#
在服务器上,我经常需要同时运行多个任务。特别常见的场景是,同一段代码需要在不同的数据集或者不同的参数设置下进行评估。
这种情况如果在本地的机器,我可能会用 GNU Parallel;但是服务器上并没有安装这个工具。作为一个替代方案,我常用的方式是手动写一个 shell 脚本来管理任务。
例如,main.py 这个程序需要在 10 个数据集上运行,我写的 shell 脚本可能会像这样:
#!/bin/bash
for i in {1..10}
do
nohup python main.py --dataset dataset_$i 2>&1 > output_$i.log &
done
这里的 nohup 是用来创建后台进程,如果不使用 nohup 直接运行 python main.py,在 SSH 连接中断后程序会停止运行;& 是用来将任务放到后台运行,和当前的终端分离。
当然,这是只是最粗糙的并行脚本,它的问题有很多,最容易想到的有两点:
- 一些任务可能会占用太多的 CPU/GPU 资源,导致服务器上的其他任务无法正常运行。
- 所有任务的输出都被 nohup 默认重定向到 nohup.out 文件,这样会导致输出混乱,不方便查看。
所以,我实际上会在脚本里手动控制并行任务的数量,并将输出重定向到不同的文件中。
#!/bin/bash
max_jobs=5
current_jobs=0
for i in {1..10}
do
if [ $current_jobs -ge $max_jobs ]; then
wait -n
current_jobs=$((current_jobs-1))
fi
nohup python main.py --dataset $i 2>&1 > output_$i.log &
current_jobs=$((current_jobs+1))
done
CPU 资源控制#
虽然上面展示的仍然是一个比较粗糙和简单的脚本,但是已经能满足我的需求了,我在几个月内都在使用这样的脚本来完成任务。
直到最近,共用服务器的其他老师反馈说我的任务占用了太多的 CPU 资源,导致他们的实验无法正常进行了,并提醒我要限制 numpy 的线程数。我打开 htop 一看,才发现所有的 CPU 都在满载状态,load average 高达 700~800(服务器有 112 个逻辑 CPU),这时我才意识到问题的严重性。
紧急 pkill 了所有任务来释放所有资源之后,我按照老师提示的方法设置了 numpy 的线程数,问题才得以解决。
事后复盘发现,我当时在实验的代码是一段 python 代码,其中的 numpy 模块运行时默认会尝试用所有 CPU 核心进行多线程优化,这是因为 numpy 调用的底层是一些高性能计算库,这些计算库会尽可能利用所有的 CPU. 为了限制这些高性能库榨干服务器的计算资源,需要在调用 numpy 之前先设置环境变量,使这些库认为只有少量的 CPU 可以使用。
import os
os.environ["OMP_NUM_THREADS"] = "4" # OpenMP 线程数
os.environ["OPENBLAS_NUM_THREADS"] = "4" # OpenBLAS 线程数
os.environ["MKL_NUM_THREADS"] = "4" # MKL 线程数
os.environ["NUMEXPR_NUM_THREADS"] = "4" # NumExpr 线程数
import numpy as np
在设置这些变量后重新运行一段时间后,服务器上出现了其他问题:内存占用过高,可用内存仅剩 10GB/400GB。经过 htop 查看和分析后,导致这些问题的仍然是我的并行任务…
对于内存的限制还是比较容易的,我在 shell 脚本的最上面加了一行 ulimit -v,限制整个脚本的内存使用量。
硬盘空间管理#
服务器上的硬盘空间是有限的,但是 home 文件夹的膨胀是没有止境的。
好在实验室有额外加装的硬盘,挂载在 /data 目录下。
最开始,我只是把不太常用的数据集放在了 /data 目录下。但后来随着使用时间的增加,home 文件夹下各类文件也越来越多。按照实验室建议,我开始把一些比较常用,但是占用了较大空间的文件夹都挪到了 /data 目录下,然后用 ln -s 重新软链接回 home 文件夹。
mv ~/folder /data/my_user_name/folder
ln -s /data/my_user_name/folder ~/folder
GPU 资源管理#
深度学习的任务十分依赖 GPU 加速。服务器上有若干 RTX 4090,但是由于服务器上有其他用户的实验,我们不能随意使用所有的 GPU 资源。
这一部分实际上分为两个话题,一个是我如何知道自己占用了多少显存,一个是如何合理地分配任务到不同的 GPU 上。
监控 GPU 资源#
查看 GPU 的状态可以使用 nividia 提供的 nvidia-smi 工具,这个工具可以查看 GPU 的使用情况,包括 GPU 的使用率、温度、显存使用情况等等。
这个工具当然很好用,不过,我需要看到这些显存具体是被哪个用户占用的。更具体地来说,我想要看到我自己占用了多少显存,同时,其他用户中是否有人也正在重度使用 GPU,我是否需要管理自己的任务,降低对服务器的影响。
因此我在 AI 工具辅助下编写了一个简单的脚本,这个脚本可以查看当前所有用户的显存使用情况,各个 GPU 剩余的显存,以及当前用户的内存使用情况。
为了方便高亮查看,我把整个脚本的核心代码贴在下面,去掉了 watch -n 1 的前缀。实际使用的脚本中,我使用 watch -n 1 来每秒刷新一次脚本显示,实现实时监控的效果。
#!/bin/sh
nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheader,nounits | \
while IFS=',' read -r pid memory; do
user=$(ps -o user= -p $pid);
echo "$user,$memory";
done | awk -F"," '"'"'{mem[$1]+=$2} END {for (u in mem) print u, mem[u] " MiB"}'"'"' | \
awk -v current_user="'$(whoami)'" '"'"'{if ($1 == current_user) printf "%-10s %-10s %-10.2f GB %-10.2f Cards <==\n", $1, $2, $2/1024, $2/24576; else printf "%-10s %-10s %-10.2f GB %-10.2f Cards\n", $1, $2, $2/1024, $2/24576}'"'"' | sort -k3 -nr | column -t
echo ""
echo "=============================================="
echo "Memory usage by me:"
CURRENT_USER=$(whoami)
TOTAL_MEMORY=$(ps -u "$CURRENT_USER" -o rss= | awk '"'"'{sum+=$1} END {print sum}'"'"')
TOTAL_MEMORY_MB=$((TOTAL_MEMORY / 1024))
TOTAL_MEMORY_GB=$((TOTAL_MEMORY_MB / 1024))
echo "Total memory usage: $TOTAL_MEMORY_MB MB ($TOTAL_MEMORY_GB GB)"
echo ""
echo "=============================================="
echo "Free memory per GPU:"
nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits | awk '"'"'{print $1/1024 " GB"}'"'"' | column -t
当我运行脚本时,可以看到如下的输出:
Every 1.0s: cxhpc: Thu Jan 23 19:22:23 2025
User_A 60806 59.38 GB 2.47 Cards
Me 49092 47.94 GB 2.00 Cards <==
==============================================
Memory usage by me:
Total memory usage: 39969 MB (39 GB)
==============================================
Free memory per GPU:
7.61328 GB
21.5918 GB
3.69727 GB
3.69727 GB
9.93164 GB
21.7715 GB
10.1504 GB
2.45312 GB
为了直观显示显存占用的大小,我直接将显存换算为 RTX 4090 显存 24GB 的倍数,即大约占用了多少张显卡。
GPU 资源分配#
使用 pytorch 框架时,可以直接指定使用哪个 GPU,例如:
import torch
torch.cuda.set_device(1)
有一些场景下,需要保证所有的张量都在同一个 GPU 上,这时可以使用 Tensor.to() 方法,将某个张量转移到指定的 GPU 上。
import torch
device = torch.device("cuda:1") if torch.cuda.is_available() else "cpu"
x = torch.randn(5, 3).to(device)
然而,我的实验场景下,最常见的情况是同一个模型要同时在多个数据集上训练。如果在代码中指定某个特定的 GPU,那么无论并行的任务数量多少,所有的任务都会被分配到同一个 GPU 上,大大限制了任务的并行度。
我为此编写了一个简单的 python 函数,这个函数可以根据当前 GPU 的使用情况,自动选择一个较为空闲的 GPU。
import torch
import subprocess
def get_gpu_memory():
result = subprocess.run(
['nvidia-smi', '--query-gpu=memory.total,memory.used', '--format=csv,nounits,noheader'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
if result.returncode != 0:
raise RuntimeError(f"nvidia-smi error: {result.stderr}")
return [tuple(map(int, x.split(', '))) for x in result.stdout.strip().split('\n')]
def get_lowest_memory_gpu():
if not torch.cuda.is_available():
return None
gpu_memory = get_gpu_memory()
free_memory = [(total - used, i) for i, (total, used) in enumerate(gpu_memory)]
_, best_gpu = max(free_memory)
if _ < 6000:
raise RuntimeError("No free GPU available")
print(f'Best GPU: {best_gpu}')
return best_gpu
opti_device = torch.device(f'cuda:{get_lowest_memory_gpu()}')
这个函数会返回一个较为空闲的 GPU,并直接生成一个名为 opti_device 的 torch.device 对象,可以直接用于 pytorch 的代码中。
指定 GPU 为 opti_device 后,pytorch 会自动选择一个较为空闲的 GPU 进行计算。
使用这个 GPU 代码片段,shell 脚本并行任务需要做出一些修改。在提交每个任务需要调用 sleep 命令等待一段时间,使任务有足够的时间把需要的 GPU 显存分配完成。
#!/bin/bash
max_jobs=5
current_jobs=0
for i in {1..10}
do
if [ $current_jobs -ge $max_jobs ]; then
wait -n
current_jobs=$((current_jobs-1))
fi
nohup python main.py --dataset $i 2>&1 > output_$i.log &
current_jobs=$((current_jobs+1))
sleep 30
done
如果不使用 sleep 命令,那么最开始的 max_jobs 个任务会在短时间内同时被提交,这时它们获取的最优 GPU 极有可能是同一个,导致单张卡的显存不足。