Apple silicon 上基于 minikube krunkit 驱动的 AI Playground:本地 GPU 共享与大模型部署实战
【免费下载链接】minikubeRun Kubernetes locally项目地址: https://gitcode.com/gh_mirrors/mi/minikube
本教程以 site/content/en/docs/tutorials/ai-playground.md 为核心脉络,完整演示如何在 Apple silicon(如 MacBook Pro)上借助 minikube 的 krunkit 驱动构建一个本地 AI 实验环境:让 Kubernetes 集群直接共享 Mac 的 GPU,部署 granite 与 tinyllama 两个大语言模型,并通过 Open WebUI 以 OpenAI 兼容 API 进行对话交互。读完本文,你将掌握 krunkit 驱动 + virtio-fs 目录挂载 + generic-device-plugin 设备插件的完整用法,能够快速创建、销毁 GPU 集群而无需重复下载模型。
前置条件
开始之前,请确认环境满足以下要求:
- 一台 Apple silicon Mac(M1/M2/M3/M4 系列,arm64 架构)
- krunkit v1.0.0 或更高版本——负责运行轻量级虚拟机
- vmnet-helper v0.6.0 或更高版本——负责虚拟机网络
- generic-device-plugin——将宿主机设备以扩展资源形式暴露给 Pod
- minikube v1.37.0 或更高版本(krunkit 驱动要求)
关于驱动支持范围,从仓库源码可以确认:krunkit 驱动仅在darwin+arm64平台上注册,状态检查逻辑会直接返回 "the krunkit driver is only supported on macOS arm64 machines"(见 pkg/minikube/registry/drvs/krunkit/krunkit.go)。同时该驱动在注册表中的Priority为registry.Experimental、Default为true(同文件 init 注册段),这解释了为什么minikube start的输出中会明确标注 "Using the krunkit (experimental) driver"。
安装 krunkit 与 vmnet-helper
安装 krunkit
通过 Homebrew 安装最新版 krunkit:
brew tap slp/krunkit brew install krunkit krunkit --version安装 vmnet-helper
vmnet-helper 由 minikube 官方配套维护,使用官方安装脚本安装:
curl -fsSL https://github.com/minikube-machine/vmnet-helper/releases/latest/download/install.sh | bash /opt/vmnet-helper/bin/vmnet-helper --version从驱动源码看,krunkit 驱动运行时由两个进程协作:
krunkit负责虚拟机本体,vmnet-helper负责网络。驱动的GetState()会同时检查两个进程的状态并返回组合结果,Start()的顺序也是先启动vmnet-helper、再启动krunkit(见 pkg/drivers/krunkit/krunkit.go),因此两者缺一不可。
下载模型到宿主机
关键设计思路:把模型文件放在 minikube 之外。这样创建、删除集群都无需重新下载动辄数 GB 的模型权重。
mkdir ~/models cd ~/models curl -LO 'https://huggingface.co/instructlab/granite-7b-lab-GGUF/resolve/main/granite-7b-lab-Q4_K_M.gguf?download=true' curl -LO 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q8_0.gguf?download=true'重要提醒:模型必须为GGUF格式。GGUF 是 llama.cpp 生态的量化模型格式,llama-server需要以该格式加载权重。
启动 minikube 集群
使用 krunkit 驱动启动集群,并通过--mount-string把宿主机的~/models目录挂载到虚拟机内的/mnt/models:
minikube start --driver krunkit --mount-string ~/models:/mnt/models预期输出:
😄 minikube v1.37.0 on Darwin 15.6.1 (arm64) ✨ Using the krunkit (experimental) driver based on user configuration 👍 Starting "minikube" primary control-plane node in "minikube" cluster 🔥 Creating krunkit VM (CPUs=2, Memory=6144MB, Disk=20000MB) ... 🐳 Preparing Kubernetes v1.34.0 on Docker 28.4.0 ... 🔗 Configuring bridge CNI (Container Networking Interface) ... 🔎 Verifying Kubernetes components... ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5 🌟 Enabled addons: storage-provisioner, default-storageclass 🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default--mount-string 的底层实现
--mount-string的官方参数说明是 "Directory to mount in the guest using format '/host-path:/guest-path'."(见 cmd/minikube/cmd/start_flags.go)。对于 krunkit 这类支持 virtiofs 的驱动,挂载字符串会经过 pkg/drivers/common/virtiofs/virtiofs.go 的ValidateMountString解析校验:
- 按
/host-path:/guest-path格式解析并校验路径合法性; - 拒绝重复的宿主机路径或重复的客户机路径(同一路径被多次共享会直接报错);
- 校验通过后转换为
Mount{HostPath, GuestPath, Tag}结构。
随后驱动配置阶段(pkg/minikube/registry/drvs/krunkit/krunkit.go)将挂载列表写入驱动的VirtiofsMounts字段。真正启动虚拟机时,startKrunkit会为每个挂载生成一条virtio-fs设备参数:
--device virtio-fs,sharedDir=<宿主机路径>,mountTag=<挂载标签>(见 pkg/drivers/krunkit/krunkit.go)。同时,由于 krunkit 属于"支持 virtiofs 挂载"的驱动,minikube 不会再额外创建基于 9p 的用户态挂载进程(相关分支见 pkg/minikube/node/config.go),整个挂载链路更干净高效。
验证 GPU 是否可用
krunkit 驱动会把宿主机的 GPU 以virtio-gpu设备形式暴露给虚拟机。通过 SSH 进入虚拟机检查 DRI(Direct Rendering Infrastructure)设备:
% minikube ssh -- tree /dev/dri /dev/dri |-- by-path | |-- platform-a007000.virtio_mmio-card -> ../card0 | `-- platform-a007000.virtio_mmio-render -> ../renderD128 |-- card0 `-- renderD128/dev/dri/card0与/dev/dri/renderD128的存在,说明 GPU 渲染节点已经成功透传进虚拟机,接下来就可以通过设备插件把它暴露给 Kubernetes 的 Pod。
部署 generic-device-plugin
要让 Pod 能使用 GPU,需要部署 generic-device-plugin——它以 DaemonSet 形式在每个节点运行,把宿主机的/dev/dri设备目录注册为扩展资源devic.es/dri:
cat <<'EOF' | kubectl apply -f - apiVersion: apps/v1 kind: DaemonSet metadata: name: generic-device-plugin namespace: kube-system labels: app.kubernetes.io/name: generic-device-plugin spec: selector: matchLabels: app.kubernetes.io/name: generic-device-plugin template: metadata: labels: app.kubernetes.io/name: generic-device-plugin spec: priorityClassName: system-node-critical tolerations: - operator: "Exists" effect: "NoExecute" - operator: "Exists" effect: "NoSchedule" containers: - image: squat/generic-device-plugin args: - --device - | name: dri groups: - count: 4 paths: - path: /dev/dri name: generic-device-plugin resources: requests: cpu: 50m memory: 10Mi limits: cpu: 50m memory: 20Mi ports: - containerPort: 8080 name: http securityContext: privileged: true volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: dev mountPath: /dev volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: dev hostPath: path: /dev updateStrategy: type: RollingUpdate EOF关键配置解读:
groups[0].count: 4:声明该设备组最多可供4 个 Pod 并发使用/dev/dri。若需要更多 Pod 共享 GPU,可调大count;system-node-critical优先级类 + 全容忍(NoExecute/NoSchedule):确保设备插件在节点调度、节点维护等场景下始终随节点运行;- 挂载
hostPath的/var/lib/kubelet/device-plugins与/dev:前者是 kubelet 与设备插件通信的标准 socket 目录,后者用于访问实际设备文件; privileged: true:设备插件需要特权访问底层设备节点。
等待 DaemonSet 就绪:
% kubectl get daemonset generic-device-plugin -n kube-system -w NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE generic-device-plugin 1 1 1 1 1 <none> 45s部署 granite 模型
接下来把下载好的 granite 模型部署为llama-serverPod(使用 Ramalama 镜像,内含 llama-server 可执行文件),并配套一个 ClusterIP Service 供集群内其他 Pod 调用。注意通过resources.limits["devic.es/dri"]声明 GPU 资源:
cat <<'EOF' | kubectl apply -f - --- apiVersion: apps/v1 kind: Deployment metadata: name: granite spec: replicas: 1 selector: matchLabels: app: granite template: metadata: labels: app: granite name: granite spec: containers: - name: llama-server image: quay.io/ramalama/ramalama:latest command: [ llama-server, --host, "0.0.0.0", --port, "8080", --model, /mnt/models/granite-7b-lab-Q4_K_M.gguf, --alias, "ibm/granite:7b", --ctx-size, "2048", --temp, "0.8", --cache-reuse, "256", -ngl, "999", --threads, "6", --no-warmup, --log-colors, auto, ] resources: limits: devic.es/dri: 1 volumeMounts: - name: models mountPath: /mnt/models volumes: - name: models hostPath: path: /mnt/models --- apiVersion: v1 kind: Service metadata: labels: app: granite name: granite spec: ports: - protocol: TCP port: 8080 selector: app: granite EOFllama-server启动参数要点:
| 参数 | 含义 |
|---|---|
--model | 指定 GGUF 模型文件路径(来自挂载的/mnt/models) |
--alias | 模型对外暴露的别名,如ibm/granite:7b,Open WebUI 中以此标识模型 |
--ctx-size 2048 | 上下文窗口大小(token 数) |
--temp 0.8 | 采样温度,控制生成随机性 |
--cache-reuse 256 | 提示词缓存复用窗口 |
-ngl 999 | 将尽可能多的层卸载到 GPU(999 表示不限层数) |
--threads 6 | CPU 线程数 |
--no-warmup | 跳过启动预热 |
--log-colors auto | 日志着色 |
等待 Deployment 就绪并检查 Service:
% kubectl get deploy granite NAME READY UP-TO-DATE AVAILABLE AGE granite 1/1 1 1 8m17s % kubectl get service granite NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE granite ClusterIP 10.105.145.9 <none> 8080/TCP 28m部署 tinyllama 模型
tinyllama 是体积更小(约 1.1B 参数)的模型,部署方式与 granite 完全一致,仅替换模型文件、别名与 GPU 资源配额。这里声明了devic.es/dri: 3,与 granite 的1合计不超过设备插件声明的总数 4,验证了多个模型可同时共享同一个 GPU:
cat <<'EOF' | kubectl apply -f - --- apiVersion: apps/v1 kind: Deployment metadata: name: tinyllama spec: replicas: 1 selector: matchLabels: app: tinyllama template: metadata: labels: app: tinyllama name: tinyllama spec: containers: - name: llama-server image: quay.io/ramalama/ramalama:latest command: [ llama-server, --host, "0.0.0.0", --port, "8080", --model, /mnt/models/tinyllama-1.1b-chat-v1.0.Q8_0.gguf, --alias, tinyllama, --ctx-size, "2048", --temp, "0.8", --cache-reuse, "256", -ngl, "999", --threads, "6", --no-warmup, --log-colors, auto, ] resources: limits: devic.es/dri: 3 volumeMounts: - name: models mountPath: /mnt/models volumes: - name: models hostPath: path: /mnt/models --- apiVersion: v1 kind: Service metadata: labels: app: tinyllama name: tinyllama spec: ports: - protocol: TCP port: 8080 selector: app: tinyllama EOF等待 Deployment 就绪并检查 Service:
% kubectl get deploy tinyllama NAME READY UP-TO-DATE AVAILABLE AGE tinyllama 1/1 1 1 9m14s % kubectl get service tinyllama NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE tinyllama ClusterIP 10.98.219.117 <none> 8080/TCP 23m至此,集群内已存在两个暴露 OpenAI 兼容 API(/v1路径)的模型服务。
部署 Open WebUI
Open WebUI 提供了开箱即用的 Web 界面,用于与 OpenAI 兼容 API(如我们的 llama-server Pod)交互。部署包含三部分:Web UI Deployment、数据持久化 PVC、NodePort Service:
--- cat <<'EOF' | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: open-webui spec: replicas: 1 selector: matchLabels: app: open-webui template: metadata: labels: app: open-webui spec: containers: - name: open-webui image: ghcr.io/open-webui/open-webui:dev-slim ports: - containerPort: 8080 env: # Preconfigure OpenAI-compatible endpoints - name: OPENAI_API_BASE_URLS value: "http://granite:8080/v1;http://tinyllama:8080/v1" volumeMounts: - name: open-webui-data mountPath: /app/backend/data volumes: - name: open-webui-data persistentVolumeClaim: claimName: open-webui-data --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: open-webui-data spec: storageClassName: standard accessModes: - ReadWriteOnce resources: requests: storage: 1Gi --- apiVersion: v1 kind: Service metadata: name: open-webui spec: ports: - protocol: TCP port: 8080 nodePort: 30080 selector: app: open-webui type: NodePort EOF关键配置解读:
OPENAI_API_BASE_URLS:以分号分隔列出两个模型服务的 OpenAI 兼容 API 地址。http://granite:8080/v1与http://tinyllama:8080/v1直接使用上一节创建的 ClusterIP Service 域名,Kubernetes 内置 DNS 会自动完成解析;- PVC
open-webui-data:使用默认的standardStorageClass(minikube 启动时默认启用的default-storageclassaddon 提供),将聊天记录、用户数据持久化在 1Gi 卷中,Pod 重建后数据不丢失; - Service 类型为 NodePort,宿主机
30080端口对外暴露,便于从浏览器访问。
等待 Deployment 就绪:
% kubectl get deploy open-webui NAME READY UP-TO-DATE AVAILABLE AGE open-webui 1/1 1 1 69s与模型对话
在浏览器中打开 Open WebUI 控制台:
open $(minikube service open-webui --url)首次访问需要创建 Admin 账号,之后即可开始使用。
与 granite 模型对话
在模型菜单(左上角)中选择 "ibm/granite:7b" 模型开始对话。输入一个提示词,例如:
> Write a very technical haiku about playing with large language models with Minikube on Apple silicon Mighty model, Minikube, Silicon-powered speed, Learning's dance, ever-changing. Through data streams it weaves, Inference's wisdom, vast and deep, Apple's heartbeat, in code, resounds. Exploring AI's vast frontier, Minikube, language model's playground, Innovation's rhythm, forever.granite 会基于模型权重实时生成回答,所有推理均在本地 GPU 上进行。
与 tinyllama 模型对话
点击左侧 "New Chat" 按钮,在左上角模型菜单中选择 "tinyllama" 模型,然后输入提示词:
> How do you feel inside this fancy Minikube cluster? I do not have a physical body. However, based on the given text material, the author is describing feeling inside a cluster of Minikube, a type of jellyfish. The use of the word "fancy" suggests that the author is impressed or appreciates the intricate design of the cluster, while the adjective "minikube" connotes its smooth texture, delicate shape, and iridescent colors. The word "cluster" suggests a group of these jellyfish, while "inside" implies being in the vicinity or enclosed within.两个模型可以并行使用:granite 用于较重的中型任务,tinyllama 适合低延迟的轻量对话,二者共享同一块 Mac GPU。
更进一步:理解 krunkit 驱动的运行机制
至此已跑通完整链路,下面从仓库源码补充几个理解要点,方便排查问题或自行扩展:
1. 双进程架构与状态管理。krunkit 驱动由krunkit(虚拟机)与vmnet-helper(网络)两个进程构成。GetState()先检查 krunkit 进程(通过krunkit.pid文件),再检查 vmnet-helper,任一异常都会反映到minikube status中(pkg/drivers/krunkit/krunkit.go)。运行时产物(krunkit.log、serial.log、krunkit.sock等)都写入机器目录,遇到启动异常可查看这些日志。
2. 设备透传由 krunkit 命令行参数决定。startKrunkit构造的启动参数中,除了 virtio-fs 挂载外,还包括内存、CPU、virtio-net(通过 unixgram socket 连接 vmnet-helper)、virtio-serial串口日志、以及两块virtio-blk磁盘(ISO 与数据盘)(pkg/drivers/krunkit/krunkit.go)。GPU 透传即由 krunkit 本身对 Apple silicon 的支持提供,暴露为文档中验证到的 virtio-gpu 设备。
3. 设备插件数量与 GPU 配额的对应关系。generic-device-plugin 中count: 4定义了资源总量,Pod 的devic.es/drilimits 则是每次调度占用的份额。本教程中 granite 占 1、tinyllama 占 3,正好用满配额;若需部署更多模型,需同步上调count。
4. 模型目录隔离带来的运维收益。由于模型在宿主机~/models、通过 virtio-fs 只读共享进虚拟机,minikube delete后重建集群无需重新下载模型;即便更换驱动或升级 minikube,模型资产也始终独立保留。
常见注意事项
- 版本对齐:krunkit 驱动需要 minikube v1.37.0+,krunkit v1.0.0+,vmnet-helper v0.6.0+,低于这些版本可能导致驱动不可用或网络异常;
- 平台限制:krunkit 驱动仅支持 macOS arm64,Intel Mac 请改用其他驱动(如 Docker、QEMU、VirtualBox),GPU 共享能力也随之不同;
- 模型格式:务必使用 GGUF 格式模型文件,llama-server 无法加载其他格式的权重;
- 资源配额:若 Pod 一直处于 Pending 状态,优先检查
kubectl describe pod中是否有 "Insufficient devic.es/dri" 类事件,并按需调整设备插件的count。
通过以上步骤,你已经在 Apple silicon 上拥有了一个完全本地、可随时重建的 Kubernetes AI 实验环境——这也是 minikube 官方教程给出的推荐玩法:把"模型资产"与"集群生命周期"彻底解耦,让 GPU 集群的实验成本降到最低。
【免费下载链接】minikubeRun Kubernetes locally项目地址: https://gitcode.com/gh_mirrors/mi/minikube
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考