K8s+Ray+vLLM打穿大模型全生命周期(有实践步骤)

摘要

K8s + Ray + PyTorch + vLLM 是当前大模型工程里出现频率很高的一套组合,但网上大多数文章只谈"为什么好",很少给出真正能跑、能验证的完整链路。这篇文章分两部分:先做定位分析------四个组件到底各自解决什么问题、边界在哪里;再给出一条贯穿数据处理、预训练、后训练(RLHF)、在线推理、Agent 五个阶段的最小可运行示例,每一段代码和命令都对照官方文档(RayvLLMKubeRayverl)核实过,不是道听途说的拼凑。

背景与问题

"K8s+Ray+vLLM 三件套"这个说法这两年很火,但仔细看会发现两个常见误区:

第一个误区是把四个组件当成平级的可替换选项,而不是分层职责。 K8s 管的是物理资源调度(Pod 放哪台机器),Ray 管的是应用层调度(进程/Actor 放哪个 Pod、CPU/GPU 阶段怎么编排),PyTorch 是训练框架,vLLM 是推理引擎。四者不是"选一个就够"的关系,而是叠在一起才完整------单独拿 vLLM 出来是没法处理数据、也没法调度多机训练的,它只负责"给定一个模型和一批请求,尽可能快地把结果吐出来"。

第二个误区是只看"训练"和"推理"两端,忽略了中间的后训练(RLHF/post-training)阶段其实是这套组合价值最集中的地方。 训练用 PyTorch 生态(FSDP、Megatron),推理用 vLLM,这两端分开看都好理解;难的是 RLHF 这种训练和推理要在同一个训练循环里反复切换的场景------策略模型每轮既要用 FSDP 算梯度,又要用 vLLM 生成 rollout,这正是 Ray 作为"全局调度层"存在的意义:由 Ray 的 Single-Controller 模式把训练 worker 和推理 worker 编排在一起,而不是各自为政。

这篇文章会按四层职责把每个组件的定位说清楚,再用一条完整链路把它们串起来验证。

核心思路与优势

四层职责定位

层级 组件 具体职责
物理资源调度 Kubernetes 把 Pod 调度到具体节点,管理 GPU 资源配额、网络、存储卷
应用层调度与编排 Ray(KubeRay 负责在 K8s 上管理 Ray 集群) 把 Actor/Task 分配到 Pod 内的具体进程,支持异构 CPU/GPU 资源请求、动态伸缩、细粒度容错
训练框架 PyTorch 生态(FSDP2、Megatron-LM 等) 提供分布式训练的具体实现------模型分片、梯度同步、混合精度
推理引擎 vLLM 用 PagedAttention 和 continuous batching 做高吞吐推理,同时提供 OpenAI 兼容 API

这四层里,K8s 和 Ray 的边界最容易混淆:K8s 的调度粒度是 Pod,一旦 Pod 起来了,K8s 不管这个 Pod 内部怎么再分配任务;Ray 恰好接着往下管,把 Pod 内的资源再细分给不同的 Ray Actor,这才能实现"同一个 Pod 里跑 CPU 解码 Actor 和 GPU 推理 Actor"这种细粒度混合调度。KubeRay 就是这两层的粘合剂------它是一个 K8s Operator,提供 RayClusterRayJobRayService 三种 CRD,让 K8s 原生地管理 Ray 集群的生命周期。

安装配置

安装 KubeRay operator(管理 Ray 集群的 K8s Operator):

bash 复制代码
helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm install kuberay-operator kuberay/kuberay-operator --version 1.6.2
kubectl get pods   # 确认 kuberay-operator Pod 处于 Running

本地开发环境安装 Ray(用于编写/调试训练和数据处理代码):

bash 复制代码
pip install -U "ray[data,train,tune,serve]"   # 支持 Python 3.10-3.13

[data,train,tune,serve] 是按用途拆分的可选依赖组,不需要 RLlib 或 Ray Core 的最小子集就不用装全量。

安装 vLLM(推理引擎,推荐用 uv 管理环境):

bash 复制代码
uv venv --python 3.12 --seed --managed-python
source .venv/bin/activate
uv pip install vllm --torch-backend=auto

官方明确建议不要从 conda 装 PyTorch 再装 vLLM------conda 版 PyTorch 会静态链接 NCCL,容易和 vLLM 自带的 NCCL 冲突,最好用全新虚拟环境安装。vLLM 预编译二进制默认基于 CUDA 12.9,支持 Python 3.10-3.13。

KubeRay 上拉起一个最小 Ray 集群: 一个 RayCluster CRD 由 headGroupSpec(头节点,暴露 6379 GCS 端口、8265 Dashboard 端口、10001 Client 端口)和一至多个 workerGroupSpecs(工作节点组,可配置 minReplicas/maxReplicas 做自动伸缩)组成,官方提供了从最小示例到生产级配置的完整样例(ray-cluster.complete.yaml),实际部署时建议直接改这个文件而不是从零写。

面向人群

  • 正在搭建或评估大模型训练/推理基础设施的平台工程师,想知道这套组合具体怎么落地而不是停留在架构图层面
  • 做 RLHF/后训练研究,需要在训练和推理之间高效切换资源的算法工程师
  • 想理解"为什么不能只用 vLLM 就够了"这类定位问题的技术决策者

实践步骤:贯穿大模型全生命周期的最小可运行示例

以下五段代码覆盖数据处理→预训练→后训练→在线推理→Agent。受限于篇幅和硬件门槛,"预训练"和"后训练"两段用的是小模型/小规模配置做可验证的最小示例,不是生产级训练脚本------但用的都是官方真实 API 和真实命令行入口,把模型和数据规模换大就是生产配置,架构不需要重新设计。

阶段一:数据处理(Ray Data)

Ray Data 是这条链路里真正干"处理"这个词的引擎------不是 vLLM。它用流式执行模型,支持在同一条 pipeline 里混合 CPU 算子(解码、清洗)和 GPU 算子(embedding 提取),并按 stage 自动做背压控制:

python 复制代码
import ray

ray.init()

# 读取 Parquet 格式的文本数据(也支持 read_images / read_videos 等多模态格式)
ds = ray.data.read_parquet("s3://your-bucket/raw-corpus/")

def clean_and_tokenize(batch):
    # 这里放实际的清洗/分词逻辑,batch 是一个 dict of numpy arrays
    batch["text"] = [t.strip() for t in batch["text"]]
    return batch

# CPU 阶段:清洗
ds = ds.map_batches(clean_and_tokenize, batch_format="numpy", num_cpus=1)

# 写出处理后的分片,供下一阶段(预训练)读取
ds.write_parquet("s3://your-bucket/processed-corpus/")

map_batchesnum_cpus/num_gpus 参数就是前面说的"Ray 在 Pod 内做细粒度资源分配"的具体体现------同一条 ds 链上不同的 map_batches 调用可以请求不同的资源,Ray Data 会自动做流水线编排。

阶段二:预训练(Ray Train + PyTorch FSDP2)

这一段用 Ray 官方验证过的 TorchTrainer + ScalingConfig API,配合 PyTorch 2.x 的 FSDP2(torch.distributed.fsdp.fully_shard)做模型分片训练:

python 复制代码
import torch
import torch.nn as nn
from torch.distributed.fsdp import fully_shard

import ray.train
from ray.train.torch import TorchTrainer
from ray.train import ScalingConfig, RunConfig

class ToyTransformerBlock(nn.Module):
    def __init__(self, dim=256):
        super().__init__()
        self.attn = nn.MultiheadAttention(dim, num_heads=4, batch_first=True)
        self.ffn = nn.Sequential(nn.Linear(dim, dim * 4), nn.GELU(), nn.Linear(dim * 4, dim))

    def forward(self, x):
        a, _ = self.attn(x, x, x)
        return self.ffn(x + a)

class ToyModel(nn.Module):
    def __init__(self, dim=256, n_layers=4):
        super().__init__()
        self.blocks = nn.ModuleList([ToyTransformerBlock(dim) for _ in range(n_layers)])

    def forward(self, x):
        for block in self.blocks:
            x = block(x)
        return x

def train_loop_per_worker(config):
    model = ToyModel()
    # FSDP2 用法:自底向上逐层包裹,再包裹根模块(PyTorch 官方推荐顺序)
    for block in model.blocks:
        fully_shard(block)
    fully_shard(model)

    optimizer = torch.optim.AdamW(model.parameters(), lr=config["lr"])
    ctx = ray.train.get_context()
    print(f"worker rank={ctx.get_world_rank()} world_size={ctx.get_world_size()}")

    for step in range(config["train_steps"]):
        x = torch.randn(8, 32, 256)  # 演示用随机数据,真实场景替换为阶段一产出的分片
        loss = model(x).mean()
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

trainer = TorchTrainer(
    train_loop_per_worker=train_loop_per_worker,
    train_loop_config={"lr": 1e-4, "train_steps": 100},
    scaling_config=ScalingConfig(num_workers=4, use_gpu=True),
    run_config=RunConfig(name="toy-pretrain-demo"),
)
result = trainer.fit()

ScalingConfig(num_workers=4, use_gpu=True) 就是"要几台机器、要不要 GPU"这个决策的完整代码化------把 num_workers 从 4 改成 400,把 ToyModel 换成真实的模型定义,就是从演示脚本变成生产预训练任务的完整路径,中间不需要改架构。完整的官方 FSDP2 教程见 docs.ray.io 的 Ray Train PyTorch FSDP2 示例

阶段三:后训练/RLHF(verl,Ray + FSDP + vLLM 协同的地方)

这是整条链路里 Ray 价值最直接的一段:RLHF 训练循环里,策略模型既要用 FSDP 算梯度更新参数,又要频繁用 vLLM 生成 rollout 样本,两个角色的资源需求完全不同(训练要显存装优化器状态,推理要显存装 KV cache),verl(字节跳动开源的 HybridFlow 论文实现)用 Ray 的 Single-Controller 模式把这两者编排在同一个训练循环里:

bash 复制代码
# 安装(官方推荐 Docker,这里给出核心两步)
git clone https://github.com/verl-project/verl && cd verl
pip3 install -e ".[vllm]"

# 用 GRPO 算法跑后训练,rollout 阶段用 vLLM 做推理生成
python3 -m verl.trainer.main_ppo \
    algorithm.adv_estimator=grpo \
    algorithm.use_kl_in_reward=False \
    actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \
    actor_rollout_ref.actor.use_kl_loss=True \
    actor_rollout_ref.actor.fsdp_config.param_offload=False \
    actor_rollout_ref.rollout.name=vllm \
    actor_rollout_ref.rollout.tensor_model_parallel_size=1 \
    actor_rollout_ref.rollout.gpu_memory_utilization=0.5 \
    data.train_files=$HOME/data/gsm8k/train.parquet \
    data.val_files=$HOME/data/gsm8k/test.parquet \
    trainer.n_gpus_per_node=1 \
    trainer.nnodes=1 \
    trainer.total_epochs=1

actor_rollout_ref.rollout.name=vllm 这一行就是训练和推理交接的地方------verl 把 vLLM 作为可插拔的 rollout 后端(另一个选项是 SGLang),训练阶段用的是 actor_rollout_ref.actor.fsdp_config.* 系列参数控制的 FSDP/FSDP2(大规模场景可换成 Megatron-LM 参数)。这个命令是从 verl 官方仓库的真实 PPO / GRPO 示例脚本精简而来,把模型路径和 trainer.n_gpus_per_node/trainer.nnodes 换成实际规模,就是生产配置------verl 官方给出的参考规模是最高可扩展到 671B 模型和数百张 GPU。

阶段四:在线推理(Ray Serve LLM,vLLM 作为后端引擎)

单机部署直接 vllm serve 就够;需要多副本自动伸缩、和其他 Ray 应用共享集群资源时,用 Ray Serve LLM 把 vLLM 包一层:

python 复制代码
from ray import serve
from ray.serve.llm import LLMConfig, build_openai_app

llm_config = LLMConfig(
    model_loading_config={
        "model_id": "qwen2.5-7b-instruct",
        "model_source": "Qwen/Qwen2.5-7B-Instruct",
    },
    deployment_config={
        "autoscaling_config": {"min_replicas": 1, "max_replicas": 2},
    },
    accelerator_type="A10G",
    engine_kwargs={"tensor_parallel_size": 1},
)

app = build_openai_app({"llm_configs": [llm_config]})
serve.run(app, blocking=True)

这段代码的整体结构(LLMConfig 字段、build_openai_app 用法)逐字来自 Ray 官方文档(docs.ray.io/en/latest/serve/llm),官方示例原本用的是 Qwen2.5-0.5B-Instruct + tensor_parallel_size=2,这里换成 Qwen2.5-7B-Instruct + tensor_parallel_size=1(7B 模型单张 A10G 显存足够,不需要再切两张卡),原因见阶段五末尾的说明。engine_kwargs 里能传的参数和 vllm serve 命令行的参数基本对齐,也就是说 vLLM 本身的能力(PagedAttention、量化、多模态支持)在 Ray Serve LLM 这层完全没有损耗,Ray 只是加了一层自动伸缩和多模型路由。跑起来之后,serve.run 暴露的就是一个标准 OpenAI 兼容的 HTTP 端点,服务已经在跑,阶段五不需要再起一个新的。

阶段五:Agent(复用阶段四的服务,追加工具调用能力)

Agent 应用本质上是"LLM 推理 + 工具调用循环"。阶段四已经有一个 Ray Serve LLM 服务在跑了,这里不需要另起一个 vLLM 进程 ------Ray Serve LLM 的 engine_kwargs 会把 vLLM 的工具调用相关参数原样透传给底层引擎,只需要在阶段四的 LLMConfig 里追加两个 key,重新 serve.run 一次,同一个服务就同时具备了普通推理和工具调用能力(这个透传机制来自 Ray/Anyscale 官方文档:docs.anyscale.com/llm/serving/tool-function-calling):

python 复制代码
llm_config = LLMConfig(
    model_loading_config={
        "model_id": "qwen2.5-7b-instruct",
        "model_source": "Qwen/Qwen2.5-7B-Instruct",
    },
    deployment_config={
        "autoscaling_config": {"min_replicas": 1, "max_replicas": 2},
    },
    accelerator_type="A10G",
    engine_kwargs={
        "tensor_parallel_size": 1,
        "enable_auto_tool_choice": True,   # 对应 vllm serve 的 --enable-auto-tool-choice
        "tool_call_parser": "hermes",      # 对应 vllm serve 的 --tool-call-parser
    },
)
app = build_openai_app({"llm_configs": [llm_config]})
serve.run(app, blocking=True)   # 重新部署同一个 app,端口和地址不变

tool_call_parserhermes 是因为 Qwen2.5 系列的 tokenizer_config.json 已经内置了 Hermes 风格的工具调用模板(这一点来自 vLLM 官方工具调用文档 的模型支持列表),不需要像部分模型那样额外指定 --chat-template 文件------这也是选 Qwen2.5-7B 而不是别的模型的原因,能让阶段四和阶段五的配置差异只有这两行 engine_kwargs,不用节外生枝地处理模板文件。

客户端代码和标准 OpenAI SDK 工具调用完全一样,直接连阶段四/五共用的那个地址:

python 复制代码
from openai import OpenAI
import json

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取给定位置的当前天气",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["location", "unit"],
        },
    },
}]

messages = [{"role": "user", "content": "旧金山现在天气怎么样?"}]
response = client.chat.completions.create(
    model="qwen2.5-7b-instruct",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

tool_call = response.choices[0].message.tool_calls[0].function
print(f"模型请求调用函数:{tool_call.name},参数:{tool_call.arguments}")

# Agent 循环的下一步:实际执行 get_weather(**json.loads(tool_call.arguments)),
# 把结果作为 role="tool" 的消息追加回 messages,再发一次 chat.completions.create
# 拿到模型基于工具结果生成的最终回答------这就是最小可运行的 Agent 闭环。

这段客户端代码的结构(tools 定义、tool_choice="auto"、解析 tool_calls)参照 vLLM 官方工具调用文档 的示例写法,model 字段换成了阶段四/五 LLMConfig 里定义的 model_id"qwen2.5-7b-instruct"),因为请求打到的是 Ray Serve LLM 暴露的端点,模型标识要和部署时注册的 model_id 对齐,而不是 HuggingFace 上的原始模型路径。这也是整条链路首尾呼应的地方:数据处理产出的语料喂给预训练,预训练模型经 verl 做 RLHF 后训练,训练完的模型经 Ray Serve LLM 上线,上线后的同一个 OpenAI 兼容端点,加两行 engine_kwargs 就直接被 Agent 应用复用,全程只有一个服务、一套 K8s + Ray 调度,没有另起炉灶。

(如果只是想脱离 Ray、单独验证 vLLM 的工具调用能力,vllm serve Qwen/Qwen2.5-7B-Instruct --enable-auto-tool-choice --tool-call-parser hermes 单独跑起来也能达到同样效果------但这是一条独立的调试路径,不是这条全链路 pipeline 的一部分。)

小结

回到最初的问题:这套组合不是"四选一",而是分层叠加------K8s 管物理资源,Ray 管应用层编排(尤其是训练和推理要交替的 RLHF 场景),PyTorch 提供训练能力,vLLM 提供推理能力。上面五段代码里,除了预训练和后训练用的是小规模演示配置,其余的安装命令和 API 调用都是从官方文档和真实仓库逐字核实的------把演示配置换成生产规模的机器数和模型大小,就是一条能实际跑起来的生产链路。

相关推荐
链上日记8 小时前
WEEX TradFi交易边界正在消失
人工智能
ctlover8 小时前
AI应用项目开发源码级教程:AI智能伴侣
人工智能
jonyleek8 小时前
JVS-Logic 实践指南:基于私有化与柔性配置的企业级逻辑引擎落地方法
人工智能·低代码·私有化部署·企业集成·逻辑引擎·流程编排·jvs
诗句藏于尽头8 小时前
deepseek harness对接商汤科技免费大模型使用教程
人工智能·科技·学习
兰亭妙微UI设计公司8 小时前
兰亭妙微用户体验公司分享:《AI 体验设计》第 3 部分:指导原则
人工智能
阿萨德528号8 小时前
DeepSeek Harness 开源了,但先别叫“正式版”:从封闭内测到 Developer Preview,只隔了两周
人工智能·deepseek·harness
轻松,带微笑8 小时前
美拓GEO平台技术升级:六大AI平台监测+多模态视频能力,赋能品牌抢占AI营销新赛道
人工智能
MEIXIFU18 小时前
便利店实际经营面积怎么选最合适
大数据·人工智能·物联网·生活·迭代加深
就是一顿骚操作8 小时前
VGG:用小卷积块把 CNN 做深的经典解读
人工智能·深度学习·神经网络·cnn·论文解读
奈斯先生Vector8 小时前
AI 视频不是会动的图片:用 Shot Contract、时间码与音画质检构建可交付流水线
人工智能·重构·架构·prompt·aigc·音视频