diff --git a/i18n/zh/docusaurus-plugin-content-docs-tutorials/current/labs/hami-kitops.md b/i18n/zh/docusaurus-plugin-content-docs-tutorials/current/labs/hami-kitops.md new file mode 100644 index 000000000..f3efab087 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs-tutorials/current/labs/hami-kitops.md @@ -0,0 +1,781 @@ +--- +title: "实验 12:在 HAMi 上使用 KitOps ModelKit 提供模型服务" +description: "将模型打包为 KitOps ModelKit,通过 initContainer 从 Jozu Hub 拉取,并使用 SGLang(可选 vLLM)在 HAMi GPU 共享资源上从本地提供服务。" +sidebar_label: "实验 12:KitOps ModelKit 推理" +lab: + level: Advanced + duration: 约 60 分钟 + environment: 配备 NVIDIA GPU 的 Kubernetes 集群 + authors: + - rudrakshkarpe + - shivaylamba + verified: "2026-07-23" +tags: + - kitops + - sglang + - vllm + - inference + - modelkit +toc_max_heading_level: 2 +--- + +本实验演示如何将模型打包为 **[KitOps](https://kitops.org/) ModelKit**(一种带版本的 OCI 制品),使用 KitOps `initContainer` 从 OCI 注册表(示例中使用 **[Jozu Hub](https://jozu.ml)**)下载到 Pod,然后通过 [SGLang](https://github.com/sgl-project/sglang)(主要示例)或 vLLM(可选的共置示例)从**本地目录**在 HAMi 虚拟化 GPU 共享资源上提供服务。 + +与[实验 6(vLLM)](./hami-vllm)和实验 11(SGLang)一样,推理引擎运行在 HAMi 资源上。本实验采用注册表原生的**模型供应链**:模型被打包为 ModelKit,在 Jozu Hub 上进行版本管理和存储,以 OCI 制品的形式拉取到 Pod,并从本地路径提供服务。 + +## 学习目标 + +- 检查 OCI 注册表中的公共 KitOps ModelKit +- 构建精简的 `kitunpacker` 初始化镜像和自定义 SGLang 服务镜像 +- 部署一个通过 `initContainer` 拉取并解包 ModelKit 的 Pod,主容器从 KitOps 提供的卷中加载模型 +- 使用 HAMi 的 `nvidia.com/gpu`、`gpumem` 和 `gpucores` 调度工作负载 +- 通过兼容 OpenAI 的 SGLang API 验证推理 +- 可选地在同一物理 GPU 上共置一个使用相同 ModelKit 的 vLLM Pod + +## 实验概览 + +```mermaid +%% title: HAMi + KitOps ModelKit Lab Flowchart +flowchart LR + Step1["Step 1
Prereqs + HAMi"] --> Step2["Step 2
Inspect ModelKit"] + Step2 --> Step3["Step 3
Build Images"] + Step3 --> Step4["Step 4
Deploy SGLang+ModelKit"] + Step4 --> Step5["Step 5
Watch Unpack"] + Step5 --> Step6["Step 6
Test Inference"] + Step6 --> Step7["Step 7
Verify HAMi Caps"] + Step7 --> Step8["Step 8
Optional vLLM"] + Step8 --> Step9["Step 9
Cleanup"] +``` + +## 部署架构 + +```mermaid +%% title: KitOps ModelKit delivery on HAMi +flowchart TB + Registry["OCI Registry
Jozu Hub ModelKit"] -->|"kit unpack --filter model"| Init["initContainer
hami-kitunpacker"] + Init --> Vol["emptyDir /models"] + Vol --> SGL["SGLang container
serve /models/qwen3"] + Vol --> VLLM["Optional vLLM container
serve /models/qwen3"] + + subgraph Pod["Pod scheduled by hami-scheduler"] + Init + SGL + Vol + end + + HAMI["HAMi
gpumem + gpucores caps"] --> Pod + GPU["Physical NVIDIA GPU"] --> HAMI +``` + +## 前置条件 + +- 实验 11(或实验 6)的全部前置条件:配备 NVIDIA GPU 的 Kubernetes 集群、正常运行的 HAMi,以及 `kubectl` 和 `helm` +- Docker(或等效的构建工具),用于构建镜像并将其加载到集群 +- 工作站上的 [`kit`](https://github.com/jozu-ai/kitops) CLI(可选,但建议用于 `kit inspect`) +- 能够从公共注册表 `jozu.ml` 拉取制品(示例 ModelKit 无需登录) + +本实验假设已经按照实验 11 安装 HAMi。若尚未安装,请先完成实验 11 的步骤 1 至 3。 + +## 示例集群状态 + +验证使用与实验 11 相同的 kind + H100 集群。HAMi 公布 10 个 vGPU,`hami-scheduler` 和 `hami-device-plugin` 均处于 Running 状态。 + +本实验使用以下公共 ModelKit: + +```plaintext +jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 +``` + +## 步骤 1:确认 HAMi 已就绪 + +```bash +kubectl get pods -n kube-system -l app.kubernetes.io/instance=hami -o wide +kubectl get nodes -o 'custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu' +``` + +预期结果:设备插件和调度器处于 Running 状态,GPU 节点显示可分配的 `nvidia.com/gpu`(例如 `10`)。 + +检查当前使用 GPU 共享资源的工作负载,并缩容本实验不需要的工作负载。下面示例中的 ModelKit 和 4B 模型需要 30 GiB 的 HAMi 显存共享资源。 + +```bash +kubectl get pods --all-namespaces \ + -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,GPU-MEM:.spec.containers[*].resources.limits.nvidia\.com/gpumem' +``` + +## 步骤 2:检查 ModelKit + +在安装了 `kit` CLI 的计算机上运行: + +```bash +kit inspect --remote jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 +``` + +验证环境中的输出示例(已截断): + +```json +{ + "digest": "sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19", + "kitfile": { + "package": { "name": "Qwen3-4B-Instruct-2507", "version": "1.0" }, + "model": { + "name": "qwen3-4b-instruct", + "path": "qwen3-4b-instruct/model", + "license": "Apache 2.0" + } + }, + "manifest": { + "artifactType": "application/vnd.kitops.modelkit.manifest.v1+json" + } +} +``` + +ModelKit 将 safetensors 权重、分词器和配置作为 OCI 层保存。initContainer 会将它们解包并整理为扁平的模型目录(`config.json` + `*.safetensors`),供 SGLang 或 vLLM 从本地加载。 + +## 步骤 3:构建流水线镜像 + +创建工作目录和以下文件。 + +### 3.1 `kitunpacker` 初始化镜像 + +`kitunpacker/Dockerfile`: + +```dockerfile +FROM alpine:3.20 + +ARG KITOPS_VERSION=v1.11.0 + +RUN apk add --no-cache bash coreutils findutils ca-certificates curl tar \ + && curl -fsSL "https://github.com/kitops-ml/kitops/releases/download/${KITOPS_VERSION}/kitops-linux-x86_64.tar.gz" -o /tmp/kit.tgz \ + && tar -xzf /tmp/kit.tgz -C /usr/local/bin kit \ + && rm -f /tmp/kit.tgz \ + && kit version + +ENV MODELKIT_REF="jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19" \ + UNPACK_PATH="/models" \ + MODEL_SUBDIR="qwen3" + +COPY unpack.sh /usr/local/bin/unpack.sh +RUN chmod +x /usr/local/bin/unpack.sh + +ENTRYPOINT ["/usr/local/bin/unpack.sh"] +``` + +`kitunpacker/unpack.sh`: + +```sh +#!/usr/bin/env sh +# kitunpacker: pull a ModelKit from an OCI registry (Jozu Hub by default) and +# unpack the model into a flat directory (config.json + *.safetensors) that +# vLLM / SGLang can load directly from local disk. +# +# Env (all overridable from the Pod spec): +# MODELKIT_REF full ModelKit reference, e.g. jozu.ml//: +# UNPACK_PATH root volume to unpack into (default /models) +# MODEL_SUBDIR final model dir under UNPACK_PATH (default qwen3) +# REGISTRY_URL/USERNAME/PASSWORD optional creds for PRIVATE registries +set -eu + +MODELKIT_REF="${MODELKIT_REF:?MODELKIT_REF is required}" +UNPACK_PATH="${UNPACK_PATH:-/models}" +MODEL_SUBDIR="${MODEL_SUBDIR:-qwen3}" +DEST="${UNPACK_PATH}/${MODEL_SUBDIR}" +RAW="${UNPACK_PATH}/.raw-${MODEL_SUBDIR}" +LOCK="${UNPACK_PATH}/.lock-${MODEL_SUBDIR}" + +# keep the kit pull cache on the (large) mounted volume, not the tiny rootfs +export KITOPS_HOME="${UNPACK_PATH}/.kitcache" + +ready() { [ -f "${DEST}/config.json" ] && ls "${DEST}"/*.safetensors >/dev/null 2>&1; } + +echo "[kitunpacker] ref=${MODELKIT_REF} -> ${DEST}" + +if ready; then + echo "[kitunpacker] model already present, skipping unpack" + exit 0 +fi + +# This lock only coordinates Pods when they mount the same shared PVC. With +# the emptyDir used in this lab, every Pod has an isolated volume and lock. +# mkdir is atomic, so Pods sharing a PVC do not race to write the same files. +if ! mkdir "${LOCK}" 2>/dev/null; then + echo "[kitunpacker] another unpack in progress, waiting for it to finish..." + i=0 + while [ "${i}" -lt 360 ]; do + ready && { echo "[kitunpacker] model became ready"; exit 0; } + i=$((i + 1)); sleep 5 + done + echo "[kitunpacker] timed out waiting for peer unpack" >&2 + exit 1 +fi +# shellcheck disable=SC2064 +trap "rmdir '${LOCK}' 2>/dev/null || true" EXIT INT TERM + +# optional login for private registries (public Jozu Hub needs none) +if [ -n "${REGISTRY_URL:-}" ] && [ -n "${USERNAME:-}" ] && [ -n "${PASSWORD:-}" ]; then + echo "[kitunpacker] logging in to ${REGISTRY_URL} as ${USERNAME}" + echo "${PASSWORD}" | kit login "${REGISTRY_URL}" -u "${USERNAME}" --password-stdin +fi + +rm -rf "${RAW}"; mkdir -p "${RAW}" +echo "[kitunpacker] pulling + unpacking model layers from registry..." +kit unpack --filter model "${MODELKIT_REF}" -d "${RAW}" + +# Flatten: ModelKits may store the .safetensors shards in a model/ subdir while +# config.json / *.index.json / tokenizer sit one level up. vLLM/transformers +# need them all in one directory, so collect everything into DEST. +SRC_CFG="$(find "${RAW}" -name config.json | head -1)" +[ -n "${SRC_CFG}" ] || { echo "[kitunpacker] config.json not found after unpack" >&2; exit 1; } +SRC="$(dirname "${SRC_CFG}")" + +mkdir -p "${DEST}" +# all weight shards, wherever they live under the unpacked tree +find "${SRC}" -name '*.safetensors' -exec mv -f {} "${DEST}/" \; +# all top-level metadata files (config, index, tokenizer, vocab, generation cfg) +find "${SRC}" -maxdepth 1 -type f -exec mv -f {} "${DEST}/" \; + +rm -rf "${RAW}" "${KITOPS_HOME}" + +echo "[kitunpacker] final model directory:" +ls -la "${DEST}" +ready || { echo "[kitunpacker] validation failed: missing config or shards" >&2; exit 1; } +echo "[kitunpacker] done." + +``` + +构建镜像: + +```bash +docker build -t hami-kitunpacker:latest ./kitunpacker +``` + +### 3.2 自定义 SGLang 镜像(从本地模型路径提供服务) + +`sglang/Dockerfile`: + +```dockerfile +FROM lmsysorg/sglang:v0.5.7 + +ENV MODEL_DIR="/models/qwen3" \ + SERVED_NAME="qwen3-4b-instruct" \ + CONTEXT_LEN="8192" \ + MEM_FRACTION="0.8" \ + PORT="30000" \ + ATTENTION_BACKEND="triton" + +COPY serve.sh /usr/local/bin/serve.sh +RUN chmod +x /usr/local/bin/serve.sh + +ENTRYPOINT ["/usr/local/bin/serve.sh"] +``` + +`sglang/serve.sh`: + +```bash +#!/usr/bin/env bash +# Custom SGLang entrypoint: serve a model unpacked from a KitOps ModelKit that +# the kitunpacker initContainer placed on a shared volume. Serves from a LOCAL +# directory (--model-path) delivered straight from the Jozu Hub ModelKit. +set -euo pipefail + +MODEL_DIR="${MODEL_DIR:-/models/qwen3}" +SERVED_NAME="${SERVED_NAME:-qwen3-4b-instruct}" +CONTEXT_LEN="${CONTEXT_LEN:-8192}" +MEM_FRACTION="${MEM_FRACTION:-0.8}" +PORT="${PORT:-30000}" +ATTENTION_BACKEND="${ATTENTION_BACKEND:-triton}" + +echo "[sglang-jozu] serving KitOps model from ${MODEL_DIR} (source: Jozu Hub ModelKit)" +if [ ! -f "${MODEL_DIR}/config.json" ]; then + echo "[sglang-jozu] ERROR: ${MODEL_DIR}/config.json not found -- did the kitunpacker init run?" >&2 + exit 1 +fi + +exec python3 -m sglang.launch_server \ + --model-path "${MODEL_DIR}" \ + --served-model-name "${SERVED_NAME}" \ + --host 0.0.0.0 \ + --port "${PORT}" \ + --mem-fraction-static "${MEM_FRACTION}" \ + --context-length "${CONTEXT_LEN}" \ + --attention-backend "${ATTENTION_BACKEND}" + +``` + +构建镜像: + +```bash +docker build -t hami-sglang-jozu:latest ./sglang +``` + +### 3.3 将镜像加载到集群 + +对于 kind: + +```bash +kind load docker-image hami-kitunpacker:latest --name +kind load docker-image hami-sglang-jozu:latest --name +``` + +对于其他集群,请将镜像推送到节点可以访问的注册表,并相应更新 Deployment 中的镜像字段。 + +### 3.4 可选:自定义 vLLM 镜像(用于步骤 8) + +`vllm/Dockerfile`: + +```dockerfile +FROM vllm/vllm-openai:v0.23.0 + +ENV MODEL_DIR="/models/qwen3" \ + SERVED_NAME="qwen3-4b-instruct" \ + MAX_MODEL_LEN="8192" \ + GPU_MEM_UTIL="0.85" \ + PORT="8000" + +COPY serve.sh /usr/local/bin/serve.sh +RUN chmod +x /usr/local/bin/serve.sh + +ENTRYPOINT ["/usr/local/bin/serve.sh"] +``` + +`vllm/serve.sh`: + +```bash +#!/usr/bin/env bash +# Custom vLLM entrypoint: serve a model unpacked from a KitOps ModelKit that the +# kitunpacker initContainer placed on a shared volume. It serves from a LOCAL +# directory (--model-path) populated from the Jozu Hub ModelKit. +set -euo pipefail + +MODEL_DIR="${MODEL_DIR:-/models/qwen3}" +SERVED_NAME="${SERVED_NAME:-qwen3-4b-instruct}" +MAX_MODEL_LEN="${MAX_MODEL_LEN:-8192}" +GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.85}" +PORT="${PORT:-8000}" + +echo "[vllm-jozu] serving KitOps model from ${MODEL_DIR} (source: Jozu Hub ModelKit)" +if [ ! -f "${MODEL_DIR}/config.json" ]; then + echo "[vllm-jozu] ERROR: ${MODEL_DIR}/config.json not found -- did the kitunpacker init run?" >&2 + exit 1 +fi + +exec vllm serve "${MODEL_DIR}" \ + --served-model-name "${SERVED_NAME}" \ + --max-model-len "${MAX_MODEL_LEN}" \ + --gpu-memory-utilization "${GPU_MEM_UTIL}" \ + --host 0.0.0.0 \ + --port "${PORT}" + +``` + +```bash +docker build -t hami-vllm-jozu:latest ./vllm +# kind load docker-image hami-vllm-jozu:latest --name +``` + +## 步骤 4:部署使用 ModelKit 的 SGLang 服务 + +该 Deployment 使用以下组件: + +1. `initContainer: kitops-init`,将 ModelKit 拉取并整理到 `/models/qwen3` +2. 主容器 `hami-sglang-jozu`,从该本地目录提供服务 +3. HAMi 调度器以及 `gpumem` 和 `gpucores` 限制 +4. 用于模型卷的 `emptyDir`(便于移植,生产环境请使用 PVC) + +模型层约为 7.5 GiB。解包期间,模型同时存在于 `KITOPS_HOME` 和暂存目录中,因此峰值用量约为 15 GiB。请将示例卷保持为 20 GiB 或更大。 + +```bash +kubectl apply -f - <<'EOF' +apiVersion: v1 +kind: Namespace +metadata: + name: kitops +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sglang-modelkit + namespace: kitops + labels: + app.kubernetes.io/name: sglang-modelkit +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: sglang-modelkit + template: + metadata: + labels: + app.kubernetes.io/name: sglang-modelkit + annotations: + hami.io/node-scheduler-policy: binpack + hami.io/gpu-scheduler-policy: binpack + spec: + schedulerName: hami-scheduler + initContainers: + - name: kitops-init + image: hami-kitunpacker:latest + imagePullPolicy: IfNotPresent + env: + - name: MODELKIT_REF + value: "jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19" + - name: UNPACK_PATH + value: "/models" + - name: MODEL_SUBDIR + value: "qwen3" + volumeMounts: + - name: modelkit + mountPath: /models + containers: + - name: sglang + image: hami-sglang-jozu:latest + imagePullPolicy: IfNotPresent + env: + - name: MODEL_DIR + value: "/models/qwen3" + - name: SERVED_NAME + value: "qwen3-4b-instruct" + - name: CONTEXT_LEN + value: "8192" + - name: MEM_FRACTION + value: "0.8" + ports: + - name: http + containerPort: 30000 + resources: + requests: + cpu: "2" + memory: 8Gi + nvidia.com/gpu: "1" + nvidia.com/gpumem: "30000" + nvidia.com/gpucores: "30" + limits: + cpu: "8" + memory: 32Gi + nvidia.com/gpu: "1" + nvidia.com/gpumem: "30000" + nvidia.com/gpucores: "30" + readinessProbe: + httpGet: + path: /health + port: 30000 + initialDelaySeconds: 40 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 90 + volumeMounts: + - name: modelkit + mountPath: /models + - name: dshm + mountPath: /dev/shm + volumes: + - name: modelkit + emptyDir: + sizeLimit: 20Gi + - name: dshm + emptyDir: + medium: Memory + sizeLimit: 8Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: sglang-modelkit + namespace: kitops +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: sglang-modelkit + ports: + - name: http + port: 8001 + targetPort: http +EOF +``` + +对于**私有**注册表,请通过 Secret 向 `kitops-init` 添加 `REGISTRY_URL`、`USERNAME` 和 `PASSWORD` 环境变量。`unpack.sh` 会在拉取前运行 `kit login`。 + +## 步骤 5:观察 ModelKit 解包过程 + +```bash +kubectl -n kitops get pods -w +kubectl -n kitops logs -l app.kubernetes.io/name=sglang-modelkit -c kitops-init -f +``` + +成功解包时输出如下: + +```plaintext +[kitunpacker] ref=jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 -> /models/qwen3 +[kitunpacker] pulling + unpacking model layers from registry... +Unpacking to /models/.raw-qwen3 +... +[kitunpacker] final model directory: +... config.json ... model-00001-of-00003.safetensors ... tokenizer.json ... +[kitunpacker] done. +``` + +然后等待 SGLang 容器就绪: + +```bash +kubectl -n kitops rollout status deploy/sglang-modelkit --timeout=30m +kubectl -n kitops logs -l app.kubernetes.io/name=sglang-modelkit -c sglang --tail=50 +``` + +自定义入口点会输出以下消息,确认模型从 ModelKit 的**本地**路径提供服务: + +```plaintext +[sglang-jozu] serving KitOps model from /models/qwen3 (source: Jozu Hub ModelKit) +... model_path='/models/qwen3' ... served_model_name='qwen3-4b-instruct' ... +``` + +## 步骤 6:测试推理 + +```bash +kubectl -n kitops port-forward svc/sglang-modelkit 8001:8001 +``` + +在另一个终端中运行: + +```bash +curl -s http://127.0.0.1:8001/v1/models | python3 -m json.tool +``` + +输出示例: + +```json +{ + "object": "list", + "data": [ + { + "id": "qwen3-4b-instruct", + "object": "model", + "owned_by": "sglang", + "max_model_len": 8192 + } + ] +} +``` + +聊天补全: + +```bash +curl -s http://127.0.0.1:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "qwen3-4b-instruct", + "messages": [ + {"role": "user", "content": "In one sentence, what is a KitOps ModelKit?"} + ], + "max_tokens": 96, + "temperature": 0.2 + }' | python3 -m json.tool +``` + +如果存在 `choices[0].message.content`,则说明 ModelKit 到本地 SGLang 的推理流程正常工作。 + +## 步骤 7:验证 HAMi 资源限制 + +```bash +POD=$(kubectl get pod -n kitops -l app.kubernetes.io/name=sglang-modelkit -o jsonpath='{.items[0].metadata.name}') +kubectl get pod -n kitops ${POD} \ + -o jsonpath='{.spec.schedulerName}{"\n"}{.spec.containers[0].resources.limits}{"\n"}' +kubectl exec -n kitops ${POD} -c sglang -- env | grep -E 'CUDA_DEVICE|NVIDIA_VISIBLE' +kubectl exec -n kitops ${POD} -c sglang -- nvidia-smi +``` + +验证集群中的结果: + +```plaintext +hami-scheduler +... nvidia.com/gpumem:30000 nvidia.com/gpucores:30 ... + +NVIDIA_VISIBLE_DEVICES=GPU-... +CUDA_DEVICE_MEMORY_LIMIT_0=30000m +CUDA_DEVICE_SM_LIMIT=30 + +| NVIDIA H100 80GB HBM3 ... | 24745MiB / 30000MiB | +``` + +主容器从 `/models/qwen3`(OCI ModelKit)加载权重,同时 HAMi 在共享 H100 上仍将 Pod 内显存上限限制为 **30000 MiB**。 + +## 步骤 8(可选):使用相同 ModelKit 模式共置 vLLM + +构建并加载 `hami-vllm-jozu:latest` 后,部署使用独立 HAMi 共享资源的第二个引擎。如果希望两个 Pod 复用同一个已解包的 ModelKit,请使用 **PVC**(或节点本地缓存)。使用 `emptyDir` 时,每个 Pod 都会独立解包。 + +```bash +kubectl apply -f - <<'EOF' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vllm-modelkit + namespace: kitops + labels: + app.kubernetes.io/name: vllm-modelkit +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: vllm-modelkit + template: + metadata: + labels: + app.kubernetes.io/name: vllm-modelkit + annotations: + hami.io/node-scheduler-policy: binpack + hami.io/gpu-scheduler-policy: binpack + spec: + schedulerName: hami-scheduler + initContainers: + - name: kitops-init + image: hami-kitunpacker:latest + imagePullPolicy: IfNotPresent + env: + - name: MODELKIT_REF + value: "jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19" + - name: UNPACK_PATH + value: "/models" + - name: MODEL_SUBDIR + value: "qwen3" + volumeMounts: + - name: modelkit + mountPath: /models + containers: + - name: vllm + image: hami-vllm-jozu:latest + imagePullPolicy: IfNotPresent + env: + - name: MODEL_DIR + value: "/models/qwen3" + - name: SERVED_NAME + value: "qwen3-4b-instruct" + ports: + - name: http + containerPort: 8000 + resources: + limits: + cpu: "8" + memory: 32Gi + nvidia.com/gpu: "1" + nvidia.com/gpumem: "30000" + nvidia.com/gpucores: "30" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 40 + periodSeconds: 10 + failureThreshold: 90 + volumeMounts: + - name: modelkit + mountPath: /models + - name: dshm + mountPath: /dev/shm + volumes: + - name: modelkit + emptyDir: + sizeLimit: 20Gi + - name: dshm + emptyDir: + medium: Memory + sizeLimit: 8Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: vllm-modelkit + namespace: kitops +spec: + selector: + app.kubernetes.io/name: vllm-modelkit + ports: + - name: http + port: 8000 + targetPort: http +EOF +``` + +使用以下命令测试: + +```bash +kubectl -n kitops port-forward svc/vllm-modelkit 8000:8000 +curl -s http://127.0.0.1:8000/v1/models +``` + +确保合计的 `gpumem` 请求能够容纳在物理 GPU 上。例如,两个 30000 MiB 的请求需要该 GPU 至少有 60 GiB 可用显存。 + +## 参考:Kitfile(重新打包自己的 ModelKit) + +```yaml +# Reference Kitfile for the Qwen3-4B-Instruct ModelKit used in this demo. +# +# The demo PULLS a pre-built public ModelKit from Jozu Hub: +# jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 +# +# This Kitfile is provided so you can (re)pack and push your OWN ModelKit to a +# registry (Jozu Hub, ACR, GHCR, ...) from a local model directory (config.json + safetensors): +# +# # 1) get a model directory (e.g. via `kit unpack` or `kit import`) +# # 2) place this Kitfile next to a ./qwen3 directory of safetensors + config +# kit pack . -t jozu.ml//qwen3-4b-instruct:latest +# kit login jozu.ml -u --password-stdin # needed only for push +# kit push jozu.ml//qwen3-4b-instruct:latest +manifestVersion: "1.0" +package: + name: qwen3-4b-instruct + version: "1.0" + authors: + - HAMi KubeCon Demo + description: > + Qwen3-4B-Instruct-2507 packaged as a KitOps ModelKit (safetensors layout), served on HAMi-virtualized GPUs by vLLM and SGLang. + + +model: + name: qwen3-4b-instruct + path: ./qwen3 + license: Apache-2.0 + description: Qwen3 4B instruct, safetensors (Qwen3ForCausalLM) +``` + +```bash +# After placing a safetensors-layout model directory at ./qwen3 next to the Kitfile: +kit pack . -t jozu.ml//qwen3-4b-instruct:latest +kit push jozu.ml//qwen3-4b-instruct:latest +``` + +然后将 Deployment 中的 `MODELKIT_REF` 指向你的标签。 + +## 故障排查 + +| 现象 | 检查内容 | +| --- | --- | +| initContainer 一直处于拉取状态 | 检查节点到注册表的连通性以及 `emptyDir` 的磁盘压力,必要时增大 `sizeLimit`。 | +| `config.json not found after unpack` | ModelKit 布局不同。使用 `kit inspect --remote` 检查,并调整整理逻辑或 `MODEL_SUBDIR`。 | +| SGLang 退出并提示模型目录缺失 | initContainer 失败。查看 `kubectl logs ... -c kitops-init`。 | +| 自定义镜像出现 ImagePullBackOff | 使用 `kind load` 或推送到你的注册表。本地标签请设置 `imagePullPolicy: IfNotPresent`。 | +| GPU Pod 处于 Pending 状态 | 释放 HAMi 共享资源、降低 `gpumem`,并检查 `hami-scheduler` 事件。 | +| 私有注册表返回 401 | 在 `kitops-init` 上设置 `REGISTRY_URL`、`USERNAME` 和 `PASSWORD`。 | +| Pod 内仍显示完整 GPU 显存 | 与实验 11 相同,请检查 HAMi 环境变量和 `schedulerName`。 | + +## 清理 + +```bash +kubectl delete namespace kitops --ignore-not-found +# optional: remove local images +# docker rmi hami-kitunpacker:latest hami-sglang-jozu:latest hami-vllm-jozu:latest +``` + +## 验证结果 + +| 验证项 | 证据 | +| --- | --- | +| 模型是 OCI ModelKit | `kit inspect --remote` 返回 KitOps 清单和模型层。 | +| 模型从 ModelKit 交付到主容器 | initContainer 日志显示 `kit unpack`,SGLang 日志显示 `serving KitOps model from /models/qwen3` 和 `model_path='/models/qwen3'`。 | +| HAMi 调度工作负载 | `schedulerName: hami-scheduler` 以及 Filtering 和 Binding 事件。 | +| GPU 显存和计算限制生效 | `CUDA_DEVICE_MEMORY_LIMIT_0=30000m`、`CUDA_DEVICE_SM_LIMIT=30`,Pod 内的 `nvidia-smi` 显示 `... / 30000MiB`。 | +| 推理正常工作 | `/v1/models` 列出 `qwen3-4b-instruct`,聊天补全返回内容。 | + +## 后续步骤 + +- 将公共 Jozu ModelKit 换成内部注册表中的 ModelKit,并配置 imagePullSecrets 或 `kit login` Secret。 +- 在 SGLang 和 vLLM 之间共享一个 PVC,使 ModelKit 只需解包一次。 +- 结合[实验 3:GPU 切分](./gpu-partitioning),在每个 GPU 上容纳更多租户。 +- 返回实验 11:SGLang,了解引擎在启动时直接拉取模型的简化方式。该方式适合在不涉及供应链的情况下独立调试推理引擎。 diff --git a/sidebars-tutorials.js b/sidebars-tutorials.js index e3881dc71..9e199c7f6 100644 --- a/sidebars-tutorials.js +++ b/sidebars-tutorials.js @@ -61,6 +61,11 @@ module.exports = { id: "labs/topology-aware-scheduling", customProps: { level: "Intermediate", duration: "about 45 minutes" }, }, + { + type: "doc", + id: "labs/hami-kitops", + customProps: { level: "Advanced", duration: "about 60 minutes" }, + }, ], }, ], diff --git a/tutorials/labs/hami-kitops.md b/tutorials/labs/hami-kitops.md new file mode 100644 index 000000000..9df1b7c03 --- /dev/null +++ b/tutorials/labs/hami-kitops.md @@ -0,0 +1,781 @@ +--- +title: "Lab 12: Serve Models from a KitOps ModelKit on HAMi" +description: "Package a model as a KitOps ModelKit, pull it from Jozu Hub with an initContainer, and serve it locally with SGLang (and optionally vLLM) on HAMi GPU shares." +sidebar_label: "Lab 12: KitOps ModelKit Inference" +lab: + level: Advanced + duration: about 60 minutes + environment: Kubernetes cluster with NVIDIA GPUs + authors: + - rudrakshkarpe + - shivaylamba + verified: "2026-07-23" +tags: + - kitops + - sglang + - vllm + - inference + - modelkit +toc_max_heading_level: 2 +--- + +This lab demonstrates how to package a model as a **[KitOps](https://kitops.org/) ModelKit**, a versioned OCI artifact, and download it from an OCI registry (**[Jozu Hub](https://jozu.ml)** in the examples) into the Pod using a KitOps `initContainer`, then serve it from a **local directory** with [SGLang](https://github.com/sgl-project/sglang) (primary) or vLLM (optional co-resident example) on HAMi-virtualized GPU shares. + +Like [Lab 6 (vLLM)](./hami-vllm) and Lab 11 (SGLang), the inference engines run on HAMi resources. Here the **model supply chain** is registry-native: the model is packaged as a ModelKit, versioned and stored on Jozu Hub, pulled into the Pod as an OCI artifact, and served from a local path. + +## Learning Objectives + +- Inspect a public KitOps ModelKit on an OCI registry +- Build a small `kitunpacker` init image and a custom SGLang serve image +- Deploy a Pod that pulls/unpacks a ModelKit via `initContainer` (the main container loads the model from the KitOps-delivered volume) +- Schedule the workload with HAMi `nvidia.com/gpu` / `gpumem` / `gpucores` +- Prove inference works against the OpenAI-compatible SGLang API +- Optionally co-locate a vLLM Pod serving the same ModelKit on the same physical GPU + +## Lab Overview + +```mermaid +%% title: HAMi + KitOps ModelKit Lab Flowchart +flowchart LR + Step1["Step 1
Prereqs + HAMi"] --> Step2["Step 2
Inspect ModelKit"] + Step2 --> Step3["Step 3
Build Images"] + Step3 --> Step4["Step 4
Deploy SGLang+ModelKit"] + Step4 --> Step5["Step 5
Watch Unpack"] + Step5 --> Step6["Step 6
Test Inference"] + Step6 --> Step7["Step 7
Verify HAMi Caps"] + Step7 --> Step8["Step 8
Optional vLLM"] + Step8 --> Step9["Step 9
Cleanup"] +``` + +## Deployment Architecture + +```mermaid +%% title: KitOps ModelKit delivery on HAMi +flowchart TB + Registry["OCI Registry
Jozu Hub ModelKit"] -->|"kit unpack --filter model"| Init["initContainer
hami-kitunpacker"] + Init --> Vol["emptyDir /models"] + Vol --> SGL["SGLang container
serve /models/qwen3"] + Vol --> VLLM["Optional vLLM container
serve /models/qwen3"] + + subgraph Pod["Pod scheduled by hami-scheduler"] + Init + SGL + Vol + end + + HAMI["HAMi
gpumem + gpucores caps"] --> Pod + GPU["Physical NVIDIA GPU"] --> HAMI +``` + +## Prerequisites + +- Everything from Lab 11 (or Lab 6): a Kubernetes cluster with NVIDIA GPUs, HAMi installed and healthy, `kubectl`, `helm` +- Docker (or an equivalent builder) to build and load images into the cluster +- [`kit`](https://github.com/jozu-ai/kitops) CLI on your workstation (optional but recommended for `kit inspect`) +- Ability to pull from the public registry `jozu.ml` (no login required for the sample ModelKit) + +This lab assumes HAMi is already installed as in Lab 11. If not, complete Lab 11 Steps 1–3 first. + +## Example Cluster State + +Verification used the same kind + H100 cluster as Lab 11, with HAMi advertising 10 vGPUs and both `hami-scheduler` / `hami-device-plugin` Running. + +Public ModelKit used throughout: + +```plaintext +jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 +``` + +## Step 1: Confirm HAMi Is Ready + +```bash +kubectl get pods -n kube-system -l app.kubernetes.io/instance=hami -o wide +kubectl get nodes -o 'custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu' +``` + +Expected: device plugin and scheduler Running; GPU nodes show allocatable `nvidia.com/gpu` (for example `10`). + +Check the workloads currently using GPU shares and scale down any workloads that are not needed for this lab. The ModelKit and 4B model need a 30 GiB HAMi memory share in the example below. + +```bash +kubectl get pods --all-namespaces \ + -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,GPU-MEM:.spec.containers[*].resources.limits.nvidia\.com/gpumem' +``` + +## Step 2: Inspect the ModelKit + +On a machine with the `kit` CLI: + +```bash +kit inspect --remote jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 +``` + +Example (truncated) output from the verification environment: + +```json +{ + "digest": "sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19", + "kitfile": { + "package": { "name": "Qwen3-4B-Instruct-2507", "version": "1.0" }, + "model": { + "name": "qwen3-4b-instruct", + "path": "qwen3-4b-instruct/model", + "license": "Apache 2.0" + } + }, + "manifest": { + "artifactType": "application/vnd.kitops.modelkit.manifest.v1+json" + } +} +``` + +The ModelKit carries safetensors weights plus tokenizer/config as OCI layers. The initContainer will unpack and flatten them into a flat model directory (`config.json` + `*.safetensors`) that SGLang/vLLM can load locally. + +## Step 3: Build the Pipeline Images + +Create a working directory and the files below. + +### 3.1 `kitunpacker` init image + +`kitunpacker/Dockerfile`: + +```dockerfile +FROM alpine:3.20 + +ARG KITOPS_VERSION=v1.11.0 + +RUN apk add --no-cache bash coreutils findutils ca-certificates curl tar \ + && curl -fsSL "https://github.com/kitops-ml/kitops/releases/download/${KITOPS_VERSION}/kitops-linux-x86_64.tar.gz" -o /tmp/kit.tgz \ + && tar -xzf /tmp/kit.tgz -C /usr/local/bin kit \ + && rm -f /tmp/kit.tgz \ + && kit version + +ENV MODELKIT_REF="jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19" \ + UNPACK_PATH="/models" \ + MODEL_SUBDIR="qwen3" + +COPY unpack.sh /usr/local/bin/unpack.sh +RUN chmod +x /usr/local/bin/unpack.sh + +ENTRYPOINT ["/usr/local/bin/unpack.sh"] +``` + +`kitunpacker/unpack.sh`: + +```sh +#!/usr/bin/env sh +# kitunpacker: pull a ModelKit from an OCI registry (Jozu Hub by default) and +# unpack the model into a flat directory (config.json + *.safetensors) that +# vLLM / SGLang can load directly from local disk. +# +# Env (all overridable from the Pod spec): +# MODELKIT_REF full ModelKit reference, e.g. jozu.ml//: +# UNPACK_PATH root volume to unpack into (default /models) +# MODEL_SUBDIR final model dir under UNPACK_PATH (default qwen3) +# REGISTRY_URL/USERNAME/PASSWORD optional creds for PRIVATE registries +set -eu + +MODELKIT_REF="${MODELKIT_REF:?MODELKIT_REF is required}" +UNPACK_PATH="${UNPACK_PATH:-/models}" +MODEL_SUBDIR="${MODEL_SUBDIR:-qwen3}" +DEST="${UNPACK_PATH}/${MODEL_SUBDIR}" +RAW="${UNPACK_PATH}/.raw-${MODEL_SUBDIR}" +LOCK="${UNPACK_PATH}/.lock-${MODEL_SUBDIR}" + +# keep the kit pull cache on the (large) mounted volume, not the tiny rootfs +export KITOPS_HOME="${UNPACK_PATH}/.kitcache" + +ready() { [ -f "${DEST}/config.json" ] && ls "${DEST}"/*.safetensors >/dev/null 2>&1; } + +echo "[kitunpacker] ref=${MODELKIT_REF} -> ${DEST}" + +if ready; then + echo "[kitunpacker] model already present, skipping unpack" + exit 0 +fi + +# This lock only coordinates Pods when they mount the same shared PVC. With +# the emptyDir used in this lab, every Pod has an isolated volume and lock. +# mkdir is atomic, so Pods sharing a PVC do not race to write the same files. +if ! mkdir "${LOCK}" 2>/dev/null; then + echo "[kitunpacker] another unpack in progress, waiting for it to finish..." + i=0 + while [ "${i}" -lt 360 ]; do + ready && { echo "[kitunpacker] model became ready"; exit 0; } + i=$((i + 1)); sleep 5 + done + echo "[kitunpacker] timed out waiting for peer unpack" >&2 + exit 1 +fi +# shellcheck disable=SC2064 +trap "rmdir '${LOCK}' 2>/dev/null || true" EXIT INT TERM + +# optional login for private registries (public Jozu Hub needs none) +if [ -n "${REGISTRY_URL:-}" ] && [ -n "${USERNAME:-}" ] && [ -n "${PASSWORD:-}" ]; then + echo "[kitunpacker] logging in to ${REGISTRY_URL} as ${USERNAME}" + echo "${PASSWORD}" | kit login "${REGISTRY_URL}" -u "${USERNAME}" --password-stdin +fi + +rm -rf "${RAW}"; mkdir -p "${RAW}" +echo "[kitunpacker] pulling + unpacking model layers from registry..." +kit unpack --filter model "${MODELKIT_REF}" -d "${RAW}" + +# Flatten: ModelKits may store the .safetensors shards in a model/ subdir while +# config.json / *.index.json / tokenizer sit one level up. vLLM/transformers +# need them all in one directory, so collect everything into DEST. +SRC_CFG="$(find "${RAW}" -name config.json | head -1)" +[ -n "${SRC_CFG}" ] || { echo "[kitunpacker] config.json not found after unpack" >&2; exit 1; } +SRC="$(dirname "${SRC_CFG}")" + +mkdir -p "${DEST}" +# all weight shards, wherever they live under the unpacked tree +find "${SRC}" -name '*.safetensors' -exec mv -f {} "${DEST}/" \; +# all top-level metadata files (config, index, tokenizer, vocab, generation cfg) +find "${SRC}" -maxdepth 1 -type f -exec mv -f {} "${DEST}/" \; + +rm -rf "${RAW}" "${KITOPS_HOME}" + +echo "[kitunpacker] final model directory:" +ls -la "${DEST}" +ready || { echo "[kitunpacker] validation failed: missing config or shards" >&2; exit 1; } +echo "[kitunpacker] done." + +``` + +Build: + +```bash +docker build -t hami-kitunpacker:latest ./kitunpacker +``` + +### 3.2 Custom SGLang image (serves a local model path) + +`sglang/Dockerfile`: + +```dockerfile +FROM lmsysorg/sglang:v0.5.7 + +ENV MODEL_DIR="/models/qwen3" \ + SERVED_NAME="qwen3-4b-instruct" \ + CONTEXT_LEN="8192" \ + MEM_FRACTION="0.8" \ + PORT="30000" \ + ATTENTION_BACKEND="triton" + +COPY serve.sh /usr/local/bin/serve.sh +RUN chmod +x /usr/local/bin/serve.sh + +ENTRYPOINT ["/usr/local/bin/serve.sh"] +``` + +`sglang/serve.sh`: + +```bash +#!/usr/bin/env bash +# Custom SGLang entrypoint: serve a model unpacked from a KitOps ModelKit that +# the kitunpacker initContainer placed on a shared volume. Serves from a LOCAL +# directory (--model-path) delivered straight from the Jozu Hub ModelKit. +set -euo pipefail + +MODEL_DIR="${MODEL_DIR:-/models/qwen3}" +SERVED_NAME="${SERVED_NAME:-qwen3-4b-instruct}" +CONTEXT_LEN="${CONTEXT_LEN:-8192}" +MEM_FRACTION="${MEM_FRACTION:-0.8}" +PORT="${PORT:-30000}" +ATTENTION_BACKEND="${ATTENTION_BACKEND:-triton}" + +echo "[sglang-jozu] serving KitOps model from ${MODEL_DIR} (source: Jozu Hub ModelKit)" +if [ ! -f "${MODEL_DIR}/config.json" ]; then + echo "[sglang-jozu] ERROR: ${MODEL_DIR}/config.json not found -- did the kitunpacker init run?" >&2 + exit 1 +fi + +exec python3 -m sglang.launch_server \ + --model-path "${MODEL_DIR}" \ + --served-model-name "${SERVED_NAME}" \ + --host 0.0.0.0 \ + --port "${PORT}" \ + --mem-fraction-static "${MEM_FRACTION}" \ + --context-length "${CONTEXT_LEN}" \ + --attention-backend "${ATTENTION_BACKEND}" + +``` + +Build: + +```bash +docker build -t hami-sglang-jozu:latest ./sglang +``` + +### 3.3 Load images into the cluster + +For kind: + +```bash +kind load docker-image hami-kitunpacker:latest --name +kind load docker-image hami-sglang-jozu:latest --name +``` + +For other clusters, push the images to a registry your nodes can pull and update the Deployment image fields accordingly. + +### 3.4 Optional: custom vLLM image (for Step 8) + +`vllm/Dockerfile`: + +```dockerfile +FROM vllm/vllm-openai:v0.23.0 + +ENV MODEL_DIR="/models/qwen3" \ + SERVED_NAME="qwen3-4b-instruct" \ + MAX_MODEL_LEN="8192" \ + GPU_MEM_UTIL="0.85" \ + PORT="8000" + +COPY serve.sh /usr/local/bin/serve.sh +RUN chmod +x /usr/local/bin/serve.sh + +ENTRYPOINT ["/usr/local/bin/serve.sh"] +``` + +`vllm/serve.sh`: + +```bash +#!/usr/bin/env bash +# Custom vLLM entrypoint: serve a model unpacked from a KitOps ModelKit that the +# kitunpacker initContainer placed on a shared volume. It serves from a LOCAL +# directory (--model-path) populated from the Jozu Hub ModelKit. +set -euo pipefail + +MODEL_DIR="${MODEL_DIR:-/models/qwen3}" +SERVED_NAME="${SERVED_NAME:-qwen3-4b-instruct}" +MAX_MODEL_LEN="${MAX_MODEL_LEN:-8192}" +GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.85}" +PORT="${PORT:-8000}" + +echo "[vllm-jozu] serving KitOps model from ${MODEL_DIR} (source: Jozu Hub ModelKit)" +if [ ! -f "${MODEL_DIR}/config.json" ]; then + echo "[vllm-jozu] ERROR: ${MODEL_DIR}/config.json not found -- did the kitunpacker init run?" >&2 + exit 1 +fi + +exec vllm serve "${MODEL_DIR}" \ + --served-model-name "${SERVED_NAME}" \ + --max-model-len "${MAX_MODEL_LEN}" \ + --gpu-memory-utilization "${GPU_MEM_UTIL}" \ + --host 0.0.0.0 \ + --port "${PORT}" + +``` + +```bash +docker build -t hami-vllm-jozu:latest ./vllm +# kind load docker-image hami-vllm-jozu:latest --name +``` + +## Step 4: Deploy SGLang Serving the ModelKit + +The Deployment uses: + +1. `initContainer: kitops-init` — pulls and flattens the ModelKit into `/models/qwen3` +2. main container `hami-sglang-jozu` — serves that local directory +3. HAMi scheduler + `gpumem` / `gpucores` caps +4. `emptyDir` for the model volume (portable; use a PVC in production) + +The model layer is about 7.5 GiB. During unpacking, it exists in both `KITOPS_HOME` and the staging directory, so peak usage is about 15 GiB. Keep the example volume at 20 GiB or larger. + +```bash +kubectl apply -f - <<'EOF' +apiVersion: v1 +kind: Namespace +metadata: + name: kitops +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sglang-modelkit + namespace: kitops + labels: + app.kubernetes.io/name: sglang-modelkit +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: sglang-modelkit + template: + metadata: + labels: + app.kubernetes.io/name: sglang-modelkit + annotations: + hami.io/node-scheduler-policy: binpack + hami.io/gpu-scheduler-policy: binpack + spec: + schedulerName: hami-scheduler + initContainers: + - name: kitops-init + image: hami-kitunpacker:latest + imagePullPolicy: IfNotPresent + env: + - name: MODELKIT_REF + value: "jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19" + - name: UNPACK_PATH + value: "/models" + - name: MODEL_SUBDIR + value: "qwen3" + volumeMounts: + - name: modelkit + mountPath: /models + containers: + - name: sglang + image: hami-sglang-jozu:latest + imagePullPolicy: IfNotPresent + env: + - name: MODEL_DIR + value: "/models/qwen3" + - name: SERVED_NAME + value: "qwen3-4b-instruct" + - name: CONTEXT_LEN + value: "8192" + - name: MEM_FRACTION + value: "0.8" + ports: + - name: http + containerPort: 30000 + resources: + requests: + cpu: "2" + memory: 8Gi + nvidia.com/gpu: "1" + nvidia.com/gpumem: "30000" + nvidia.com/gpucores: "30" + limits: + cpu: "8" + memory: 32Gi + nvidia.com/gpu: "1" + nvidia.com/gpumem: "30000" + nvidia.com/gpucores: "30" + readinessProbe: + httpGet: + path: /health + port: 30000 + initialDelaySeconds: 40 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 90 + volumeMounts: + - name: modelkit + mountPath: /models + - name: dshm + mountPath: /dev/shm + volumes: + - name: modelkit + emptyDir: + sizeLimit: 20Gi + - name: dshm + emptyDir: + medium: Memory + sizeLimit: 8Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: sglang-modelkit + namespace: kitops +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: sglang-modelkit + ports: + - name: http + port: 8001 + targetPort: http +EOF +``` + +For a **private** registry, add `REGISTRY_URL`, `USERNAME`, and `PASSWORD` env vars to `kitops-init` (from a Secret). `unpack.sh` will run `kit login` before pulling. + +## Step 5: Watch the ModelKit Unpack + +```bash +kubectl -n kitops get pods -w +kubectl -n kitops logs -l app.kubernetes.io/name=sglang-modelkit -c kitops-init -f +``` + +Successful unpack looks like: + +```plaintext +[kitunpacker] ref=jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 -> /models/qwen3 +[kitunpacker] pulling + unpacking model layers from registry... +Unpacking to /models/.raw-qwen3 +... +[kitunpacker] final model directory: +... config.json ... model-00001-of-00003.safetensors ... tokenizer.json ... +[kitunpacker] done. +``` + +Then wait for the SGLang container: + +```bash +kubectl -n kitops rollout status deploy/sglang-modelkit --timeout=30m +kubectl -n kitops logs -l app.kubernetes.io/name=sglang-modelkit -c sglang --tail=50 +``` + +You should see the custom entrypoint message confirming the model is served from the **local** ModelKit path: + +```plaintext +[sglang-jozu] serving KitOps model from /models/qwen3 (source: Jozu Hub ModelKit) +... model_path='/models/qwen3' ... served_model_name='qwen3-4b-instruct' ... +``` + +## Step 6: Test Inference + +```bash +kubectl -n kitops port-forward svc/sglang-modelkit 8001:8001 +``` + +In another terminal: + +```bash +curl -s http://127.0.0.1:8001/v1/models | python3 -m json.tool +``` + +Example: + +```json +{ + "object": "list", + "data": [ + { + "id": "qwen3-4b-instruct", + "object": "model", + "owned_by": "sglang", + "max_model_len": 8192 + } + ] +} +``` + +Chat completion: + +```bash +curl -s http://127.0.0.1:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "qwen3-4b-instruct", + "messages": [ + {"role": "user", "content": "In one sentence, what is a KitOps ModelKit?"} + ], + "max_tokens": 96, + "temperature": 0.2 + }' | python3 -m json.tool +``` + +If `choices[0].message.content` is present, ModelKit → local SGLang inference is working. + +## Step 7: Verify HAMi Caps + +```bash +POD=$(kubectl get pod -n kitops -l app.kubernetes.io/name=sglang-modelkit -o jsonpath='{.items[0].metadata.name}') +kubectl get pod -n kitops ${POD} \ + -o jsonpath='{.spec.schedulerName}{"\n"}{.spec.containers[0].resources.limits}{"\n"}' +kubectl exec -n kitops ${POD} -c sglang -- env | grep -E 'CUDA_DEVICE|NVIDIA_VISIBLE' +kubectl exec -n kitops ${POD} -c sglang -- nvidia-smi +``` + +Verification cluster evidence: + +```plaintext +hami-scheduler +... nvidia.com/gpumem:30000 nvidia.com/gpucores:30 ... + +NVIDIA_VISIBLE_DEVICES=GPU-... +CUDA_DEVICE_MEMORY_LIMIT_0=30000m +CUDA_DEVICE_SM_LIMIT=30 + +| NVIDIA H100 80GB HBM3 ... | 24745MiB / 30000MiB | +``` + +The main container loaded weights from `/models/qwen3` (OCI ModelKit), while HAMi still enforced a **30000 MiB** in-pod memory ceiling on the shared H100. + +## Step 8 (Optional): Co-locate vLLM on the Same ModelKit Pattern + +After building/loading `hami-vllm-jozu:latest`, deploy a second engine with its own HAMi slice. Use a **PVC** (or node-local cache) if you want both Pods to reuse one unpacked ModelKit; with `emptyDir` each Pod unpacks independently. + +```bash +kubectl apply -f - <<'EOF' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vllm-modelkit + namespace: kitops + labels: + app.kubernetes.io/name: vllm-modelkit +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: vllm-modelkit + template: + metadata: + labels: + app.kubernetes.io/name: vllm-modelkit + annotations: + hami.io/node-scheduler-policy: binpack + hami.io/gpu-scheduler-policy: binpack + spec: + schedulerName: hami-scheduler + initContainers: + - name: kitops-init + image: hami-kitunpacker:latest + imagePullPolicy: IfNotPresent + env: + - name: MODELKIT_REF + value: "jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19" + - name: UNPACK_PATH + value: "/models" + - name: MODEL_SUBDIR + value: "qwen3" + volumeMounts: + - name: modelkit + mountPath: /models + containers: + - name: vllm + image: hami-vllm-jozu:latest + imagePullPolicy: IfNotPresent + env: + - name: MODEL_DIR + value: "/models/qwen3" + - name: SERVED_NAME + value: "qwen3-4b-instruct" + ports: + - name: http + containerPort: 8000 + resources: + limits: + cpu: "8" + memory: 32Gi + nvidia.com/gpu: "1" + nvidia.com/gpumem: "30000" + nvidia.com/gpucores: "30" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 40 + periodSeconds: 10 + failureThreshold: 90 + volumeMounts: + - name: modelkit + mountPath: /models + - name: dshm + mountPath: /dev/shm + volumes: + - name: modelkit + emptyDir: + sizeLimit: 20Gi + - name: dshm + emptyDir: + medium: Memory + sizeLimit: 8Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: vllm-modelkit + namespace: kitops +spec: + selector: + app.kubernetes.io/name: vllm-modelkit + ports: + - name: http + port: 8000 + targetPort: http +EOF +``` + +Test with: + +```bash +kubectl -n kitops port-forward svc/vllm-modelkit 8000:8000 +curl -s http://127.0.0.1:8000/v1/models +``` + +Ensure combined `gpumem` requests fit on the physical GPU (for example two × 30000 MiB needs ≥ 60 GiB free on that card). + +## Reference: Kitfile (repack your own ModelKit) + +```yaml +# Reference Kitfile for the Qwen3-4B-Instruct ModelKit used in this demo. +# +# The demo PULLS a pre-built public ModelKit from Jozu Hub: +# jozu.ml/jonathangamer202002/qwen3-4b-instruct@sha256:df4629f6a10bba7bec45e12bd15f910ed1024699bfbb44b63240899f71bb1c19 +# +# This Kitfile is provided so you can (re)pack and push your OWN ModelKit to a +# registry (Jozu Hub, ACR, GHCR, ...) from a local model directory (config.json + safetensors): +# +# # 1) get a model directory (e.g. via `kit unpack` or `kit import`) +# # 2) place this Kitfile next to a ./qwen3 directory of safetensors + config +# kit pack . -t jozu.ml//qwen3-4b-instruct:latest +# kit login jozu.ml -u --password-stdin # needed only for push +# kit push jozu.ml//qwen3-4b-instruct:latest +manifestVersion: "1.0" +package: + name: qwen3-4b-instruct + version: "1.0" + authors: + - HAMi KubeCon Demo + description: > + Qwen3-4B-Instruct-2507 packaged as a KitOps ModelKit (safetensors layout), served on HAMi-virtualized GPUs by vLLM and SGLang. + + +model: + name: qwen3-4b-instruct + path: ./qwen3 + license: Apache-2.0 + description: Qwen3 4B instruct, safetensors (Qwen3ForCausalLM) +``` + +```bash +# After placing a safetensors-layout model directory at ./qwen3 next to the Kitfile: +kit pack . -t jozu.ml//qwen3-4b-instruct:latest +kit push jozu.ml//qwen3-4b-instruct:latest +``` + +Then point `MODELKIT_REF` in the Deployment at your tag. + +## Troubleshooting + +| Symptom | What to Check | +| --- | --- | +| initContainer stuck pulling | Registry reachability from the node; disk pressure on `emptyDir`; increase `sizeLimit`. | +| `config.json not found after unpack` | ModelKit layout differs; inspect with `kit inspect --remote` and adjust flatten logic / `MODEL_SUBDIR`. | +| SGLang exits: model dir missing | initContainer failed; `kubectl logs ... -c kitops-init`. | +| ImagePullBackOff for custom images | `kind load` / push to your registry; set `imagePullPolicy: IfNotPresent` for local tags. | +| Pod Pending on GPU | Free HAMi shares; lower `gpumem`; confirm `hami-scheduler` events. | +| Private registry 401 | Set `REGISTRY_URL` / `USERNAME` / `PASSWORD` on `kitops-init`. | +| In-pod memory still full GPU size | Same as Lab 11 — verify HAMi env vars and `schedulerName`. | + +## Cleanup + +```bash +kubectl delete namespace kitops --ignore-not-found +# optional: remove local images +# docker rmi hami-kitunpacker:latest hami-sglang-jozu:latest hami-vllm-jozu:latest +``` + +## Verification Results + +| Claim | Evidence | +| --- | --- | +| Model is an OCI ModelKit | `kit inspect --remote` returns KitOps manifest / model layers. | +| Model delivered from the ModelKit into the main container | initContainer logs show `kit unpack`; SGLang logs show `serving KitOps model from /models/qwen3` and `model_path='/models/qwen3'`. | +| HAMi schedules the workload | `schedulerName: hami-scheduler` + Filtering/Binding events. | +| GPU memory/compute caps apply | `CUDA_DEVICE_MEMORY_LIMIT_0=30000m`, `CUDA_DEVICE_SM_LIMIT=30`; in-pod `nvidia-smi` shows `... / 30000MiB`. | +| Inference works | `/v1/models` lists `qwen3-4b-instruct`; chat completions return content. | + +## Next Steps + +- Swap the public Jozu ModelKit for your internal registry ModelKit and wire imagePullSecrets / `kit login` Secrets. +- Share one PVC across SGLang and vLLM so the ModelKit is unpacked once. +- Combine with [Lab 3: GPU Partitioning](./gpu-partitioning) to pack more tenants per GPU. +- Return to Lab 11: SGLang for the simpler path where the engine pulls the model directly at startup, useful for debugging engines independently of the supply chain.