注意:

在 Oracle Kubernetes 引擎 (OKE) 上部署 OpenAI vLLM 生产堆栈

简介

为生产工作负载采用大型语言模型 (LLM) 的企业面临着关键的基础设施决策:依赖第三方推理 API 或部署自托管推理堆栈。自托管部署具有显著优势:全面的数据隐私和合规性控制;消除网络往返延迟,实现低于 100 毫秒的推断延迟;大规模的可预测成本;无需供应商锁定即可自由地微调和服务任何开源模型。

但是,从头开始构建生产级 LLM 推断堆栈非常复杂。它需要具备 GPU 感知的容器编排、跨多个模型副本的智能请求路由、用于大型模型权重的持久存储以及持续监视—所有这些都经过可靠集成和运行。

Oracle Cloud Infrastructure 提供了多种 AI 推断路径。OCI Generative AI 服务通过与租户隔离的专用 AI 集群提供完全托管的体验,非常适合希望快速开始使用受支持模型的团队。本教程采用替代方法:在 OKE 上部署自己的推理堆栈。此路径专为需要对 GPU 驱动程序、CUDA 版本、模型配置和服务参数进行精确控制的团队,或者需要训练和微调定制模型并希望直接为其提供服务的团队而设计。OCI 为裸金属 GPU 实例提供 NVIDIA A10、A100 和 H100 GPU,并通过超低延迟 RDMA 集群网络连接,为您提供与本地部署相同的硬件控制水平,同时享受云弹性。

vLLM 生产堆栈通过提供基于 vLLM(Meta、Mistral AI 和 IBM 等组织在生产中使用的高吞吐量推断引擎)构建的开源 Kubernetes 本机平台来解决自托管推断的复杂性。与标准服务框架相比,它通过高效的 GPU 内存管理和 KV 高速缓存优化提供高达 24x 的吞吐量。结合 OKE 和 OCI GPU 配置,您可以获得一个生产就绪的推断平台,具有企业级网络、存储和安全性。本教程中使用的 OCI 部署脚本在 official vLLM production-stack repository 中提供和维护。

本教程将引导您完成在 OKE 上部署 vLLM 生产堆栈,从基础结构预配到运行第一个推断请求。

注:本教程使用 OCI CLI 分步预配资源,以帮助您了解 GPU 推断部署所需的 OCI 云资源的完整流。对于生产环境,建议使用 TerraformOCI Resource Manager (Shepherd) 将此基础结构编纂为可重复的版本控制部署。

图中显示了包含子网、OKE 集群、GPU 节点池、vLLM 云池和负载平衡器的 VCN

本教程使用以下 OCI 服务:

服务 用途
Oracle Kubernetes 引擎 (OKE) 用于容器编排和 GPU 工作负载调度的托管 Kubernetes 集群
OCI 计算(GPU 配置) 用于模型推断的 NVIDIA A10 (24GB) 和 A100 (80GB) GPU 实例
OCI 块存储卷 具有可配置性能层的模型权重的持久存储
OCI Virtual Cloud Network, VCN 网络基础设施,包括子网、网关和安全列表
OCI 负载均衡器 对推断端点的外部访问
OCI 堡垒 用于专用集群访问的托管 SSH 隧道
OCI 对象存储 使用预先验证的请求 (PAR) URL 的替代模型源

目标

在本教程里,您将:

Prerequisites

注:本教程中的示例输出和屏幕截图使用 us-chicago-1 。您可以通过设置 OCI_REGION 在任何受支持的区域中部署。GPU 容量因区域和可用性域而异,因此请确认您的目标 GPU 配置在部署之前可用。检查按区域划分的 GPU 配置可用性,如果出现容量错误,请准备好尝试其他可用性域 (GPU_AD_INDEX)。

注:本教程预配付费 GPU 资源(例如 VM.GPU.A10.1)。它不是 OCI Always Free 工作负载。完成后始终运行清理步骤,以避免持续收费。

注:本教程部署 openai/gpt-oss-20b,这是 OpenAI 中 Apache 2.0 许可的型号。不需要 Hugging Face 标记。如果要部署受限模型(例如 Meta Llama 3.1),则需要具有 API 标记的拥抱面账户。

任务 1:配置环境变量

在部署基础结构之前设置所需的 OCI 配置。

  1. 在 OCI 控制台中查找区间 OCID。导航到身份和安全 > 区间,然后单击目标区间并复制 OCID。

    oci iam compartment list --query 'data[].{name:name,id:id}' --output table

    OCI CLI 输出显示区间 OCID

  2. 导出所需的环境变量。

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. (可选)通过设置以下任一环境变量覆盖默认配置。

    变量 Default 说明
    OCI_REGION us-ashburn-1 用于部署的 OCI 区域
    OCI_PROFILE DEFAULT OCI CLI 配置概要信息
    CLUSTER_NAME production-stack OKE 集群的名称
    GPU_SHAPE VM.GPU.A10.1 节点池的 GPU 计算配置
    GPU_NODE_COUNT 1 池中的 GPU 节点数
    GPU_BOOT_VOLUME_GB 200 GPU 节点的引导卷的大小 (GB)
    CPU_BOOT_VOLUME_GB 100 CPU 节点的引导卷的大小 (GB)
    GPU_AD_INDEX 1 用于放置 GPU 的可用性域索引(基于 0)
    PRIVATE_CLUSTER true 对于公共 Kubernetes API 端点,设置为 false
    KUBERNETES_VERSION v1.31.10 OKE 集群的 Kubernetes 版本

    例如,要使用两个 A100 GPU 节点进行部署:

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
    export GPU_SHAPE="BM.GPU.A100-v2.8"
    export GPU_NODE_COUNT="2"
  4. 查看可用的 GPU 配置,并根据模型大小要求选择一个配置。

    形状 GPU GPU 类型 GPU 内存 建议用于
    VM.GPU.A10.1 1 NVIDIA A10 24 GB 7B – 13B 参数模型
    VM.GPU.A10.2 2 NVIDIA A10 48 GB 与小型模型平行的张量
    BM.GPU4.8 8 NVIDIA A100 40 GB 320 GB 70B 模型,经济高效
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640 GB 70B+ 参数模型
    BM.GPU.H100.8 8 NVIDIA H100 640 GB 最大的型号,RDMA 支持

    注:裸金属配置 (BM.*) 提供没有虚拟化开销的专用硬件,并支持多 GPU 张量并行。对于较小的型号,虚拟机配置 (VM.*) 更具成本效益。

    注:本教程使用 VM.GPU.A10.1(具有 24 GB GPU 内存的单个 NVIDIA A10)来部署 openai/gpt-oss-20b,这是一种专家 (MoE) 模型,其中包含 3.6B 活动参数,通常适用于单个 A10 GPU。高级部分演示了使用 BM.GPU.H100.8 的多 GPU 配置,适用于大型模型,例如 Llama 3.1 70B。

任务 2:使用自动化脚本进行部署(快速启动)

vLLM 生产堆栈包含一个自动化部署脚本,可预配所有 OCI 资源并使用单个命令部署推断堆栈。使用此方法可以快速部署。对于想要自定义流程的用户,任务 3 到 10 分别涵盖每个步骤。

  1. 克隆 vLLM 生产堆栈系统信息库。

    git clone https://github.com/vllm-project/production-stack.git
    cd production-stack/deployment_on_cloud/oci
  2. 导出区间 OCID。

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. 运行部署脚本。

    ./entry_point.sh setup

    entry_point.sh 设置的终端输出,显示 VCN、集群、堡垒和节点池创建

    对于公共群集 (PRIVATE_CLUSTER=false),设置将创建所有基础结构并在单个命令中部署 vLLM 堆栈。将 Helm 值文件作为第二个参数传递:

    PRIVATE_CLUSTER=false ./entry_point.sh setup ./production_stack_specification.yaml

    对于专用集群(默认设置),设置将创建基础结构,但无法直接访问 Kubernetes API。打开一个单独的终端并启动隧道,然后部署:

    # In a separate terminal, start the SSH tunnel (auto-reconnects on drops):
    ./entry_point.sh tunnel
    
    # Back in the first terminal, deploy vLLM:
    ./entry_point.sh deploy-vllm ./production_stack_specification.yaml
  4. 验证部署是否正在运行。

    kubectl get pods

    预期输出:

    NAME                                            READY   STATUS    RESTARTS   AGE
    vllm-deployment-router-xxxxxxxxxx-xxxxx          1/1     Running   0          5m
    vllm-gpt-oss-deployment-vllm-xxxxxxxxxx-xxxxx    1/1     Running   0          5m

注:如果两个云池都显示 Running 状态,您的部署就绪。跳转到 Task 10:Test the Inference Endpoint

注: GPU 实例受到 OCI 容量限制。如果脚本在“Waiting for GPU node(等待 GPU 节点)”循环中停留超过 15 分钟,则所选可用性域中可能无法使用 GPU 配置。使用 oci ce node-pool get 检查节点池状态,并查找“Out of host capacity(主机容量不足)”错误。要解决此问题,请使用 ./entry_point.sh cleanup 进行清理,然后使用其他可用性域(例如 GPU_AD_INDEX=0GPU_AD_INDEX=2)或其他 GPU 配置(例如 GPU_SHAPE=VM.GPU.A10.2)重新部署。

注:部署脚本使用会产生大量成本的 GPU 实例(对于单个 A10 GPU,每天约 50 美元)。完成后,请始终运行 ./entry_point.sh cleanup 以避免持续收费。

任务 3:创建 VCN 和网络

创建 OKE 集群所需的 OCI 网络基础结构。这包括虚拟云网络 (Virtual Cloud Network,VCN)、网关、路由表、安全列表和子网。每个网络资源都是在几秒钟内创建的;完整的命令集在不到 2 分钟内完成。

  1. 创建具有 10.0.0.0/16 CIDR 块的 VCN。

    VCN_ID=$(oci network vcn create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --display-name "${CLUSTER_NAME}-vcn" \
        --cidr-blocks '["10.0.0.0/16"]' \
        --dns-label "prodstack" \
        --query "data.id" \
        --raw-output)
  2. 创建用于公共子网路由的 Internet 网关。

    IGW_ID=$(oci network internet-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-igw" \
        --is-enabled true \
        --query "data.id" \
        --raw-output)
  3. 为来自专用子网的出站流量创建 NAT 网关。

    NAT_ID=$(oci network nat-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-nat" \
        --query "data.id" \
        --raw-output)
  4. 创建服务网关以访问 Oracle 服务网络。OKE 云控制器使用 Oracle 服务初始化 worker 节点(设置可用性域标签,删除初始化污点)。如果没有服务网关,GPU 节点可能会保持未初始化状态,块存储卷预配将失败。

    SGW_SERVICE_ID=$(oci network service list \
        --query "data[?contains(name, 'All') && contains(name, 'Services')].id | [0]" \
        --raw-output)
    
    SGW_SERVICE_NAME=$(oci network service list \
        --query "data[?contains(name, 'All') && contains(name, 'Services')].\"cidr-block\" | [0]" \
        --raw-output)
    
    SGW_ID=$(oci network service-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-sgw" \
        --services "[{\"serviceId\": \"${SGW_SERVICE_ID}\"}]" \
        --query "data.id" \
        --raw-output)
  5. 为专用子网和公共子网创建路由表。

    PRIVATE_RT_ID=$(oci network route-table create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-private-rt" \
        --route-rules "[
            {\"cidrBlock\": \"0.0.0.0/0\", \"networkEntityId\": \"${NAT_ID}\"},
            {\"destination\": \"${SGW_SERVICE_NAME}\", \"destinationType\": \"SERVICE_CIDR_BLOCK\", \"networkEntityId\": \"${SGW_ID}\"}
        ]" \
        --query "data.id" \
        --raw-output)
    
    PUBLIC_RT_ID=$(oci network route-table create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-public-rt" \
        --route-rules "[{\"cidrBlock\": \"0.0.0.0/0\", \"networkEntityId\": \"${IGW_ID}\"}]" \
        --query "data.id" \
        --raw-output)

    注:专用路由表有两个规则:用于常规 Internet 访问的 NAT 网关路由(提取容器映像、下载模型)和用于直接访问 Oracle 服务网络的服务网关路由。服务网关路由至关重要。如果没有它,OKE 云控制器将无法初始化 worker 节点,这会阻止块存储卷预配。公共路由表使用互联网网关进行负载平衡器访问。

  6. 使用 OKE 所需的入站和出站规则创建安全列表。

    SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 22, "max": 22}}, "description": "SSH access"},
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "10.244.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes pods CIDR"},
            {"source": "10.96.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes services CIDR"},
            {"source": "0.0.0.0/0", "protocol": "1", "isStateless": false, "icmpOptions": {"type": 3, "code": 4}, "description": "Path MTU discovery"}
        ]' \
        --query "data.id" \
        --raw-output)

    安全注意:为了简单起见,此示例安全列表非常广泛。对于生产环境,将 SSH 限制为堡垒子网和 IP 范围,并首选每个子网单独的安全列表或 NSG,以便负载平衡器子网不允许从 0.0.0.0/0 执行 SSH。

    安全默认设置:首先将 SSH 限制为公共 IP,然后将 SSH 规则仅附加到堡垒子网。您可以将 Kubernetes 云池/服务 CIDR 保留在 Worker 子网中,并且完全从负载平衡器子网省略 SSH。

    可选(推荐)拆分:为堡垒子网创建一个小型的仅 SSH 安全列表,并为 worker/LB 子网创建一个单独的列表。

    BASTION_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-bastion-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "YOUR_PUBLIC_IP/32", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 22, "max": 22}}, "description": "SSH from your IP"}
        ]' \
        --query "data.id" \
        --raw-output)
    
    WORKER_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-worker-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "10.244.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes pods CIDR"},
            {"source": "10.96.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes services CIDR"},
            {"source": "0.0.0.0/0", "protocol": "1", "isStateless": false, "icmpOptions": {"type": 3, "code": 4}, "description": "Path MTU discovery"}
        ]' \
        --query "data.id" \
        --raw-output)
    
    LB_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-lb-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 80, "max": 80}}, "description": "HTTP (public LB)"},
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 443, "max": 443}}, "description": "HTTPS (public LB)"}
        ]' \
        --query "data.id" \
        --raw-output)

    注:如果仅使用内部负载平衡器,请将上面的 0.0.0.0/0 源替换为 10.0.0.0/16(或您的 VCN CIDR)。用法:BASTION_SL_ID 连接到堡垒子网,将 WORKER_SL_ID 连接到 API/worker 子网,将 LB_SL_ID 连接到负载平衡器子网。

    注: GPU Worker 节点需要在集群中注册 Kubernetes 云池 CIDR (10.244.0.0/16) 和服务 CIDR (10.96.0.0/16) 规则。ICMP 类型 3 代码 4 规则启用路径 MTU 搜索,从而防止数据包碎片问题。

  7. 创建子网。该集群需要四个子网:一个用于 Kubernetes API 端点,一个用于 worker 节点,一个用于负载平衡器,一个用于用于访问专用集群的堡垒主机。

    API_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-api-subnet" \
        --cidr-block "10.0.0.0/28" \
        --route-table-id "${PRIVATE_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "kubeapi" \
        --prohibit-public-ip-on-vnic true \
        --query "data.id" \
        --raw-output)
    
    WORKER_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-worker-subnet" \
        --cidr-block "10.0.10.0/24" \
        --route-table-id "${PRIVATE_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "workers" \
        --prohibit-public-ip-on-vnic true \
        --query "data.id" \
        --raw-output)
    
    LB_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-lb-subnet" \
        --cidr-block "10.0.20.0/24" \
        --route-table-id "${PUBLIC_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "loadbalancers" \
        --query "data.id" \
        --raw-output)
    
    BASTION_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-bastion-subnet" \
        --cidr-block "10.0.30.0/24" \
        --route-table-id "${PUBLIC_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "bastion" \
        --query "data.id" \
        --raw-output)
    子网 CIDR 可见性 用途
    API 端点 10.0.0.0/28 专用 Kubernetes API 服务器
    Worker 节点 10.0.10.0/24 专用 GPU 计算节点
    负载平衡器 10.0.20.0/24 公共 外部服务访问
    堡垒 10.0.30.0/24 公共 用于专用集群访问的 SSH 隧道

任务 4:创建 OKE 群集

使用在任务 3 中创建的网络资源在 OKE 上部署托管 Kubernetes 集群。预配集群大约需要 10 分钟。本教程创建专用集群(脚本默认设置),它不使用 Kubernetes API 端点的预留公共 IP。对于生产工作负载,建议使用专用集群,因为 API 服务器未向公共 Internet 公开。

  1. 使用专用端点创建 OKE 集群。

    oci ce cluster create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --name "${CLUSTER_NAME}" \
        --vcn-id "${VCN_ID}" \
        --kubernetes-version "${KUBERNETES_VERSION}" \
        --endpoint-subnet-id "${API_SUBNET_ID}" \
        --service-lb-subnet-ids "[\"${LB_SUBNET_ID}\"]" \
        --endpoint-public-ip-enabled false

    该命令返回工作请求 ID。从群集列表中获取群集 ID。

    CLUSTER_ID=$(oci ce cluster list \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --lifecycle-state CREATING \
        --query 'data[0].id' \
        --raw-output)
    echo "CLUSTER_ID=${CLUSTER_ID}"

    注:专用集群不需要预留公共 IP。Worker 节点仍可通过 NAT 网关访问互联网,以提取容器映像并下载模型。只有 kubectl 访问需要通过堡垒的 SSH 隧道(在后续步骤中配置)。

  2. 等待群集变为 ACTIVE。此步骤大约需要 10 分钟。

    oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query "data.\"lifecycle-state\"" \
        --raw-output

    轮询命令,直到输出返回 ACTIVE

    可选:显示简洁的状态概要(包括专用 API 端点)。

    oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query 'data.{name:name,state:"lifecycle-state","k8s-version":"kubernetes-version",endpoint:endpoints."private-endpoint"}' \
        --output table

    OCI CLI 输出显示 OKE 集群处于 ACTIVE 状态

    kubectl get nodes -o wide

    显示两个就绪节点的 kubectl get 节点输出

  3. 创建 OCI 堡垒以访问专用集群。堡垒提供连接到专用 Kubernetes API 端点的托管 SSH 隧道。

    BASTION_ID=$(oci bastion bastion create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --bastion-type STANDARD \
        --target-subnet-id "${BASTION_SUBNET_ID}" \
        --name "${CLUSTER_NAME}-bastion" \
        --client-cidr-list '["YOUR_PUBLIC_IP/32"]' \
        --query "data.id" \
        --raw-output)

    注:YOUR_PUBLIC_IP/32 替换为您当前的公共 IP。对于共享网络,请改用公司 CIDR 块。

    等待堡垒变为 ACTIVE(大约 1 分钟)。

    oci bastion bastion get \
        --bastion-id "${BASTION_ID}" \
        --query "data.\"lifecycle-state\"" \
        --raw-output

    安全注意:对于生产环境,请勿使用 0.0.0.0/0。将 --client-cidr-list 限制为公共 IP 或公司 CIDR(例如 "YOUR_PUBLIC_IP/32"),否则互联网上的任何人都可以尝试堡垒会话。

  4. 使用专用端点下载 kubeconfig。

    oci ce cluster create-kubeconfig \
        --cluster-id "${CLUSTER_ID}" \
        --file "${HOME}/.kube/config" \
        --region "${OCI_REGION}" \
        --token-version 2.0.0 \
        --kube-endpoint PRIVATE_ENDPOINT
  5. 获取 SSH 隧道的专用端点 IP 地址。

    PRIVATE_ENDPOINT=$(oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query "data.endpoints.\"private-endpoint\"" \
        --raw-output)
    PRIVATE_IP="${PRIVATE_ENDPOINT%:*}"
    echo "Private endpoint IP: ${PRIVATE_IP}"
  6. 创建堡垒端口转发会话。您将需要 SSH 公共密钥文件。

    SESSION_ID=$(oci bastion session create-port-forwarding \
        --bastion-id "${BASTION_ID}" \
        --target-private-ip "${PRIVATE_IP}" \
        --target-port 6443 \
        --session-ttl 10800 \
        --display-name "kubectl-tunnel" \
        --ssh-public-key-file ~/.ssh/id_rsa.pub \
        --query "data.id" \
        --raw-output)

    等待会话变为 ACTIVE,然后获取 SSH 命令。

    oci bastion session get \
        --session-id "${SESSION_ID}" \
        --query "data.{state:\"lifecycle-state\", ssh:\"ssh-metadata\".command}" 2>&1
  7. 打开一个单独的终端并使用上一步中的命令启动 SSH 隧道。隧道将本地端口 6443 转发到专用 Kubernetes API。

    ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -N -L 6443:<PRIVATE_IP>:6443 \
        -p 22 -o ServerAliveInterval=30 \
        <SESSION_OCID>@host.bastion.<REGION>.oci.oraclecloud.com

    注:<PRIVATE_IP><SESSION_OCID><REGION> 替换为上一步中的值。在会话期间将此终端保持打开状态。当 SSH 代理加载了多个密钥时,-o IdentitiesOnly=yes 标志可防止“验证失败次数过多”错误。

  8. 更新 kubeconfig 以通过本地隧道进行连接。

    CLUSTER_NAME_KUBE=$(kubectl config view --minify -o jsonpath='{.clusters[0].name}')
    kubectl config set-cluster "${CLUSTER_NAME_KUBE}" \
        --server=https://127.0.0.1:6443 \
        --insecure-skip-tls-verify=true

    注:需要 --insecure-skip-tls-verify 标志,因为已为专用端点 IP(而不是 127.0.0.1)签发了群集证书。这是安全的,因为流量是通过 SSH 隧道加密的。

  9. 如果您使用的是非默认 OCI CLI 配置文件(例如 API_KEY_AUTH),请更新 kubeconfig 以使用它。生成的 kubeconfig 缺省为用于生成令牌的 DEFAULT 配置文件。

    kubectl config set-credentials \
        $(kubectl config view --minify -o jsonpath='{.users[0].name}') \
        --exec-env=OCI_CLI_PROFILE=${OCI_PROFILE}

    提示:步骤 6-9 由 ./entry_point.sh tunnel 自动执行,如果 SSH 隧道在长时间运行的操作(如磁盘扩展)期间断开,也会自动重新连接。在单独的终端中运行它,并使其在会话期间运行。

  10. 验证群集访问。

kubectl get nodes

此时,输出将不显示任何节点,因为尚未添加 GPU 节点池。

No resources found

任务 5:添加 GPU 节点池

将具有 GPU 计算实例的节点池添加到 OKE 集群。

  1. 查找与 GPU 兼容的最新 OKE 节点映像。OKE 需要预先安装有 kubelet 和节点注册组件的特定映像。使用 node-pool-options API 查找适用于 Kubernetes 版本的正确映像。

    GPU_IMAGE_ID=$(oci ce node-pool-options get \
        --node-pool-option-id all \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --query "data.sources[?contains(\"source-name\", 'GPU') && contains(\"source-name\", 'OKE-${KUBERNETES_VERSION#v}') && contains(\"source-name\", '8.10')].\"image-id\" | [0]" \
        --raw-output)
    echo "GPU Image: ${GPU_IMAGE_ID}"

    注:与 Kubernetes 版本匹配的 Oracle Linux 8.10 GPU 映像的查询筛选器(例如 OKE-1.31.10)。如果需要基于 ARM 的映像,请将 8.10 替换为相应的筛选器。

  2. 确定具有 GPU 配置的可用性域。并非所有可用性域都具有 GPU 容量。

    AD=$(oci iam availability-domain list \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --query "data[${GPU_AD_INDEX}].name" \
        --raw-output)
    echo "Availability Domain: ${AD}"

    注: GPU 容量因区域和可用性域而异。如果节点池创建失败并出现“主机容量不足”错误,请尝试使用其他可用性域 (GPU_AD_INDEX) 或 GPU 配置,或者通过常规 OCI 流程请求容量。

  3. 创建具有 200 GB 引导卷的 GPU 节点池。

    oci ce node-pool create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --cluster-id "${CLUSTER_ID}" \
        --name "${GPU_NODE_POOL_NAME:-gpu-pool}" \
        --kubernetes-version "${KUBERNETES_VERSION}" \
        --node-shape "${GPU_SHAPE}" \
        --node-image-id "${GPU_IMAGE_ID}" \
        --node-boot-volume-size-in-gbs "${GPU_BOOT_VOLUME_GB:-200}" \
        --size "${GPU_NODE_COUNT}" \
        --placement-configs "[{\"availabilityDomain\": \"${AD}\", \"subnetId\": \"${WORKER_SUBNET_ID}\"}]" \
        --initial-node-labels '[{"key": "app", "value": "gpu"}, {"key": "nvidia.com/gpu", "value": "true"}]'

    注: vLLM Helm 图表稍后会使用节点标签 app=gpunvidia.com/gpu=true 来调度 GPU 节点上的推断云池。200 GB 引导卷为 vLLM 容器映像 (container image,~10 GB) 和模型权重提供空间,但文件系统在使用前必须进行扩展(请参见任务 8)。

  4. 等待 GPU 节点变为就绪。这通常需要 5 – 10 分钟,而节点会预配、引导、安装 GPU 驱动程序以及向群集注册。

    注: GPU 实例受容量约束。如果节点池处于 CREATING 状态,请在 OCI 控制台中或使用 oci ce node-pool get 检查节点状态。“超出主机容量”错误表示该可用性域中没有可用的 GPU 实例。要解决此问题,请尝试使用其他可用性域(GPU_AD_INDEX=0GPU_AD_INDEX=2),尝试使用其他 GPU 配置,或者通过 OCI 控制台或支持单请求容量预留。

    kubectl get nodes -w

    节点准备就绪后预期输出:

    NAME          STATUS   ROLES   AGE   VERSION
    10.0.10.x     Ready    node    5m    v1.31.10
  5. 验证是否在节点上检测到 GPU。

    kubectl get nodes -o=custom-columns=NAME:.metadata.name,GPUs:.status.capacity.'nvidia\.com/gpu'

    预期输出:

    NAME          GPUs
    10.0.10.x     1
  6. 为 CoreDNS 打补丁以在 GPU 节点上调度。OKE GPU 节点具有 nvidia.com/gpu=present:NoSchedule 污点。在仅具有 GPU 节点的集群中,像 CoreDNS 这样的系统云池无法调度,而不会对此污点进行容忍。如果没有 DNS,云池将无法解析外部主机名以下载模型。

    kubectl patch deployment coredns -n kube-system --type='json' \
      -p='[{"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}]'
    
    kubectl patch deployment kube-dns-autoscaler -n kube-system --type='json' \
      -p='[{"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}]'

    验证 CoreDNS 是否正在运行。

    kubectl get pods -n kube-system | grep coredns

    注:如果群集具有用于系统工作负荷的专用 CPU 节点池,则不需要执行此步骤。仅当 GPU 节点是集群中的唯一节点时,才需要此补丁程序。

    oci ce node-pool list --compartment-id "${OCI_COMPARTMENT_ID}" --cluster-id "${CLUSTER_ID}" \
        --query 'data[].{name:name,state:"lifecycle-state",shape:"node-shape",size:"node-config-details".size}' --output table

    OCI CLI 输出显示同时处于 ACTIVE 状态的 cpu-pool 和 gpu-pool

任务 6:安装 NVIDIA 设备插件

安装 NVIDIA 设备插件,以便 Kubernetes 可以在 GPU 上检测和调度工作负载。

  1. 应用 NVIDIA 设备插件 DaemonSet。

    kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml
  2. 等待插件云池准备就绪。

    kubectl wait --for=condition=Ready pods -l name=nvidia-device-plugin-ds -n kube-system --timeout=300s

    注:某些 OKE GPU 节点映像包括预安装的 NVIDIA 设备插件 (nvidia-gpu-device-plugin)。如果映像已包含该映像,则应用上游 DaemonSet 将创建另一个不会导致冲突的实例。自动脚本 (entry_point.sh deploy-vllm) 始终会安装它,以确保无论节点映像版本如何,GPU 检测都能正常工作。

  3. 确认 GPU 可由 Kubernetes 分配。

    kubectl get nodes -o=custom-columns=NAME:.metadata.name,GPUs:.status.allocatable.'nvidia\.com/gpu'

    预期输出:

    NAME          GPUs
    10.0.10.x     1
  4. 修补 CoreDNS 以容忍 GPU 节点污染。在 GPU 节点是唯一 worker 节点的集群中,CoreDNS 云池无法调度,因为 OKE GPU 节点携带 nvidia.com/gpu=present:NoSchedule 污点。如果没有 DNS,云池将无法解析映像注册表或模型下载 URL。

    kubectl patch deployment coredns -n kube-system --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}
    ]'
    kubectl rollout status deployment/coredns -n kube-system --timeout=60s

    注:仅当 GPU 节点是集群中的唯一 worker 节点时,才需要执行此步骤。如果您具有用于系统工作负荷的专用 CPU 节点池,则默认情况下 CoreDNS 会调度该池,并且不需要此修补程序。

任务 7:配置存储

应用 OCI Block Volume StorageClass 为模型权重提供持久性存储。

  1. 应用 StorageClass 定义。

    kubectl apply -f oci-block-storage-sc.yaml

    该文件定义两个性能层:

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: oci-block-storage-enc
    provisioner: blockvolume.csi.oraclecloud.com
    parameters:
      vpusPerGB: "10"
    reclaimPolicy: Delete
    volumeBindingMode: WaitForFirstConsumer
    allowVolumeExpansion: true
    StorageClass 性能 用例
    oci-block-storage-enc 平衡 (vpusPerGB: 10) 适用于大多数模型的默认、经济高效
    oci-block-storage-hp 高性能 (vpusPerGB: 20) 更快的模型加载,适用于大型模型
  2. 验证 StorageClasses 是否可用。

    kubectl get storageclass

注:对于需要跨多个云池共享存储的多节点部署,请使用 OCI 文件存储服务 (NFS) 和 ReadWriteMany 访问模式,而不是块存储卷。

任务 8:扩展 GPU 节点文件系统

无论您指定的引导卷大小是多少,OCI 引导卷具有固定约 47 GB 的分区。仅 vLLM 容器映像大约为 10 GB,模型权重需要额外的空间。在部署 vLLM 之前,必须扩展文件系统以避免 DiskPressure 逐出。

注:这是特定于 OCI 的要求。引导卷预配为 200 GB,但默认情况下操作系统仅分区约 47 GB。剩余空间必须手动声明。

  1. 验证 GPU 节点上的当前文件系统大小。

    GPU_NODE=$(kubectl get nodes -l app=gpu -o jsonpath='{.items[0].metadata.name}')
    kubectl run check-disk --rm -i --restart=Never \
      --image=busybox:latest \
      --overrides="{\"spec\":{\"nodeName\":\"${GPU_NODE}\",\"tolerations\":[{\"operator\":\"Exists\"}],\"containers\":[{\"name\":\"check\",\"image\":\"busybox:latest\",\"command\":[\"sh\",\"-c\",\"chroot /host df -h / | tail -1\"],\"securityContext\":{\"privileged\":true},\"volumeMounts\":[{\"name\":\"host\",\"mountPath\":\"/host\"}]}],\"volumes\":[{\"name\":\"host\",\"hostPath\":{\"path\":\"/\"}}]}}"

    输出将显示大约 47 GB 总数,确认需要进行扩展。

  2. 在 GPU 节点上创建一个特权 pod 以运行扩展命令。

    kubectl run expand-disk --restart=Never \
      --image=busybox:latest \
      --overrides="{\"spec\":{\"nodeName\":\"${GPU_NODE}\",\"tolerations\":[{\"operator\":\"Exists\"}],\"containers\":[{\"name\":\"expand\",\"image\":\"busybox:latest\",\"command\":[\"sleep\",\"600\"],\"securityContext\":{\"privileged\":true},\"volumeMounts\":[{\"name\":\"host\",\"mountPath\":\"/host\"}]}],\"volumes\":[{\"name\":\"host\",\"hostPath\":{\"path\":\"/\"}}]}}"

    等待 pod 启动。

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. 在一个 kubectl exec 命令中运行所有四个扩展步骤。将它们一起运行可以避免 kubectl exec 在步骤之间返回退出代码 137 (SIGKILL) 的风险,这在主机上的重磁盘 I/O 过程中可能会发生。

    kubectl exec expand-disk -- chroot /host bash -c '
      set -x
      growpart /dev/sda 3 || echo "growpart: partition may already be expanded"
      sleep 3
      pvresize /dev/sda3
      lvextend -l +100%FREE /dev/ocivolume/root || echo "lvextend: may already be extended"
      xfs_growfs /
      echo "EXPANSION_COMPLETE"
      df -h /
    '
    阶梯图 命令 用途
    1 growpart /dev/sda 3 展开分区 3 以使用完整磁盘
    2 pvresize /dev/sda3 调整 LVM 物理卷大小
    3 lvextend -l +100%FREE /dev/ocivolume/root 扩展逻辑卷
    4 xfs_growfs / 增长 XFS 文件系统以填充卷

    注意:这四个操作都是幂等的。如果 exec 返回退出代码 137,则可以安全地重新运行整个块。在输出中查找 EXPANSION_COMPLETE 以确认成功。

  4. 重新启动 kubelet,以便节点报告更新的可分配存储,然后验证并清除。

    kubectl exec expand-disk -- nsenter -t 1 -m -p -- systemctl restart kubelet 2>/dev/null \
        || echo "Warning: kubelet restart returned non-zero (non-critical)"
    kubectl exec expand-disk -- chroot /host df -h /
    kubectl delete pod expand-disk --force

    注:nsenter 命令将输入主机的 PID 名称空间以访问 systemd。普通 chroot /host systemctl restart kubelet 失败,因为它无法从 chroot 内连接到 systemd 总线。

    预期输出总量应约为 189 GB。

任务 9:部署 vLLM 生产堆栈

使用 Helm 安装 vLLM 推断堆栈。

  1. 添加 vLLM Helm 系统信息库。

    helm repo add vllm https://vllm-project.github.io/production-stack
    helm repo update
  2. 查看 Helm 值文件。production_stack_specification.yaml 为 OCI 配置模型、资源和存储。

    servingEngineSpec:
      runtimeClassName: ""
      modelSpec:
      - name: "gpt-oss"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "openai/gpt-oss-20b"
    
        replicaCount: 1
        requestCPU: 4
        requestMemory: "24Gi"
        requestGPU: 1
    
        # No HF token needed - Apache 2.0 licensed OpenAI open source model
    
        pvcStorage: "100Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          app: gpu
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.90"

    注:openai/gpt-oss-20b 模型是专家 (MoE) 模型的混合体,每个正向传递具有 20B 总参数和 3.6B 活动参数。它是在 Apache 2.0 许可证下发布的,因此不需要 Hugging Face 令牌。vllm/vllm-openai 容器映像提供与 OpenAI 兼容的 API 服务器,允许客户端对自托管端点使用标准 OpenAI SDK 调用。

  3. 部署堆栈。请不要在此处使用 --wait,因为路由器云池将 CrashLoop,直到在下一步中打补丁为止。

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml

    等待 vLLM 引擎 pod 启动(接下来将为路由器打补丁)。

    kubectl wait --for=condition=Ready pods -l model=gpt-oss --timeout=600s

    注:引擎云池需要几分钟时间才能变为“就绪”,因为它会在首次启动时下载模型权重。如果 pod 保持在 ContainerCreating 中,容器映像 (~10 GB) 仍处于拉取状态。使用 kubectl describe pod <pod-name> 检查进度。

  4. 为路由器部署打补丁。路由器需要 GPU 迭代(因此,它可以安排 GPU 节点是唯一具有容量的节点时),并增加内存限制(默认的 500 Mi 可能导致 OOMKill)。

    kubectl patch deployment vllm-deployment-router --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations", "value": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}]},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "512Mi"},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/limits/memory", "value": "1Gi"}
    ]'

    注:由于 OKE GPU 节点具有阻止非 GPU 工作负载调度的 nvidia.com/gpu=present:NoSchedule 污点,因此需要 GPU 宽容。由于路由器不使用 GPU,但需要在某个地方运行,因此这种宽容允许它在 GPU 节点上调度。在具有专用 CPU 节点池的集群中,不需要此宽容。

  5. 确认已部署 Helm 发行版。

    helm list

    显示已部署 vllm-stack 的 helm 列表的终端输出

  6. 验证云池是否正在运行。

    kubectl get pods

    预期输出:

    NAME                                            READY   STATUS    RESTARTS   AGE
    vllm-deployment-router-xxxxxxxxxx-xxxxx          1/1     Running   0          5m
    vllm-gpt-oss-deployment-vllm-xxxxxxxxxx-xxxxx    1/1     Running   0          5m
    kubectl get pods

    kubectl get pods 显示路由器和引擎 pods 都运行 1/1

  7. 在云池日志中检查模型加载进度。

    kubectl logs -f deployment/vllm-gpt-oss-deployment-vllm

    等到您看到一条消息,指示模型已加载并且服务器已准备好接受请求。

任务 10:测试推断端点

验证部署是否正在提供推断请求。vLLM 生产堆栈通过路由器服务公开与 OpenAI 兼容的 API,因此任何 OpenAI SDK 客户端或 curl 命令都可以与其交互。

下图显示了推断请求生命周期:从客户机请求到路由器的引擎选择逻辑,再到 vLLM 引擎的预填充和解码阶段,然后作为流响应返回。

显示从客户端到路由器再到 vLLM 引擎和 GPU 的推断请求生命周期的顺序图

  1. 列出可用模型以确认部署运行状况良好。

    kubectl get svc vllm-router-service

    路由器服务为所有已部署模型提供了 API 网关。由于集群使用专用端点,因此可以通过 kubectl port-forward 访问服务。

  2. 从本地计算机启动端口转发到路由器服务。打开一个新终端(保持 SSH 隧道在另一个终端中运行)并运行:

    kubectl port-forward svc/vllm-router-service 8080:80

    这会将计算机上的 localhost:8080 映射到群集内路由器服务上的端口 80。

    安全注意:kubectl port-forward 在本地绑定,不会公开服务。通过堡垒隧道使用专用集群时,这是最安全的测试方法。

    注: port-forward 命令在前台运行。测试时保持此终端打开状态。按 Ctrl+C 完成后将其停止。

  3. 在另一个终端中,通过查询模型端点来验证模型是否可用。

    curl -s http://localhost:8080/v1/models | python3 -m json.tool

    预期输出:

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. 发送文本完成请求。

    curl -s http://localhost:8080/v1/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "prompt": "Oracle Cloud Infrastructure is",
        "max_tokens": 50
      }' | python3 -m json.tool

    预期输出(缩写):

    {
        "id": "cmpl-xxxxxxxxxxxx",
        "object": "text_completion",
        "model": "openai/gpt-oss-20b",
        "choices": [
            {
                "index": 0,
                "text": " a cloud computing platform that provides ...",
                "finish_reason": "length"
            }
        ],
        "usage": {
            "prompt_tokens": 5,
            "completion_tokens": 50,
            "total_tokens": 55
        }
    }
  5. 发送聊天完成请求。这是 OpenAI Python SDK 使用的相同格式,也是以编程方式与 LLM 进行交互的最常见方式。

    curl -s http://localhost:8080/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "messages": [{"role": "user", "content": "What is Oracle Cloud Infrastructure in one sentence?"}],
        "max_tokens": 100
      }' | python3 -m json.tool

    预期输出(缩写):

    {
        "id": "chatcmpl-xxxxxxxxxxxx",
        "object": "chat.completion",
        "model": "openai/gpt-oss-20b",
        "choices": [
            {
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": "Oracle Cloud Infrastructure (OCI) is Oracle's enterprise-grade cloud platform that provides a full range of services for building, deploying, and managing applications and workloads..."
                },
                "finish_reason": "length"
            }
        ],
        "usage": {
            "prompt_tokens": 71,
            "completion_tokens": 100,
            "total_tokens": 171
        }
    }

    这两种响应都包括模型在 choices 数组中生成的文本、令牌使用情况统计信息以及 stop(模型自然完成)或 length(输出在 max_tokens 中被截断)的 finish_reason

    注: API 请求中的模型名称是完整模型路径 (openai/gpt-oss-20b),它与 Helm 值中的 modelURL 字段匹配。任何与 OpenAI 兼容的客户机都可以使用此端点,方法是将 base_url 设置为 http://localhost:8080/v1

    终端输出,显示来自 openai/gpt-oss-20b 的 curl 聊天完成请求和 JSON 响应

  6. 针对短请求评估端到端延迟。

    time curl -s http://localhost:8080/v1/chat/completions \
        -H "Content-Type: application/json" \
        -d '{"model":"openai/gpt-oss-20b","messages":[{"role":"user","content":"What is Kubernetes?"}],"max_tokens":50}' > /dev/null

    在单个 A10 GPU 上,需要 1-3 秒的端到端完成。第一个令牌 (TTFT) 的时间通常为 50-200 毫秒,具体取决于提示长度。为了提高吞吐量,在 Helm 值中增加 replicaCount 以在路由器后面添加更多引擎副本。

任务 11:通过 OCI 负载平衡器公开(可选)

使推断端点可通过 OCI 负载平衡器外部访问。

安全注意:默认情况下,这会将推理 API 公开给公共互联网。在没有 TLS、验证(API 密钥/JWT/mTLS)和 IP 允许列表或 WAF 控制的情况下,不要在生产环境中启用此功能。如果必须将其公开,请在它前面添加一个入口控制器或 API 网关,以强制实施验证和速率限制,或者使用内部负载平衡器。

  1. 为路由器服务打补丁以使用 LoadBalancer 类型。

    kubectl patch svc vllm-router-service \
      -p '{"spec": {"type": "LoadBalancer"}}'

    注:如果需要内部负载平衡器,请向服务添加 OCI 注释(示例如下)。这会将端点保持在 VCN 内。

    kubectl annotate svc vllm-router-service \
      "service.beta.kubernetes.io/oci-load-balancer-internal"="true"
  2. 等待分配外部 IP。

    kubectl get svc vllm-router-service -w

    预配负载平衡器后预计输出:

    NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)
    vllm-router-service    LoadBalancer   10.96.x.x      129.xxx.xxx.xx   80:xxxxx/TCP
    kubectl get svc

    kubectl 获取显示路由器和引擎服务的 svc 输出

  3. 测试外部端点。

    curl http://<EXTERNAL-IP>/v1/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "prompt": "Hello from OCI!",
        "max_tokens": 50
      }'

任务 12:配置多 GPU Tensor 并行性(高级)

在裸金属配置上的多个 GPU 上部署更大的模型。

张量并行度在单个节点上的多个 GPU 上拆分模型。当模型的内存要求超过单个 GPU 时,这是必需的。例如,Meta Llama 3.1 70B 需要大约 140 GB 的 GPU 内存,这超过了任何单个 GPU 的容量,但适用于 8x A100 80 GB 或 8x H100 GPU。

  1. 使用 Hugging Face 令牌创建 Kubernetes 密钥。Llama 3.1 70B 等受限模型需要验证。

    kubectl create secret generic hf-token-secret \
      --from-literal=token=YOUR_HUGGINGFACE_TOKEN
  2. 使用多 GPU 配置更新 production_stack_specification.yaml

    servingEngineSpec:
      modelSpec:
      - name: "llama70b"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "meta-llama/Llama-3.1-70B-Instruct"
    
        replicaCount: 1
        tensorParallelSize: 8
    
        requestCPU: 32
        requestMemory: "256Gi"
        requestGPU: 8
    
        hf_token:
          secretName: "hf-token-secret"
          secretKey: "token"
    
        pvcStorage: "500Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          node.kubernetes.io/instance-type: "BM.GPU.H100.8"
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.95"
          - "--tensor-parallel-size=8"
  3. 使用更新的值进行部署。与任务 9 一样,不要使用 --wait。路由器将 CrashLoop,直到打补丁为止。

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml
    kubectl wait --for=condition=Ready pods -l model=llama70b --timeout=900s

    然后修补路由器(与任务 9、步骤 4 相同)并验证:

    kubectl patch deployment vllm-deployment-router --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations", "value": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}]},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "512Mi"},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/limits/memory", "value": "1Gi"}
    ]'
  4. 验证 pod 是否正在运行并且所有 GPU 都正在使用中。

    kubectl get pods
    kubectl logs -f deployment/vllm-llama70b-deployment-vllm

注:在部署多 GPU 配置之前,请确保 OKE 集群具有具有具有相应裸金属 GPU 配置(例如 BM.GPU.H100.8)的节点池。

任务 13:将 OCI 对象存储用于模型(高级)

从 OCI Object Storage 加载模型权重,而不是从 Hugging Face 下载。这对于专用模型、OCI 中的更快下载或没有外部 Internet 访问的环境非常有用。

  1. 将模型权重上载到 OCI 对象存储桶。在 OCI 控制台中导航到 Storage(存储) > Object Storage(对象存储),如果您还没有存储桶,请创建一个存储桶。

  2. 为存储桶创建预先验证的请求 (PAR) URL。在 OCI 控制台中,选择存储桶,单击预先验证的请求,然后创建一个具有读取访问权限的新请求。

  3. 更新 production_stack_specification.yaml 以使用 PAR URL。

    servingEngineSpec:
      modelSpec:
      - name: "custom-model"
        repository: "iad.ocir.io/YOUR_TENANCY/vllm-custom:latest"
        modelURL: "/models/custom-model"
    
        env:
          - name: BUCKET_PAR_URL
            value: "https://objectstorage.us-ashburn-1.oraclecloud.com/p/xxx/n/namespace/b/bucket/o"
          - name: MODEL_NAME
            value: "custom-model"
  4. 使用更新的值进行部署(不使用 --wait)。有关原因,请参见任务 9)。

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml
    kubectl wait --for=condition=Ready pods -l model=custom-model --timeout=600s
  5. 通过检查云池日志来验证模型从对象存储加载。

    kubectl logs -f deployment/vllm-custom-model-deployment-vllm

任务 14:清除资源

删除所有已部署的资源以避免持续收费。

  1. 使用清理脚本删除 Kubernetes 资源。

    cd production-stack/deployment_on_cloud/oci
    ./clean_up.sh

    这将卸载 Helm 发行版,删除所有 PersistentVolumeClaims、PersistentVolumes 和定制 vLLM 资源。

  2. 删除 OKE 集群和所有 OCI 网络资源。

    ./entry_point.sh cleanup

    这将按顺序删除以下资源:

    • GPU 节点池
    • OKE 集群
    • 堡垒主机(如果已创建)
    • 子网(API、worker、负载平衡器、堡垒)
    • 安全列表
    • Service Gateway、NAT Gateway 和 Internet Gateway
    • 路由表
    • VCN
  3. 验证是否已在 OCI 控制台的开发人员服务 > Kubernetes 集群网络 > 虚拟云网络下删除了所有资源。

    ./entry_point.sh cleanup

    显示完全资源拆卸的 entry_point.sh 清除的终端输出

注:确保删除所有资源以避免持续收费。GPU 实例和块存储卷在闲置时也会产生成本。

下一步操作

本教程部署了一个功能推断堆栈。对于生产负载,请考虑以下增强功能:

确认

更多学习资源

通过 docs.oracle.com/learn 浏览其他实验室,或者通过 Oracle Learning YouTube 频道访问更多免费学习内容。此外,请访问 education.oracle.com/learning-explorer 以成为 Oracle Learning Explorer。

有关产品文档,请访问 Oracle 帮助中心