@@ -5,8 +5,8 @@ use std::time::Duration;
55use futures:: StreamExt ;
66use k8s_openapi:: api:: apps:: v1:: { Deployment , DeploymentSpec } ;
77use k8s_openapi:: api:: core:: v1:: {
8- Container , ContainerPort , EnvVar , HTTPGetAction , PodSpec , PodTemplateSpec , Probe ,
9- ResourceRequirements ,
8+ Container , ContainerPort , EmptyDirVolumeSource , HTTPGetAction , PodSpec , PodTemplateSpec , Probe ,
9+ ResourceRequirements , Volume , VolumeMount ,
1010} ;
1111use k8s_openapi:: apimachinery:: pkg:: api:: resource:: Quantity ;
1212use k8s_openapi:: apimachinery:: pkg:: apis:: meta:: v1:: LabelSelector ;
@@ -65,9 +65,15 @@ fn build_deployment(svc: &VllmService) -> Result<Deployment, Error> {
6565
6666 let enforce_eager = matches ! ( svc. spec. warmup_strategy, WarmupStrategy :: Eager ) ;
6767
68+ // A request for GPUs is the signal that this is a real vLLM workload
69+ // rather than the inert CI placeholder (gpu=0, pause image). Only then
70+ // do we inject the serving command, the GPU limit, the shared-memory
71+ // volume and any tuning args; the placeholder stays a do-nothing pod.
72+ let serving = svc. spec . gpu > 0 ;
73+
6874 // GPU resource limit, only when requested. With gpu=0 (CI / CPU-only)
6975 // the container requests no GPU and schedules on any node.
70- let resources = if svc . spec . gpu > 0 {
76+ let resources = if serving {
7177 let mut limits = BTreeMap :: new ( ) ;
7278 limits. insert (
7379 "nvidia.com/gpu" . to_string ( ) ,
@@ -81,6 +87,54 @@ fn build_deployment(svc: &VllmService) -> Result<Deployment, Error> {
8187 None
8288 } ;
8389
90+ // Invocation. The vllm/vllm-openai image's entrypoint is `vllm serve`,
91+ // but we set command+args explicitly so the operator does not depend on
92+ // the image's entrypoint staying stable across tags. `vllm serve` takes
93+ // the model as a positional argument; warmupStrategy maps to
94+ // --enforce-eager (CUDA graphs off => faster cold start); extraArgs
95+ // carries per-deployment engine tuning (e.g. --max-model-len,
96+ // --gpu-memory-utilization). The placeholder keeps the pause entrypoint.
97+ let ( command, args) = if serving {
98+ let mut a = vec ! [
99+ svc. spec. model. clone( ) ,
100+ "--host" . to_string( ) ,
101+ "0.0.0.0" . to_string( ) ,
102+ "--port" . to_string( ) ,
103+ "8000" . to_string( ) ,
104+ ] ;
105+ if enforce_eager {
106+ a. push ( "--enforce-eager" . to_string ( ) ) ;
107+ }
108+ a. extend ( svc. spec . extra_args . iter ( ) . cloned ( ) ) ;
109+ ( Some ( vec ! [ "vllm" . to_string( ) , "serve" . to_string( ) ] ) , Some ( a) )
110+ } else {
111+ ( None , None )
112+ } ;
113+
114+ // vLLM uses /dev/shm for intra-process tensor transfer; the container
115+ // runtime's default 64MiB is too small and causes hard-to-diagnose
116+ // crashes under load. Back it with a memory-medium emptyDir on real
117+ // serving pods. The placeholder needs none.
118+ let ( volumes, volume_mounts) = if serving {
119+ (
120+ Some ( vec ! [ Volume {
121+ name: "dshm" . to_string( ) ,
122+ empty_dir: Some ( EmptyDirVolumeSource {
123+ medium: Some ( "Memory" . to_string( ) ) ,
124+ ..Default :: default ( )
125+ } ) ,
126+ ..Default :: default ( )
127+ } ] ) ,
128+ Some ( vec ! [ VolumeMount {
129+ name: "dshm" . to_string( ) ,
130+ mount_path: "/dev/shm" . to_string( ) ,
131+ ..Default :: default ( )
132+ } ] ) ,
133+ )
134+ } else {
135+ ( None , None )
136+ } ;
137+
84138 // Readiness probe, built only when health_path is non-empty. This is
85139 // what makes "Ready" mean "warm": when set, Kubernetes marks the pod
86140 // ready only once the server answers on this endpoint, so the
@@ -104,45 +158,33 @@ fn build_deployment(svc: &VllmService) -> Result<Deployment, Error> {
104158 let container = Container {
105159 name : "inference" . to_string ( ) ,
106160 image : Some ( svc. spec . image . clone ( ) ) ,
161+ command,
162+ args,
107163 ports : Some ( vec ! [ ContainerPort {
108164 container_port: 8000 ,
109165 name: Some ( "http" . to_string( ) ) ,
110166 ..Default :: default ( )
111167 } ] ) ,
112- env : Some ( vec ! [
113- EnvVar {
114- name: "VLLM_MODEL" . to_string( ) ,
115- value: Some ( svc. spec. model. clone( ) ) ,
116- ..Default :: default ( )
117- } ,
118- EnvVar {
119- name: "VLLM_ENFORCE_EAGER" . to_string( ) ,
120- value: Some ( enforce_eager. to_string( ) ) ,
121- ..Default :: default ( )
122- } ,
123- ] ) ,
124168 resources,
125169 readiness_probe,
170+ volume_mounts,
126171 ..Default :: default ( )
127172 } ;
128173
129- // On K3s the default runtime is runc; a pod needs the "nvidia"
130- // RuntimeClass to actually get GPU access. Request it only when GPUs
131- // are requested, so gpu=0 (CI / CPU) pods schedule with the default
132- // runtime and need no RuntimeClass installed on the cluster.
133- let runtime_class_name = if svc. spec . gpu > 0 {
134- Some ( "nvidia" . to_string ( ) )
135- } else {
136- None
137- } ;
174+ // RuntimeClass is cluster-dependent and comes from the spec (default
175+ // None). Managed clusters (GKE/EKS/AKS) expose GPUs via the device
176+ // plugin with the default runtime and define no "nvidia" RuntimeClass;
177+ // setting a non-existent one makes the API server reject the pod. K3s
178+ // GPU nodes set it to "nvidia".
138179 let template = PodTemplateSpec {
139180 metadata : Some ( ObjectMeta {
140181 labels : Some ( labels. clone ( ) ) ,
141182 ..Default :: default ( )
142183 } ) ,
143184 spec : Some ( PodSpec {
144185 containers : vec ! [ container] ,
145- runtime_class_name,
186+ runtime_class_name : svc. spec . runtime_class_name . clone ( ) ,
187+ volumes,
146188 ..Default :: default ( )
147189 } ) ,
148190 } ;
0 commit comments