Qwen/Qwen2.5-0.5B on CPU
A 0.5B dense chat model served on CPUs alone, on a Civo CPU pool.
View on Hugging FaceA 0.5B dense chat model served on CPUs alone, on a Civo g4p.kube.medium
pool: one Standalone engine, no cache, weights pulled straight from Hugging
Face. A CPU pool is the cheapest way to exercise a Modelplane deployment end
to end - no GPU to find, no driver to wait for - and a 0.5B model is small
enough that CPU inference answers in sensible time.
The cluster pins Kubernetes 1.36: the claim consumes the device’s cores as consumable capacity, which Kubernetes enables by default from 1.36, on an older version the engine pod fails when the node prepares its claim.
The class’s provisioning block omits accelerator, which is what makes the
pool a CPU pool: the cluster composition builds it with a standard node image
and the modelplane.ai/cpu label and taint, and the serving stack installs
dra-driver-cpu - and no GPU machinery - because the class claims dra.cpu.
The claim pins the engine to its NUMA node’s cpuset, and the engine’s CPU
request is the one number that sizes everything: Kubernetes accounts the pod
by it, and Modelplane stamps it into the claim as the share of the device to
take. Five of the six claimable cores, because the driver refuses a claim
that would take them all - the node’s shared containers keep the last one.
The engine is vLLM’s CPU build, whose image serves the same OpenAI API as the
CUDA one. The same class without its provisioning block describes a
bring-your-own CPU pool, on any conformant cluster: label its nodes
modelplane.ai/pool=cpu-pool and modelplane.ai/cpu=true instead.
Validated deployments
Platform
# An InferenceClass describing a Civo CPU node pool - no GPU at all.
#
# The provisioning block has no accelerator, so the cluster composition
# builds the pool with the CPU role: a standard node image and the
# modelplane.ai/cpu label and taint, nothing GPU. The serving stack installs
# dra-driver-cpu when a class claims dra.cpu, which publishes each node's
# cores as claimable devices (one device per NUMA node) and registers the
# dra.cpu DeviceClass.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
name: civo-cpu-8vcpu
spec:
description: "Civo g4p.kube.medium, 8 vCPUs, no GPU"
provisioning:
provider: Civo
civo:
size: g4p.kube.medium
devices:
- name: cpu
claim: DRA
driver: dra.cpu
deviceClassName: dra.cpu
count: 1
capacity:
# The node's 8 vCPUs less the two dra-driver-cpu reserves for the OS
# and kubelet (reservedCPUs 0-1), which is what the driver's
# ResourceSlices report as claimable.
cpu: { value: "6" }
# A Civo cluster in NYC1 with a single CPU node offering the class above.
# The composed ServingStack gets draDrivers: [dra.cpu], so dra-driver-cpu
# installs and the GPU device stacks stay out - a cluster with no GPU runs
# no GPU machinery.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: civo-cpu
labels:
modelplane.ai/region: nyc1
spec:
cluster:
source: Civo
civo:
region: NYC1
# CPU cores are claimed as consumable capacity, which Kubernetes
# enables by default from 1.36 (DRAConsumableCapacity) - on an older
# version the engine pod fails at NodePrepareResources. Civo's
# default version may be older, so a CPU cluster pins it; list the
# exact strings with: civo kubernetes versions.
kubernetesVersion: 1.36.0-k3s1
nodePools:
- name: cpu-pool
className: civo-cpu-8vcpu
nodeCount: 1
Deployment
# A small model served on CPUs alone. The device selector matches against
# the claimable cores the civo-cpu-8vcpu class declares, so >= 4 lands it on
# the CPU pool; the claim takes the engine's CPU request, five cores, as
# capacity from the NUMA-node device and pins the engine container to a
# cpuset. The engine is vLLM's CPU build - the default
# vllm/vllm-openai image is CUDA-only. --block-size=32 is the smallest
# block its CPU attention backend accepts (multiples of 32); Modelplane
# reads the flag and chunks the endpoint picker's prefix cache the same
# way, so routing stays aligned. VLLM_CPU_KVCACHE_SPACE sets the KV
# cache's share of the node's RAM in GiB. No ModelCache: the engine pulls
# weights straight from Hugging Face.
apiVersion: modelplane.ai/v1alpha1
kind: ModelDeployment
metadata:
name: qwen-cpu-demo
namespace: ml-team
spec:
replicas: 1
template:
spec:
engines:
- name: qwen
members:
- role: Standalone
nodeSelector:
devices:
- name: cpu
count: 1
selectors:
- cel: |
device.capacity["dra.cpu"].cpu.compareTo(quantity("4")) >= 0
template:
spec:
containers:
- name: engine
image: vllm/vllm-openai-cpu:v0.32.0-x86_64
args:
- --model=Qwen/Qwen2.5-0.5B-Instruct
- --served-model-name=$(MODELPLANE_SERVED_MODEL_NAME)
- --max-model-len=4096
- --block-size=32
env:
- name: VLLM_CPU_KVCACHE_SPACE
value: "4"
# Sizes the pod's accounting and the DRA claim alike:
# compose-model-replica stamps this as the claim's capacity
# request, and the kubelet accounts CPUs only from requests
# until KEP-5517. It must stay below the device's six
# claimable cores - a whole-device claim exhausts the
# shared CPU pool and the driver refuses to prepare it
# while the node's DaemonSets need somewhere to run. Five
# leaves them one claimable core.
resources:
requests:
cpu: "5"
limits:
cpu: "5"
# Publishes the deployment's endpoints as one model a caller can name:
# <namespace>/<service>, so ml-team/qwen-cpu here. Modelplane composes one
# ModelEndpoint per ModelReplica, labeled with the deployment name, so
# this selector reaches every replica.
apiVersion: modelplane.ai/v1alpha1
kind: ModelService
metadata:
name: qwen-cpu
namespace: ml-team
spec:
endpoints:
- name: qwen-cpu-demo
selector:
matchLabels:
modelplane.ai/deployment: qwen-cpu-demo