背景
实验室有一台 GPU 服务器(RTX 3080 + RTX 5060 Ti,共两张卡),之前多个人要用 GPU 只能排队——一个人独占整张卡,其他人等着。这种粗放模式显然效率太低,尤其是跑 Jupyter Notebook 做实验的时候,大部分时间 GPU 根本跑不满。
目标很明确:
- 一台物理机,两张消费级 GPU
- 多个用户同时使用 JupyterHub 做 PyTorch 开发
- 每个用户分到 4GB 显存 + 25% GPU 算力
- 一张卡最多切 5 个 vGPU,总共支撑 10 个用户并发
- 能可视化监控 GPU 使用情况
最终效果:一个 Helm values 文件 + 一套自动化安装脚本,在 K3s 上把 Z2JH、HAMi、HAMi-WebUI 全栈跑起来。
架构总览
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
| ┌──────────────────────────────────────────────────────────────┐ │ Ubuntu 24.04 LTS │ │ K3s (轻量 Kubernetes) │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ HAMi — GPU 虚拟化中间件 │ │ │ │ ├─ hami-scheduler # 自定义 GPU 感知调度器 │ │ │ │ ├─ hami-device-plugin # 注册 vGPU 资源到 K8s │ │ │ │ └─ webhook # 自动注入 GPU 环境变量 │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Z2JH (JupyterHub) │ │ │ │ ├─ Hub # 用户认证 & 管理 │ │ │ │ ├─ Proxy # NodePort │ │ │ │ └─ User Pods # PyTorch CUDA Notebook │ │ │ │ # 每个 Pod: 4GB显存 / 25%算力 │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ HAMi-WebUI (魔改版) │ │ │ │ ├─ 前端: Vue.js / NodePort │ │ │ │ ├─ 后端: Go (Kratos) │ │ │ │ └─ Prometheus + DCGM-Exporter 监控 │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ GPU: RTX 3080 + RTX 5060 Ti │ └──────────────────────────────────────────────────────────────┘
|
核心技术选型
K3s 替代标准 K8s
标准 K8s 太重了——kube-apiserver、etcd、controller-manager、scheduler 一堆组件,单机跑起来吃掉好几 GB 内存。我们是单节点场景,用 K3s 完美替代:
- 二进制只有 ~70MB,内存占用不到 500MB
- 自带 containerd、Flannel CNI
- 完全兼容标准 K8s API,Helm 直接可用
- 禁用了 traefik ingress,用 NodePort 直出端口更简单
HAMi 做 GPU 虚拟化
HAMi(异构算力管理中间件,原 vGPU-device-plugin)是开源的 GPU 共享调度方案,核心能力:
| 功能 |
说明 |
| 显存切分 |
一张物理 GPU 按 MB 级别切分,deviceSplitCount: 10 最多切 10 份 |
| 算力共享 |
deviceCoreSharing: 1,按比例分配 GPU SM 算力 |
| 调度器 |
自定义 scheduler + admission webhook,binpack 策略优先塞满同一张卡 |
| 多厂商 |
除了 NVIDIA,还支持华为 Ascend、海光 DCU、寒武纪 MLU 等 |
用户 Pod 只需声明三个资源:
1 2 3 4 5
| resources: limits: nvidia.com/gpu: "1" nvidia.com/gpumem: "4000" nvidia.com/gpucores: "25"
|
HAMi webhook 会自动注入 NVIDIA 可见设备环境变量,scheduler 把 Pod 调度到有足够剩余资源的 GPU 切片上。
Z2JH 做用户界面
JupyterHub 是学术界最流行的多用户 Notebook 方案,Z2JH 是它的 Helm Chart。关键配置:
1 2 3 4 5 6 7 8 9 10
| singleuser: extraResource: limits: nvidia.com/gpumem: "4000" nvidia.com/gpucores: "25" extraPodConfig: runtimeClassName: nvidia scheduling: userScheduler: enabled: false
|
认证插件使用 Dummy Authenticator,适合实验室内网环境。
魔改 HAMi-WebUI
原生的 HAMi-WebUI 有个 bug:GPU 使用率永远显示为 0。
Bug 根因
HAMi scheduler 在 Pod annotation 里编码 GPU 分配信息时,是按所有容器(包括 init containers)的顺序写入的。但 WebUI 解码时只遍历了 pod.Spec.Containers,忽略了 init containers,导致索引错位——应用容器拿到的永远是 init container(block-cloud-metadata)的空设备记录。
1 2 3 4 5
| Scheduler 写入 annotation: [init-container设备(空)] ; [主容器设备(GPU-xxx,NVIDIA,4000,25)]
WebUI 解码 (修复前): i=0 → pod.Spec.Containers[0] → 错误地取了 init-container 的空记录 ❌
|
修复方案
修了三个文件:
1. util.go — 解码时考虑 init containers
1 2 3 4 5
| totalContainers := len(pod.Spec.Containers)
totalContainers := len(pod.Spec.InitContainers) + len(pod.Spec.Containers)
|
2. pod.go — 分配 device 索引时跳过 init containers
1 2 3 4 5 6 7 8 9
| c.ContainerDevices = bizContainerDevices[i]
initContainerCount := len(pod.Spec.InitContainers) deviceIdx := initContainerCount + i if deviceIdx < len(bizContainerDevices) { c.ContainerDevices = bizContainerDevices[deviceIdx] }
|
3. node.go — 添加 nil 防护
当 POST 请求不带 filters 时 req.Filters 为 nil,会触发 panic。加上判空:
1 2 3 4
| if filters != nil { if filters.Ip != "" && filters.Ip != nodeReply.Ip { continue } }
|
修复后 WebUI 正确显示各 GPU 使用率、显存和算力占用。
部署流程
整套部署压缩到三个脚本里:
1 2 3 4 5 6 7 8
| ./install-z2jh.sh
./status-z2jh.sh
./uninstall-z2jh.sh
|
HAMi 和 HAMi-WebUI 通过 Helm 单独部署:
1 2
| helm install hami HAMi/charts/hami -n kube-system helm install hami-webui HAMi-WebUI/charts/hami-webui -n kube-system
|
访问方式
| 服务 |
地址 |
说明 |
| JupyterHub |
http://<服务器IP>:31080 |
按配置认证登录 |
| HAMi-WebUI |
http://<服务器IP>:32361 |
GPU 资源监控面板 |
资源分配示意
1 2 3 4 5 6 7 8
| RTX 3080 (10GB) RTX 5060 Ti (16GB) ┌─────────────────┐ ┌─────────────────┐ │ vGPU 1: 4GB 25% │ │ vGPU 6: 4GB 25% │ │ vGPU 2: 4GB 25% │ │ vGPU 7: 4GB 25% │ │ vGPU 3: 2GB 25% │ │ vGPU 8: 4GB 25% │ │ │ │ vGPU 9: 4GB 25% │ └─────────────────┘ └─────────────────┘ 5 个 vGPU 切片 5 个 vGPU 切片
|
每个 JupyterHub 用户 Pod 自动分配到一个 vGPU 切片,跑 PyTorch 和独占一张卡体验完全一致——torch.cuda.is_available() 返回 true,nvidia-smi 能看到 GPU,只不过显存上限是 4GB。
总结
这套方案的核心价值:
- 消费级 GPU 也能虚拟化——不需要 Tesla/V100 这类数据中心卡
- 资源利用率大幅提升——从一人占整卡到十人共享
- 开箱即用——K3s 单机一条脚本拉起全栈
- 可观测——WebUI 实时看 GPU 占用,Prometheus 留存历史数据
- 易维护——Helm 统一管理,卸载一条命令清理干净
适合实验室、小团队、教学机房等预算有限但需要多用户 GPU 环境的场景。