缘起:一台空转的 GPU

  自今年以来我在公司内部搭过几次自建大模型推理服务。流程大同小异:拉一个 vLLM 镜像,写个 Deployment,挂上 nvidia.com/gpu: 1,配个 Service,再加个 HPA。能跑,但有两件事一直让我别扭。

  第一件:模型在空转的时候,GPU 还在烧钱。 内部很多模型是「偶尔有人用」的——评测环境、给某个业务方留的测试通道、Demo 用的小模型。它们一天可能就被调用几十次,但 Pod 得 7×24 占着一整张卡。HPA 默认最小副本是 1,它不会帮你缩到 0;就算你把 minReplicas 设成 0,原生 HPA 也不支持从 0 启动——因为没有副本就没有指标,没有指标就永远不会扩容,这是个先有鸡还是先有蛋的死结。

  第二件:这套东西是「NVIDIA 优先 + 英文优先」的。 现在国内自建推理,绕不开昇腾、寒武纪这些国产卡。vLLM 社区有 vllm-ascendvllm-mlu 这些后端,但把它们编排进 K8s、做好健康检查、模型加载、缓存预热、指标采集——这部分活儿,每家都在自己那套 YAML 里重写一遍。换一种卡,整套部署模板推倒重来。

我翻了一圈现有方案:KServe、llm-d 这些是冲着数据中心级、大规模 serving 去的,重;vLLM 自己只管推理引擎,不管 K8s 编排;各家云厂商的托管方案又把你锁死在它们的平台上。没有一个东西,是专门为「少量 GPU、可以容忍冷启动、对成本敏感、还想要厂商中立」这个场景设计的。

于是我开源Hearth(hearth-project/hearth: Declarative, scale-to-zero LLM serving on Kubernetes — vendor-neutral.)——你的模型,你的炉火,平时熄着,要用的时候点起来。

一句话:把「在我的集群上跑 Qwen」变成一个声明式 manifest

Hearth 是一个 Kubernetes Operator(基于 Kubebuilder / controller-runtime)。它的目标是把一坨手写 YAML,收敛成一个 LLMService:

apiVersion: serving.hearth.dev/v1alpha1
kind: LLMService
metadata:
  name: qwen3-8b
  namespace: ai
spec:
  model:
    source:
      uri: modelscope://Qwen/Qwen3-8B-Instruct   # hf:// | modelscope://
  runtime:
    selector: { vendor: [nvidia, ascend] }        # 按偏好顺序自动选后端
  resources:
    accelerators: 1
  scaling:
    min: 0            # 缩容到零
    max: 3
    metric: queueDepth
    target: 10

kubectl apply 之后,Operator 会把它展开成一整套子对象:vLLM 的 Deployment、Service、模型缓存 PVC、预热 Job、网关(Gateway)、以及一个 KEDA 的 ScaledObject。然后你会看到:

$ kubectl get llmservice -n ai
NAME       PHASE          RUNTIME       REPLICAS   AGE
qwen3-8b   ScaledToZero   vllm-nvidia   0          30s

REPLICAS 0——没有请求的时候,它一张卡都不占。同样这份 manifest,把可用后端换成 vllm-ascend,在昇腾集群上一行不用改就能跑。这种可移植性,是我做这个项目的核心目的。

 

下面我想认真讲讲,作为维护者,这里面我觉得最有意思的两个设计。

 

设计一:缩容到零,难点根本不在「缩」,在「从零启动」

「缩到 0」很容易,没流量了把副本砍到 0 就行。真正的难点是:当一个请求打进来,后端却是 0 副本时,这个请求该怎么办?

原生 HPA 在这里是死的。所以 Hearth 在后端前面放了一个轻量的 网关(Gateway)——一个 OpenAI 协议兼容的反向代理,每个 LLMService 一个。整条数据通路是这样的:

client ──OpenAI API──▶ Hearth Gateway ──就绪后转发──▶ vLLM pods (0..N)
                            ▲                              │
                            │  轮询 /hearth/queue          │ 加载权重
                          KEDA ──────扩缩 0..N────────────▶│
                                                       模型缓存

关键点在于网关暴露的一个指标端点 /hearth/queue,它返回当前积压(pending)的请求数。这个数,就是给 KEDA 看的「需求信号」。

一次冷启动的完整过程:

  1. 空闲:KEDA 把后端 Deployment 摁在 0 副本。

  2. 冷请求到达:网关收下请求(有界队列,满了就 429 背压),把 pending 计数 +1,然后把这个连接 hold 住。在 keepalive 模式下,它会往这条 SSE 流里发心跳(: heartbeat),让客户端和网关层不要超时断开;在 reject 模式下,它直接返回 503 + Retry-After 让客户端稍后重试。

  3. 激活:KEDA 用 metrics-api 这个 scaler 去轮询网关的 /hearth/queue,一看 pending > 0,就把 Deployment 从 0 拉到 1。Pod 起来后从本地缓存(已预热)加载权重,只有当模型真正加载完、就绪探针通过,Pod 才算 Ready

  4. 服务:网关把那个一直 hold 着的请求转发给后端,开始往回 stream token。

  5. 扩容:如果队列持续高于阈值,KEDA 继续 1 → N(一个副本一整张卡)。

  6. 缩容:需求降下来,KEDA 往回缩到 0;Pod 终止前有一个 preStop 排水钩子,让还在流式输出的请求先跑完再退出。

这里有几个我踩过、也想强调的工程细节:

  • Operator 永远不设置 .spec.replicas 副本数完全归 KEDA 的 HPA 管(0..N)。Operator 只负责把「形状」reconcile 出来,谁来决定副本数,是另一回事。这个边界一旦混了,两个控制器会互相打架。

  • 冷启动会有一个「需求残留」(demandLinger)。 一个返回很快的请求,可能在 KEDA 的两次轮询间隔之间就处理完了,导致 pending 又变回 0,激活信号一闪而过、扩容没触发。所以网关会把信号多举一会儿,保证它能跨过一个轮询周期。

  • 冷启动成本主要在「拉权重 + 加载」上,所以缓存才是让「缩容到零」真正可用的前提。v0 支持 HostPath 和节点本地 PVC,外加一个预热 Job 在首次流量前把权重灌好。

设计二:厂商中立,靠的是「把差异当数据,而不是当代码」

要支持 N 种加速卡,最糟的做法是写 N 套 Operator,或者在代码里堆一堆 if vendor == "ascend"。Hearth 的做法是把后端描述成数据——一个集群级的 InferenceRuntime CRD:

apiVersion: serving.hearth.dev/v1alpha1
kind: InferenceRuntime
metadata:
  name: vllm-nvidia
spec:
  vendor: nvidia
  image: ...
  accelerator: nvidia.com/gpu        # 换昇腾就是 huawei.com/Ascend910
  args: [ ... ]                      # 带模板的启动参数
  probes: { ... }                    # 模型加载感知的探针
  metrics: { ... }                   # LLM 指标在哪

镜像、启动参数模板、设备插件资源名、探针、指标路径——这些厂商之间真正的差异,全都是数据。于是适配器(adapter)的代码可以非常薄:它只做「K8s 层的适配」——调度、健康检查、模型加载、指标采集,绝不去碰芯片算子,绝不重写 vLLM

后端 引擎 加速卡 v0 状态
vllm-nvidia NVIDIA-vLLM 英伟达 已实现,真机 GPU 验证
vllm-ascend vLLM-Ascend 昇腾 已搭好,golden 测试通过
vllm-mlu vLLM-MLU 寒武纪 规划中

加一种新卡 = 加一个薄薄的 adapter 包,在 registry 里注册一下,而不是重写整个项目。这条边界(Hearth 只做 K8s 编排/生命周期层,推理引擎交给 vLLM,算子和设备插件交给厂商和 HAMi、Volcano)是我一直克制自己不要越界的地方——一个项目想做的事越少,它能做好的概率才越大。

 

一个我自己很在意的细节:没有 GPU 也能开发和测试

厂商中立有个现实问题:贡献者大概率没有一柜子各种卡。如果改个调度逻辑都得上真机,这个项目就别想有人参与了。

所以我专门做了一套无 GPU 测试链路:一个 CPU 版的 vllm-stub(假的 vLLM,能模拟启动延迟、流式输出、/metrics),配上给节点打的假扩展资源,在 kind 集群上不需要任何加速卡,就能把完整的 0 → 1 → N → 0 缩放循环、冷启动 keepalive、优雅排水全部跑通。make test-scale-e2e 一条命令,本地或 CI 都能验证缩容到零的核心行为。

这意味着:你想给 Hearth 贡献代码,一台普通笔记本就够了。

诚实地说说现状:这是 alpha,不是成品

我不想把它包装得比实际更好。截至 v0.1.0:

 

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐