Qwen2.5-1.5B部署教程:Kubernetes StatefulSet部署Qwen2.5-1.5B服务集群

1. 为什么需要在Kubernetes中部署Qwen2.5-1.5B?

你可能已经试过本地运行Qwen2.5-1.5B——启动快、响应顺、不联网、隐私强。但当团队多人需要同时使用,或者你想把它集成进内部知识库、客服系统、自动化工作流时,单机Streamlit服务就显得力不从心了:端口冲突、显存争抢、无健康检查、无法自动扩缩容、重启后对话状态丢失……这些问题不是“能不能用”,而是“能不能稳、能不能管、能不能扩”。

本教程不讲怎么在笔记本上跑通一个demo,而是带你把Qwen2.5-1.5B真正变成一个生产级AI服务

  • 支持多实例并行推理,负载自动分发
  • 每个实例独占GPU资源,避免显存踩踏
  • 对话状态与模型加载分离,重启不丢上下文(通过StatefulSet特性保障)
  • 内置就绪探针(readinessProbe)和存活探针(livenessProbe),K8s自动剔除异常Pod
  • 镜像预置全部依赖,无需每次拉取模型文件,启动即服务
  • 全YAML声明式管理,可GitOps化、可CI/CD集成

这不是“把本地脚本扔进容器”,而是一套面向工程落地的轻量大模型服务编排方案——专为1.5B这类显存友好型模型设计,兼顾效率、稳定与可维护性。

2. 部署前准备:环境与资源确认

2.1 硬件与集群要求

项目 最低要求 推荐配置 说明
GPU NVIDIA T4(16GB显存)×1 A10(24GB)或L4(24GB)×1 Qwen2.5-1.5B-Instruct在bfloat16下约占用9.2GB显存,预留缓冲空间
CPU 4核 8核 用于模型加载、tokenization、HTTP服务调度
内存 16GB 32GB 模型权重+缓存+系统开销
存储 5GB空闲空间(模型+镜像) SSD挂载持久卷(PV) 模型文件需持久化,避免每次Pod重建重复下载

注意:本方案不依赖NVIDIA GPU Operator,仅需集群已安装nvidia-device-pluginkubectl get nodes -o wide中节点OS-IMAGE列显示UbuntuCentOS等主流发行版即可。

2.2 软件依赖清单

  • Kubernetes v1.24+(支持StatefulSet原生特性)
  • kubectl 已配置访问集群权限
  • helm(可选,用于后续扩展如Prometheus监控)
  • Docker或containerd(用于构建镜像)
  • git(克隆示例仓库)

2.3 模型文件准备(关键!)

Qwen2.5-1.5B-Instruct官方模型需提前下载并组织为标准Hugging Face格式:

# 在任意有网络的机器上执行(推荐使用hf-mirror加速)
mkdir -p /tmp/qwen2.5-1.5b-instruct
cd /tmp/qwen2.5-1.5b-instruct

# 使用huggingface-hub下载(需先pip install huggingface-hub)
huggingface-cli download --resume-download \
  Qwen/Qwen2.5-1.5B-Instruct \
  --local-dir . \
  --include "config.json" \
  --include "tokenizer.*" \
  --include "pytorch_model.bin" \
  --include "model.safetensors" \
  --include "generation_config.json"

验证目录结构应为:

/tmp/qwen2.5-1.5b-instruct/
├── config.json
├── generation_config.json
├── model.safetensors      # 或 pytorch_model.bin
├── tokenizer.json
├── tokenizer.model
└── tokenizer_config.json

提示:若使用safetensors格式(更安全、加载更快),请确保后续Dockerfile中transformers>=4.37.0;若用.bin,则无版本强依赖。

3. 构建生产就绪镜像

3.1 创建Dockerfile

新建文件 Dockerfile.qwen,内容如下(已针对K8s场景深度优化):

FROM python:3.10-slim-bookworm

# 设置非root用户提升安全性
RUN groupadd -g 1001 -f app && useradd -r -u 1001 -g app app
USER app

# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

# 安装Python依赖(精简+指定版本,避免冲突)
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt

# 创建模型挂载点(非绑定宿主机路径,由K8s VolumeMount控制)
RUN mkdir -p /app/model
WORKDIR /app

# 复制应用代码
COPY app.py streamlit_config.toml ./

# 暴露Streamlit默认端口
EXPOSE 8501

# 启动命令(禁用Streamlit默认重载,适配K8s生命周期)
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.headless=true", "--server.enableCORS=false"]

配套 requirements.txt(严格锁定版本,避免K8s中随机失败):

streamlit==1.32.0
transformers==4.38.2
torch==2.1.2+cu118
accelerate==0.27.2
sentence-transformers==2.2.2
bitsandbytes==0.43.1  # 可选,如需4bit量化

3.2 编写核心服务代码 app.py

import os
import torch
import streamlit as st
from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
from threading import Thread

# 从环境变量读取模型路径(K8s中由VolumeMount注入)
MODEL_PATH = os.getenv("MODEL_PATH", "/app/model")

@st.cache_resource
def load_model():
    st.info(" 正在加载模型...")
    tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_PATH,
        device_map="auto",
        torch_dtype="auto",
        trust_remote_code=True,
    )
    return tokenizer, model

def main():
    st.set_page_config(
        page_title="Qwen2.5-1.5B 本地对话助手",
        page_icon="🧠",
        layout="centered",
        initial_sidebar_state="expanded",
    )

    st.title(" Qwen2.5-1.5B 本地对话助手")
    st.caption("基于Kubernetes StatefulSet部署 · 全链路本地化 · 零数据出域")

    # 初始化模型(首次访问触发)
    if "tokenizer" not in st.session_state or "model" not in st.session_state:
        try:
            st.session_state.tokenizer, st.session_state.model = load_model()
            st.success(" 模型加载完成,可以开始对话!")
        except Exception as e:
            st.error(f" 模型加载失败:{e}")
            st.stop()

    # 对话历史(StatefulSet中每个Pod独立维护,不共享)
    if "messages" not in st.session_state:
        st.session_state.messages = [
            {"role": "system", "content": "你是通义千问Qwen2.5-1.5B-Instruct,专注提供准确、简洁、友好的中文对话服务。"}
        ]

    # 显示历史消息
    for msg in st.session_state.messages[1:]:  # 跳过system消息
        st.chat_message(msg["role"]).write(msg["content"])

    # 输入框
    if prompt := st.chat_input("请输入你的问题..."):
        st.session_state.messages.append({"role": "user", "content": prompt})
        st.chat_message("user").write(prompt)

        # 构建对话模板(严格对齐官方)
        messages = st.session_state.messages.copy()
        text = st.session_state.tokenizer.apply_chat_template(
            messages, tokenize=False, add_generation_prompt=True
        )

        model_inputs = st.session_state.tokenizer(
            [text], return_tensors="pt"
        ).to(st.session_state.model.device)

        # 流式生成(提升用户体验)
        streamer = TextIteratorStreamer(
            st.session_state.tokenizer,
            skip_prompt=True,
            skip_special_tokens=True
        )
        generate_kwargs = {
            "input_ids": model_inputs.input_ids,
            "max_new_tokens": 1024,
            "temperature": 0.7,
            "top_p": 0.9,
            "do_sample": True,
            "streamer": streamer,
        }

        # 异步生成
        thread = Thread(target=st.session_state.model.generate, kwargs=generate_kwargs)
        thread.start()

        # 流式输出
        with st.chat_message("assistant"):
            message_placeholder = st.empty()
            full_response = ""
            for new_token in streamer:
                full_response += new_token
                message_placeholder.markdown(full_response + "▌")
            message_placeholder.markdown(full_response)
        
        st.session_state.messages.append({"role": "assistant", "content": full_response})

    # 清空按钮(释放显存+重置历史)
    if st.sidebar.button("🧹 清空对话", use_container_width=True):
        torch.cuda.empty_cache()
        st.session_state.messages = [
            {"role": "system", "content": "你是通义千问Qwen2.5-1.5B-Instruct,专注提供准确、简洁、友好的中文对话服务。"}
        ]
        st.rerun()

if __name__ == "__main__":
    main()

配套 streamlit_config.toml(关闭开发模式,启用生产参数):

[server]
headless = true
enableCORS = false
enableXSSProtection = true
maxUploadSize = 50

[theme]
primaryColor = "#2a9d8f"
backgroundColor = "#fafafa"
secondaryBackgroundColor = "#ffffff"
textColor = "#262730"
font = "sans serif"

3.3 构建并推送镜像

# 构建镜像(假设registry为私有Harbor或阿里云ACR)
docker build -f Dockerfile.qwen -t your-registry/qwen2.5-1.5b:v1.0 .

# 登录并推送
docker login your-registry
docker push your-registry/qwen2.5-1.5b:v1.0

4. Kubernetes StatefulSet部署详解

4.1 创建命名空间与资源配置

新建 qwen-ns.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: qwen-inference
  labels:
    name: qwen-inference
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: qwen-sa
  namespace: qwen-inference
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: qwen-role
  namespace: qwen-inference
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: qwen-rolebinding
  namespace: qwen-inference
subjects:
- kind: ServiceAccount
  name: qwen-sa
  namespace: qwen-inference
roleRef:
  kind: Role
  name: qwen-role
  apiGroup: rbac.authorization.k8s.io

4.2 模型持久卷(PV)与声明(PVC)

新建 qwen-pv-pvc.yaml(以NFS为例,也可替换为CSI驱动):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: qwen-model-pv
  labels:
    type: model
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadOnlyMany  # 模型只读,安全第一
  nfs:
    server: nfs-server.example.com
    path: "/exports/qwen2.5-1.5b-instruct"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: qwen-model-pvc
  namespace: qwen-inference
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 5Gi
  selector:
    matchLabels:
      type: model

关键设计:ReadOnlyMany确保所有Pod共享同一份模型文件,零拷贝、零同步、零一致性风险。

4.3 StatefulSet核心部署文件

新建 qwen-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: qwen-inference
  namespace: qwen-inference
  labels:
    app: qwen-inference
spec:
  serviceName: "qwen-headless"
  replicas: 2  # 可根据GPU节点数调整
  selector:
    matchLabels:
      app: qwen-inference
  template:
    metadata:
      labels:
        app: qwen-inference
    spec:
      serviceAccountName: qwen-sa
      containers:
      - name: qwen-app
        image: your-registry/qwen2.5-1.5b:v1.0
        ports:
        - containerPort: 8501
          name: http
        env:
        - name: MODEL_PATH
          value: "/app/model"
        volumeMounts:
        - name: model-storage
          mountPath: /app/model
          readOnly: true
        - name: tmp-storage
          mountPath: /tmp
        resources:
          limits:
            nvidia.com/gpu: 1  # 关键:显卡独占
            memory: "12Gi"
            cpu: "4"
          requests:
            nvidia.com/gpu: 1
            memory: "10Gi"
            cpu: "2"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8501
          initialDelaySeconds: 60
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8501
          initialDelaySeconds: 45
          periodSeconds: 15
        startupProbe:
          httpGet:
            path: /startupz
            port: 8501
          failureThreshold: 30
          periodSeconds: 5
      volumes:
      - name: model-storage
        persistentVolumeClaim:
          claimName: qwen-model-pvc
      - name: tmp-storage
        emptyDir: {}
      nodeSelector:
        kubernetes.io/os: linux
        accelerator: nvidia  # 标记含GPU节点
      tolerations:
      - key: "nvidia.com/gpu"
        operator: "Exists"
        effect: "NoSchedule"
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi

4.4 Headless Service与Ingress(可选)

新建 qwen-service-ingress.yaml

apiVersion: v1
kind: Service
metadata:
  name: qwen-headless
  namespace: qwen-inference
  labels:
    app: qwen-inference
spec:
  clusterIP: None
  selector:
    app: qwen-inference
  ports:
  - port: 8501
    name: http
---
apiVersion: v1
kind: Service
metadata:
  name: qwen-service
  namespace: qwen-inference
  labels:
    app: qwen-inference
spec:
  type: ClusterIP
  selector:
    app: qwen-inference
  ports:
  - port: 8501
    targetPort: 8501
---
# 若需公网访问,添加Ingress(此处省略,按实际Ingress Controller配置)

5. 部署与验证全流程

5.1 一键部署命令

# 应用全部资源
kubectl apply -f qwen-ns.yaml
kubectl apply -f qwen-pv-pvc.yaml
kubectl apply -f qwen-statefulset.yaml
kubectl apply -f qwen-service-ingress.yaml

# 查看Pod状态(等待Running & Ready)
kubectl -n qwen-inference get pods -w
# 输出示例:
# NAME                READY   STATUS    RESTARTS   AGE
# qwen-inference-0    1/1     Running   0          45s
# qwen-inference-1    1/1     Running   0          32s

5.2 日志与健康检查

# 查看某Pod日志(确认模型加载成功)
kubectl -n qwen-inference logs qwen-inference-0 | grep ""

# 手动调用就绪探针(应返回200)
kubectl -n qwen-inference exec qwen-inference-0 -- curl -s http://localhost:8501/readyz

# 进入Pod测试本地推理(快速验证)
kubectl -n qwen-inference exec -it qwen-inference-0 -- bash
# 然后在容器内运行:
# python -c "from transformers import AutoTokenizer; print(AutoTokenizer.from_pretrained('/app/model').encode('hello'))"

5.3 访问服务

  • 集群内访问curl http://qwen-service.qwen-inference.svc.cluster.local:8501
  • NodePort方式(修改Service类型):http://<NODE_IP>:30851
  • Ingress方式https://qwen.your-domain.com

打开浏览器,你会看到熟悉的Streamlit聊天界面——但背后已是K8s调度的、可伸缩、可监控、可灰度发布的生产服务。

6. 运维与调优建议

6.1 显存监控(关键!)

# 实时查看各Pod GPU显存占用
kubectl -n qwen-inference top pods -o wide --use-protocol-buffers

# 结合nvidia-smi(需进入Pod)
kubectl -n qwen-inference exec qwen-inference-0 -- nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits

建议:设置Prometheus告警规则,当nvidia_gpu_duty_cycle > 95%持续5分钟,或nvidia_gpu_memory_used_bytes > 14Gi时触发扩容。

6.2 扩容策略

  • 水平扩容:直接修改StatefulSet replicas,K8s自动创建新Pod(每个Pod独占1卡)
  • 垂直扩容:调整resources.limits.nvidia.com/gpu2(需A100/A800等双卡机型),配合device_map="balanced"实现单Pod多卡推理
  • 自动扩缩容(KPA):需配合Knative,超出本教程范围,但StatefulSet是其底层基础

6.3 故障自愈实践

  • 当Pod因OOM被K8s Kill时,StatefulSet自动重建,volumeClaimTemplates确保/data目录(如缓存)保留
  • 若模型加载失败,startupProbe会阻止Pod进入Ready状态,避免流量打入
  • livenessProbe检测到HTTP无响应,自动重启容器,不依赖节点级恢复

7. 总结:从本地玩具到生产服务的关键跨越

部署Qwen2.5-1.5B从来不只是“让模型跑起来”。本教程带你完成三个本质跃迁:

  • 从单点到服务:Streamlit单进程 → K8s多实例Service,支持并发、负载均衡、故障隔离
  • 从临时到持久:本地文件路径 → PV/PVC声明式挂载,模型一次准备、全集群复用
  • 从手动到自治:人工启停 → Liveness/Readiness/Startup三探针驱动,K8s自动健康治理

你获得的不再是一个“能用的Demo”,而是一个:
🔹 可交付(YAML即文档)、
🔹 可观测(指标+日志+事件)、
🔹 可扩展(水平/垂直/自动)、
🔹 可审计(RBAC+PodSecurityPolicy)、
🔹 可回滚(镜像Tag+StatefulSet Revision)

的轻量大模型服务基座。它足够小——1.5B参数、单卡起步;也足够大——支撑起你团队的第一代AI原生应用。

下一步,你可以:
→ 将qwen-service接入内部API网关,供其他服务调用
→ 为StatefulSet添加HorizontalPodAutoscaler,按GPU利用率自动扩缩
→ 基于qwen-inference-0 Pod导出Prometheus指标,构建LLM服务大盘
→ 替换为Qwen2.5-7B或Qwen2-VL,复用同一套编排框架

真正的AI工程化,始于对最小可行单元的严谨封装——而Qwen2.5-1.5B,正是那个刚刚好、不冗余、不妥协的起点。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐