0%

搞来了一个GPU服务器来爽爽

背景交代

在公司的开发环境搞到一个tesla T4 16显存的GPU服务器(同时还有16core 64G),搭配我之前的 32core 128G的CPU服务器,这俩可以做一个集群了,我给他俩的分工就是:CPU当k8s-master,GPU当k8s-worker。

这么考虑的原因是:

  1. CPU服务器有32core,毕竟在master上是要运营“api server、scheduler、etcd、controller”这么多组件,所以需要一个逻辑密集型的服务器,而这些工作可以完全不需要GPU。而且我这个服务器的内存128G,能装很多东西~
  2. GPU唯一的使命就是把显存贡献给大模型,如果master节点的管理任务在GPU节点上,那么消耗GPU的CPU资源是不值当的。

他俩的配合流程是:

  1. 这个GPU上主要跑的就是docker和k3s-agent,在CPU master上执行kubectl apply xxx,master看到yaml里的nodeSeletor写的是gpu的服务器,就把任务下发。
  2. GPU服务器的k3s-agent监听6443端口知道属于自己的任务来了。
  3. GPU让docker去按照yaml里的要求拉取镜像。
  4. 启动任务POD,反向给master同步。

搭建GPU的环境

首先,先确认GPU和CPU的服务器是互通的,互相的ping 和curl都是没问题的。

去下载一个docker,注意调整docker的/etc/docker/daemon.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@gpu-worker-01:/data00/home/chenshuo.007# cat  /etc/docker/daemon.json 
{
"default-runtime": "nvidia",
"data-root": "/data00/docker", # 这样镜像都去到数据盘里,不会放本地盘
"insecure-registries": [
"hub.byted.org",
"hub.byted.org:443"
],
"live-restore": true,
"registry-mirrors": [
"https://docker.m.daocloud.io"
],
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
}
}

然后启动docker 看一下挂载的目录是否正确:

1
2
3
systemctl daemon-reload   
systemctl restart docker
docker info | grep "Docker Root Dir"

执行 nvidia-smi 看一下显卡是否启动了:

然后去cpu master机器上看一下 node-token,默认是在/var/lib/rancher/k3s/server/node-token里,把这个token复制出来,在GPU的服务器里:

1
2
3
4
curl -Lo k3s_install.sh https://rancher-mirror.oss-cn-beijing.aliyuncs.com/k3s/k3s-install.sh
chmod +x k3s_install.sh
INSTALL_K3S_MIRROR=cn K3S_URL=https://master IP:6443 K3S_TOKEN=<node-token> ./k3s_install.sh agent --data-dir /data00/k3s --docker

在gpu的服务器使用sudo journalctl -u k3s-agent -f ,时刻关注日志的情况,如果日志没问题的话,就能在cpu master服务器上,看到2个node ready:

在master上给这个GPU 服务器加一个label:kubectl label node gpu-worker-01 accelerator=nvidia-t4

然后我们要在gpu服务器上启动一个daemonset,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-device-plugin-daemonset
namespace: kube-system
spec:
selector:
matchLabels:
name: nvidia-device-plugin-ds
template:
metadata:
labels:
name: nvidia-device-plugin-ds
spec:
nodeSelector:
accelerator: nvidia-t4 # 这里写明要去GPU服务器
containers:
- image: nvcr.io/nvidia/k8s-device-plugin:v0.18.1 # 这个的链接用国内的链接,不然下载不下来。报错falling to pull image
name: nvidia-device-plugin-ctr
securityContext:
privileged: true
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
EOF

然后我这里发现一个问题,用kubectl get pods -A来看,看到nvidia-device-plugin-XXX是在不断的重启:

这里我们一个一个的看event:

  1. Successfully assigned:调度器(Scheduler)成功地为这个 Pod 在集群中找到了一个合适的家(Node)。
  2. Container image … already present:运行这个 Pod 所需的容器镜像(nvcr.io/nvidia/k8s-device-plugin:v0.18.1)已经在本地存在了,不需要重新下载。
  3. Created container:kubelet 成功地根据镜像创建了容器实例。
  4. Started container:kubelet 成功地执行了启动容器的命令。
  5. Last State: Terminated, Reason: Error, Exit Code: 1:容器在启动后,因为内部程序错误,立刻就退出了,并且返回了一个非零的退出码(1 表示有错误)。
  6. 然后继续重启

遇到这种问题我觉得可以去看看具体的日志了,kubectl logs nvidia-device-plugin-daemonset-r8ht9 -n kube-system --previous , 发现里面报错:

重点看第三句,If this is a GPU node, did you configure the NVIDIA Container Toolkit? 翻译过来就是“如果这是个GPU节点,你配置好NVIDIA容器工具包(NVIDIA Container Toolkit)了吗?”,这就是问题,我的GPU没有NVIDIA Container Toolkit,于是就退出了。

安装方法如下:

1
2
3
4
5
6
7
8
9
10
11
# 在 gpu-worker-01 节点上执行
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=containerd
sudo systemctl restart k3s-agent
kubectl describe node gpu-worker-01 | grep nvidia.com/gpu #通过这个确认状态应该是running了

至此,准备工作终于完成了:

  1. gpu服务器已经顺利加入集群,并且身份是worker.
  2. gpu服务器配置好了label。
  3. GPU服务器安装好了NVIDIA Container Toolkit,nvidia-device-plugin-XXX RS已经能OK了。
  4. GPU服务器启动一个nvidia-device-plugin的ds,然后他会起一个pod,这个pod的存在就是让master知道这个节点是有一个t4 显卡。

测试一下GPU的威力

现在GPU服务器已经就位,那么我们就可以搞一个可以识别图片的ai了,毕竟我们之前那个CPU的大模型只能识别文字,工作中我们还是要扔一些图片进去的。

首先我们启动一个ollama的pod,让它准确的去调度gpu的服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
apiVersion: apps/v1
kind: Deployment
metadata:
name: ollama-gpu
spec:
replicas: 1
selector:
matchLabels:
app: ollama-gpu
template:
metadata:
labels:
app: ollama-gpu
spec:
nodeSelector:
accelerator: nvidia-t4 # 定向投送到 GPU 机器
containers:
- name: ollama
image: ollama/ollama:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 11434
env:
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
- name: NVIDIA_DRIVER_CAPABILITIES
value: "compute,utility"
resources:
limits:
nvidia.com/gpu: 1 # 核心指令:索要显卡
volumeMounts:
- name: ollama-storage
mountPath: /root/.ollama
volumes:
- name: ollama-storage
hostPath:
path: /data00/k3s/ollama-models # 直接挂载 GPU 机器的 500G 大盘路径,也就是说容器里的/root/.ollama 对应的是宿主机上的/data00/k3s/ollama-models
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: ollama-gpu-svc
spec:
selector:
app: ollama-gpu
ports:
- protocol: TCP
port: 11434
targetPort: 11434

启动了之后,就看到有个ollama的pod已经在GPU worker上生成了。但是没有具体的大模型,所以我们选择用Qwen3.5,目前它能力是很强的。

1
2
3
ollama pull qwen3.5:latest   # 然后等待下载完毕
ollama run qwen3.5:latest # 进入命令行模式
>>> who is this man? /root/.ollama/yaoming.jpg # 注意!这里的图片的路径是POD里的路径。

但是这里要确认一下ollama ps,看一下究竟是cpu在运行还是gpu在运行。比如我这里就发现“我的GPU在摸鱼,CPU在干活”:

于是可以先kubectl logs <GPU pod> | grep -i 'CUDA'

这里为空,说明这个GPU POD 启动的时候,没有发现GPU编号,于是在“应该卷GPU的场景里卷CPU”。

我后来查了一下,是因为我tesla T4跑的是450版本的驱动,最高只支持CUDA 10,而qwen 3.5是比较新的,要CUDA 12以上的版本,于是就在GPU上更新一下驱动:

1
2
3
apt-cache search nvidia-driver
apt-get install nvidia-tesla-535-driver -y #安装535驱动
reboot # 重启宿主机,因为我们已经装了新驱动,新的.ko文件已经落盘,但是450模块还在内核里,所以需要重启来卸载酒模块

重启之后,检查一下k3s-agent和docker的状态,再来刷新一下nvidia-smi:

此时再跑一个大模型的识图任务,就看到效果了,GPU开始工作了,而且返回字的速度也明显加快了:

这样才是真正的实现了“GPU通过ai大模型分析图形“的效果:

最后要注意一下,我这里做了图片上传覆盖的逻辑,也就是说如果上传了一个1.jpg,再上传另一幅图片但是也叫1.jpg的话,后者会覆盖前者。这样的好处一个是好管理,节省磁盘空间。

缺点就是:

  1. 缓存幻觉:如果两个用户(或你先后两次)上传了同名但内容不同的图片,后上传的会把前面的冲掉。
  2. 并发冲突:如果 AI 正在读第一个 yaoming.jpg 进行推理,此时第二个同名文件写进来,可能会导致 AI 读取文件流中断或报错。
  3. 历史追溯难:数据库 history 表里记录的图片路径都是同一个,你以后想看“昨天那个姚明”和“今天这个姚明”的区别,就找不到了。
  4. 最重要的一点,如果直接用了os.path.join(文件路径),那么遇到用户上传的文件故意起名叫”../../etc/hosts”,这样很有可能真保存到/etc/hosts里,他猜这个上传的路径就是根路径的第二级,第二级不对就第三极。反正就是破坏你的原文件。

所以建议给文件名加个“时间戳”或者“随机指纹” + secure_filename,secure_filename可以过滤掉危险字符(比如/ 和 .. 这样的路径字段),这样唯一性+安全就都解决了。

感谢你请我喝咖啡~

欢迎关注我的其它发布渠道