Skip to content

Commit fcdd6c7

Browse files
radu-malliuclaude
andcommitted
feat(mps): grant full GPU resources when a container requests all replicas
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7a906f3 commit fcdd6c7

3 files changed

Lines changed: 70 additions & 14 deletions

File tree

cmd/mps-control-daemon/mps/daemon.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -249,32 +249,34 @@ func (d *Daemon) setComputeMode(mode computeMode) error {
249249
return nil
250250
}
251251

252-
// perDevicePinnedMemoryLimits returns the pinned memory limits for each device.
252+
// perDevicePinnedDeviceMemoryLimits returns the pinned memory limits for each
253+
// device as the full physical memory. Per-client throttling for shared
254+
// (non-full-node) requests is applied by the device plugin via the
255+
// CUDA_MPS_PINNED_DEVICE_MEM_LIMIT env var in the Allocate response; the MPS
256+
// server default must be at least that high or the env var is clamped down.
253257
func (m *Daemon) perDevicePinnedDeviceMemoryLimits() map[string]string {
254258
totalMemoryInBytesPerDevice := make(map[string]uint64)
255-
replicasPerDevice := make(map[string]uint64)
256259
for _, device := range m.Devices() {
257-
index := device.Index
258-
totalMemoryInBytesPerDevice[index] = device.TotalMemory
259-
replicasPerDevice[index] += 1
260+
totalMemoryInBytesPerDevice[device.Index] = device.TotalMemory
260261
}
261262

262263
limits := make(map[string]string)
263264
for index, totalMemory := range totalMemoryInBytesPerDevice {
264265
if totalMemory == 0 {
265266
continue
266267
}
267-
replicas := replicasPerDevice[index]
268-
limits[index] = fmt.Sprintf("%vM", totalMemory/replicas/1024/1024)
268+
limits[index] = fmt.Sprintf("%vM", totalMemory/1024/1024)
269269
}
270270
return limits
271271
}
272272

273+
// activeThreadPercentage returns the server-side default active thread
274+
// percentage. Set to 100 so it does not cap per-client env-var overrides;
275+
// per-client throttling is applied via CUDA_MPS_ACTIVE_THREAD_PERCENTAGE in
276+
// the Allocate response.
273277
func (m *Daemon) activeThreadPercentage() string {
274278
if len(m.Devices()) == 0 {
275279
return ""
276280
}
277-
replicasPerDevice := len(m.Devices()) / len(m.Devices().GetUUIDs())
278-
279-
return fmt.Sprintf("%d", 100/replicasPerDevice)
281+
return "100"
280282
}

internal/plugin/mps.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package plugin
1919
import (
2020
"errors"
2121
"fmt"
22+
"sort"
23+
"strings"
2224

2325
"k8s.io/klog/v2"
2426
pluginapi "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1"
@@ -71,7 +73,7 @@ func (m *mpsOptions) waitForDaemon() error {
7173
return nil
7274
}
7375

74-
func (m *mpsOptions) updateReponse(response *pluginapi.ContainerAllocateResponse) {
76+
func (m *mpsOptions) updateReponse(response *pluginapi.ContainerAllocateResponse, grantedCount int) {
7577
if m == nil || !m.enabled {
7678
return
7779
}
@@ -88,4 +90,54 @@ func (m *mpsOptions) updateReponse(response *pluginapi.ContainerAllocateResponse
8890
HostPath: m.hostRoot.ShmDir(m.resourceName),
8991
},
9092
)
93+
94+
// The MPS control daemon is configured with per-device defaults at full
95+
// hardware. When a container has been granted every replica the plugin
96+
// advertises on this node, kubelet's accounting guarantees no other pod
97+
// holds any replica concurrently, so it can use the full daemon defaults —
98+
// no client-side env vars are needed.
99+
//
100+
// For any other (non-full-node) grant, inject the 1/replicas per-device
101+
// caps via the client env vars. The MPS server clamps the per-client value
102+
// to no more than the daemon default; since the default is now full, these
103+
// env vars are the effective cap. This preserves the historical behavior
104+
// where every non-full grant got 1/replicas memory and thread percentage,
105+
// regardless of how many replicas were granted.
106+
devices := m.daemon.Devices()
107+
total := len(devices)
108+
if total == 0 || grantedCount >= total {
109+
return
110+
}
111+
112+
replicasByIndex := make(map[string]uint64)
113+
totalMemoryByIndex := make(map[string]uint64)
114+
for _, device := range devices {
115+
replicasByIndex[device.Index]++
116+
totalMemoryByIndex[device.Index] = device.TotalMemory
117+
}
118+
119+
// All physical GPUs managed by this daemon share the same replicas value
120+
// (it comes from a single config); pick any.
121+
var replicasPerGPU uint64
122+
for _, n := range replicasByIndex {
123+
replicasPerGPU = n
124+
break
125+
}
126+
if replicasPerGPU == 0 {
127+
return
128+
}
129+
130+
response.Envs["CUDA_MPS_ACTIVE_THREAD_PERCENTAGE"] = fmt.Sprintf("%d", 100/replicasPerGPU)
131+
132+
limits := make([]string, 0, len(totalMemoryByIndex))
133+
for index, totalMemory := range totalMemoryByIndex {
134+
if totalMemory == 0 {
135+
continue
136+
}
137+
limits = append(limits, fmt.Sprintf("%s=%dM", index, totalMemory/replicasPerGPU/1024/1024))
138+
}
139+
if len(limits) > 0 {
140+
sort.Strings(limits)
141+
response.Envs["CUDA_MPS_PINNED_DEVICE_MEM_LIMIT"] = strings.Join(limits, ",")
142+
}
91143
}

internal/plugin/server.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ func (plugin *nvidiaDevicePlugin) getAllocateResponse(requestIds []string) (*plu
330330
}
331331
}
332332
if plugin.mps.enabled {
333-
plugin.updateResponseForMPS(response)
333+
plugin.updateResponseForMPS(response, len(requestIds))
334334
}
335335

336336
// The following modifications are only made if at least one non-CDI device
@@ -361,8 +361,10 @@ func (plugin *nvidiaDevicePlugin) getAllocateResponse(requestIds []string) (*plu
361361
// updateResponseForMPS ensures that the ContainerAllocate response contains the information required to use MPS.
362362
// This includes per-resource pipe and log directories as well as a global daemon-specific shm
363363
// and assumes that an MPS control daemon has already been started.
364-
func (plugin nvidiaDevicePlugin) updateResponseForMPS(response *pluginapi.ContainerAllocateResponse) {
365-
plugin.mps.updateReponse(response)
364+
// grantedCount is the number of replica IDs kubelet allocated to this container and
365+
// is used to decide whether to inject per-client limit env vars.
366+
func (plugin nvidiaDevicePlugin) updateResponseForMPS(response *pluginapi.ContainerAllocateResponse, grantedCount int) {
367+
plugin.mps.updateReponse(response, grantedCount)
366368
}
367369

368370
// updateResponseForCDI updates the specified response for the given device IDs.

0 commit comments

Comments
 (0)