@@ -250,13 +250,16 @@ func TestBuildNumaRequests(t *testing.T) {
250250 cpu := func (q string ) v1.ResourceRequirements {
251251 return v1.ResourceRequirements {Requests : v1.ResourceList {"cpu" : resource .MustParse (q )}}
252252 }
253- pod := & v1.Pod {Spec : v1.PodSpec {
254- InitContainers : []v1.Container {
255- {Resources : cpu ("10" )}, // ordinary init: not a steady-state request
256- {Resources : cpu ("1" ), RestartPolicy : & always }, // native sidecar: a steady-state request
253+ pod := & v1.Pod {
254+ Status : v1.PodStatus {QOSClass : v1 .PodQOSGuaranteed }, // cpu aligns only for a Guaranteed pod
255+ Spec : v1.PodSpec {
256+ InitContainers : []v1.Container {
257+ {Resources : cpu ("10" )}, // ordinary init: not a steady-state request
258+ {Resources : cpu ("1" ), RestartPolicy : & always }, // native sidecar: a steady-state request
259+ },
260+ Containers : []v1.Container {{Resources : cpu ("2" )}, {Resources : cpu ("2" )}},
257261 },
258- Containers : []v1.Container {{Resources : cpu ("2" )}, {Resources : cpu ("2" )}},
259- }}
262+ }
260263 reqs := buildNumaRequests (pod , resource_info .NewResourceVectorMap ())
261264 cores := func (v resource_info.ResourceVector ) int64 { return int64 (v .Get (resource_info .CPUIndex )) / 1000 }
262265
@@ -282,6 +285,122 @@ func TestBuildNumaRequests(t *testing.T) {
282285 })
283286}
284287
288+ // TestBuildNumaRequestsNonIntegralCPU verifies the plugin charges NUMA zones only for the CPU the
289+ // kubelet actually pins. The CPU manager gives exclusive CPUs to whole-number requests in a
290+ // Guaranteed pod only (staticPolicy.guaranteedCPUs), so a sidecar requesting fractional CPU must
291+ // not inflate the request -- summing it would reject pods the kubelet admits.
292+ func TestBuildNumaRequestsNonIntegralCPU (t * testing.T ) {
293+ always := v1 .ContainerRestartPolicyAlways
294+ resources := func (cpu , mem string ) v1.ResourceRequirements {
295+ return v1.ResourceRequirements {Requests : v1.ResourceList {
296+ "cpu" : resource .MustParse (cpu ), "memory" : resource .MustParse (mem ),
297+ }}
298+ }
299+ podWith := func (qos v1.PodQOSClass , containers ... v1.Container ) * v1.Pod {
300+ return & v1.Pod {
301+ Status : v1.PodStatus {QOSClass : qos },
302+ Spec : v1.PodSpec {Containers : containers },
303+ }
304+ }
305+ milliCores := func (v resource_info.ResourceVector ) int64 { return int64 (v .Get (resource_info .CPUIndex )) }
306+ memory := func (v resource_info.ResourceVector ) int64 {
307+ return int64 (v .Get (resource_info .NewResourceVectorMap ().GetIndex ("memory" )))
308+ }
309+
310+ main := v1.Container {Resources : resources ("44" , "16Gi" )}
311+ fractionalMain := v1.Container {Resources : resources ("700m" , "16Gi" )}
312+ fractionalSidecar := v1.Container {Resources : resources ("4500m" , "2Gi" )}
313+ integralSidecar := v1.Container {Resources : resources ("5" , "2Gi" )}
314+
315+ t .Run ("fractional sidecar does not inflate the pod-scope cpu request" , func (t * testing.T ) {
316+ reqs := buildNumaRequests (podWith (v1 .PodQOSGuaranteed , main , fractionalSidecar ),
317+ resource_info .NewResourceVectorMap ())
318+ concurrent , _ := reqs .forScope (node_info .TopologyScopePod )
319+ assert .Equal (t , int64 (44000 ), milliCores (concurrent [0 ]), "only the main container's 44 integral cpus" )
320+ assert .Equal (t , int64 (18 )<< 30 , memory (concurrent [0 ]),
321+ "memory is summed over every container: the memory manager applies no integrality filter" )
322+ })
323+
324+ t .Run ("integral sidecar is charged in full" , func (t * testing.T ) {
325+ reqs := buildNumaRequests (podWith (v1 .PodQOSGuaranteed , main , integralSidecar ),
326+ resource_info .NewResourceVectorMap ())
327+ concurrent , _ := reqs .forScope (node_info .TopologyScopePod )
328+ assert .Equal (t , int64 (49000 ), milliCores (concurrent [0 ]))
329+ })
330+
331+ t .Run ("integral sidecar is charged when the main container is fractional" , func (t * testing.T ) {
332+ reqs := buildNumaRequests (podWith (v1 .PodQOSGuaranteed , fractionalMain , integralSidecar ),
333+ resource_info .NewResourceVectorMap ())
334+ concurrent , _ := reqs .forScope (node_info .TopologyScopePod )
335+ assert .Equal (t , int64 (5000 ), milliCores (concurrent [0 ]))
336+
337+ concurrent , _ = reqs .forScope (node_info .TopologyScopeContainer )
338+ assert .Zero (t , milliCores (concurrent [0 ]), "the main container stays in the shared cpu pool" )
339+ assert .Equal (t , int64 (5000 ), milliCores (concurrent [1 ]))
340+ })
341+
342+ t .Run ("container scope zeroes only the fractional container's cpu" , func (t * testing.T ) {
343+ reqs := buildNumaRequests (podWith (v1 .PodQOSGuaranteed , main , fractionalSidecar ),
344+ resource_info .NewResourceVectorMap ())
345+ concurrent , _ := reqs .forScope (node_info .TopologyScopeContainer )
346+ assert .Equal (t , int64 (44000 ), milliCores (concurrent [0 ]))
347+ assert .Zero (t , milliCores (concurrent [1 ]), "the sidecar stays in the shared cpu pool" )
348+ assert .Equal (t , int64 (2 )<< 30 , memory (concurrent [1 ]),
349+ "the sidecar is still a memory alignment unit of its own" )
350+ })
351+
352+ t .Run ("non-Guaranteed pod aligns no cpu at all" , func (t * testing.T ) {
353+ reqs := buildNumaRequests (podWith (v1 .PodQOSBurstable , main , fractionalSidecar ),
354+ resource_info .NewResourceVectorMap ())
355+ concurrent , _ := reqs .forScope (node_info .TopologyScopePod )
356+ assert .Zero (t , milliCores (concurrent [0 ]))
357+ })
358+
359+ t .Run ("fractional init containers are zeroed in both roles" , func (t * testing.T ) {
360+ pod := & v1.Pod {
361+ Status : v1.PodStatus {QOSClass : v1 .PodQOSGuaranteed },
362+ Spec : v1.PodSpec {
363+ InitContainers : []v1.Container {
364+ {Resources : resources ("10500m" , "1Gi" )},
365+ {Resources : resources ("1500m" , "1Gi" ), RestartPolicy : & always },
366+ },
367+ Containers : []v1.Container {main },
368+ },
369+ }
370+ reqs := buildNumaRequests (pod , resource_info .NewResourceVectorMap ())
371+ concurrent , serial := reqs .forScope (node_info .TopologyScopeContainer )
372+ assert .Zero (t , milliCores (serial [0 ]), "ordinary init" )
373+ assert .Zero (t , milliCores (concurrent [0 ]), "native sidecar" )
374+
375+ podConcurrent , _ := reqs .forScope (node_info .TopologyScopePod )
376+ assert .Equal (t , int64 (44000 ), milliCores (podConcurrent [0 ]),
377+ "neither init container contributes to the pod-scope peak" )
378+ })
379+ }
380+
381+ // TestFractionalSidecarAdmits is the unit-test form of examples/numa/sidecar-cpu-inflation: a 44-cpu
382+ // main container with a 4500m sidecar must admit (the kubelet asks for 44 exclusive cpus, which fit
383+ // one zone), while rounding that sidecar to an integral 5 cpus must reject (49 fits no zone).
384+ func TestFractionalSidecarAdmits (t * testing.T ) {
385+ pp , _ , node := wiredPlugin (numaTopology (node_info .TopologyPolicySingleNUMANode , node_info .TopologyScopePod ,
386+ numaZone ("node-0" , map [string ]string {gpu : "4" , "cpu" : "47" }),
387+ numaZone ("node-1" , map [string ]string {gpu : "4" , "cpu" : "48" }),
388+ ))
389+
390+ task := func (sidecarCPU string ) * pod_info.PodInfo {
391+ t := makeGuaranteedTask ("task-" + sidecarCPU , map [string ]string {gpu : "1" , "cpu" : "44" })
392+ t .Pod .Spec .Containers = append (t .Pod .Spec .Containers , v1.Container {
393+ Resources : v1.ResourceRequirements {Requests : v1.ResourceList {"cpu" : resource .MustParse (sidecarCPU )}},
394+ })
395+ return t
396+ }
397+
398+ assert .True (t , pp .allocatable (task ("4500m" ), node ),
399+ "a fractional sidecar is not pinned, so the pod still needs only 44 cpus in one zone" )
400+ assert .False (t , pp .allocatable (task ("5" ), node ),
401+ "an integral sidecar is pinned, pushing the pod to 49 cpus, which no zone has" )
402+ }
403+
285404// TestPredicateOrdinaryInitContainer verifies an ordinary init container is checked for
286405// alignability on its own (rejected if it cannot fit a zone) but is not accumulated into the
287406// concurrent app containers' headroom.
0 commit comments