Wait for MPS daemon readiness before advertising shared resources - #1946
jonathan-meiri wants to merge 1 commit into
Conversation
6966a94 to
bfcb598
Compare
|
Thanks for the contribution. We will review this soon. |
bfcb598 to
827e00b
Compare
ed8adcd to
13985c2
Compare
13985c2 to
cc570b7
Compare
cc570b7 to
53d5ad1
Compare
|
Trimmed all four, comments now ≤2 lines. Thanks @tariq1890! |
53d5ad1 to
6e1d955
Compare
| @@ -0,0 +1,43 @@ | |||
| /** | |||
| # Copyright 2026 NVIDIA CORPORATION | |||
There was a problem hiding this comment.
| # Copyright 2026 NVIDIA CORPORATION | |
| # Copyright (c) NVIDIA CORPORATION. All rights reserved. |
| // TODO: Check the .ready file here. | ||
| // TODO: Have some retry strategy here. | ||
|
|
||
| return wait.PollUntilContextTimeout(context.Background(), mpsReadyCheckInterval, mpsReadyCheckTimeout, true, func(context.Context) (bool, error) { |
There was a problem hiding this comment.
let's propagate a context from the caller instead of intitialising a new one here.
There was a problem hiding this comment.
Done, copyright header fixed, and context is now propagated from the caller
(Start(ctx, ...) → waitForDaemon(ctx)). Thanks!
| @@ -62,12 +72,26 @@ func (m *mpsOptions) waitForDaemon() error { | |||
There was a problem hiding this comment.
| func (m *mpsOptions) waitForDaemon(ctx context.Context) error { |
| } | ||
| } | ||
| readyFile, err := os.Create("/mps/.ready") | ||
| readyFile, err := os.Create(mps.ContainerRoot.ReadyFilePath()) |
There was a problem hiding this comment.
Can we remove .ready before starting the mps daemons? /mps is a hostPath, so the file can survive a container restart. A stale .ready can make the device plugin think mps is ready before the new daemon has finished applying the new config.
| // finished initialization (compute mode, memory limits, thread percentages). | ||
| func (d *Daemon) Ready() bool { | ||
| _, err := os.Stat(d.root.ReadyFilePath()) | ||
| return err == nil |
There was a problem hiding this comment.
Can Ready() return an error as well? Right now any os.Stat error is treated as not ready, so a missing file looks the same as a permission or filesystem error.
| require.False(t, d.Ready(), "not ready before the .ready file exists") | ||
|
|
||
| require.NoError(t, os.WriteFile(filepath.Join(root, ".ready"), nil, 0o644)) | ||
| require.True(t, d.Ready(), "ready once the .ready file exists") |
There was a problem hiding this comment.
Can we add regression tests for this change? The current tests only check .ready file presence. They don't cover the race this PR fixes, such as the mps pipe being healthy before .ready is created.
There was a problem hiding this comment.
All three addressed:
- Clear stale .ready at daemon startup (hostPath can survive restarts).
- Ready() now returns (bool, error) so real stat errors surface.
- Added regression tests: readiness is withheld until .ready exists (before the pipe is checked), plus a stat-error case.
Thanks for the careful review!
There was a problem hiding this comment.
Thanks, these address the earlier comments. I left one more comment about a possible config update race.
c596c7b to
5773b45
Compare
| // checkDaemonReady requires the .ready file (written after full configuration) | ||
| // and a responsive pipe; AssertHealthy alone responds before config is applied. | ||
| func (m *mpsOptions) checkDaemonReady() error { | ||
| ready, err := m.daemon.Ready() |
There was a problem hiding this comment.
One case I think is still possible: say the current config is A and it changes to B. The device plugin can observe B before the MPS daemon does. In that window, .ready may still be from A and the MPS pipe may still be healthy, so this check can succeed before MPS has applied B.
This should converge once the MPS daemon processes the update, so I don't think it needs to block this PR, but ideally readiness should also confirm that MPS is running the expected config
There was a problem hiding this comment.
Good catch, you're right this gates startup but not a config change. Opened #2055 to track it; I'll follow up there once this PR merges.
Thanks!
5773b45 to
f0d0e7d
Compare
The device plugin's waitForDaemon only ran a single AssertHealthy check
before serving and registering the resource with kubelet. AssertHealthy
issues get_default_active_thread_percentage, which only proves the MPS
control pipe is responsive — and the pipe becomes responsive at
Daemon.Start (mpsControlBin -d) before the per-device pinned memory
limits and active thread percentage are applied. A pod scheduled in
that window starts against MPS with the daemon defaults (no pinned
memory limit, 100% threads) rather than the configured limits,
silently bypassing the intended isolation.
The MPS control daemon already creates a node-global .ready file, but
only after every daemon's full initialization completes. Nothing
consumed it (the two TODOs in waitForDaemon noted exactly this), so the
readiness signal was unused.
Gate readiness on that file:
- Add Root.ReadyFilePath so the marker path has a single definition,
and use it in the MPS control daemon for both create and remove
instead of the hardcoded "/mps/.ready".
- Add Daemon.Ready, which reports whether the .ready file exists.
- Rewrite waitForDaemon to poll checkDaemonReady (Ready AND
AssertHealthy) every 5s up to a 5m bound, replacing the single
unconditional AssertHealthy. On timeout the caller fails and is
retried by the plugin manager, so the bound is per-attempt.
This closes both TODOs and ensures shared MPS resources are not
advertised until the daemon is fully configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: runatom-ai <258621014+runatom-ai@users.noreply.github.com>
Signed-off-by: Jonathan Meiri <33288957+Meiri28@users.noreply.github.com>
f0d0e7d to
07131cd
Compare
Summary
The device plugin can advertise MPS-shared resources to kubelet before the MPS daemon has finished configuring per-device memory limits and thread percentages. A pod scheduled in that window runs against MPS with the daemon defaults (no pinned memory limit, 100% threads) instead of the configured limits — silently bypassing the intended isolation.
This gates readiness on the MPS daemon's
.readyfile, closing the two existing TODOs inwaitForDaemon.Contributed by @Meiri28 on behalf of @runatom-ai.
The race
internal/plugin/server.go:Start()callsplugin.mps.waitForDaemon()beforeServe()+Register(). PreviouslywaitForDaemonran a singleAssertHealthy():AssertHealthy()issuesget_default_active_thread_percentage, which only proves the control pipe is responsive. InDaemon.Start()the ordering is:mpsControlBin -dstarts → pipe becomes responsive (AssertHealthypasses here).readyfile created (after all daemons'Start()return)If the single check lands between (1) and (4), the plugin registers as ready while the configured MPS limits are not yet in place.
The MPS control daemon already creates a node-global
.readyfile only after full initialization, but nothing consumed it — the two TODOs noted exactly this gap. Both components share the same/mpshostPath mount, so the file is visible to the device plugin.Changes
mps/root.go: addRoot.ReadyFilePath()so the marker path has a single definition.mps-control-daemon/main.go: create/remove.readyviaReadyFilePath()instead of the hardcoded/mps/.ready, keeping writer and reader in sync.mps/daemon.go: addDaemon.Ready(), reporting whether the.readyfile exists.internal/plugin/mps.go: rewritewaitForDaemonto pollcheckDaemonReady(Ready()andAssertHealthy()) every 5s up to a 5m bound. On timeout the caller fails and is retried by the plugin manager, so the bound is per-attempt.Test plan
go test ./...— full suite passes; newTestReadyFilePathandTestDaemonReadycover the readiness primitive (readiness reflects.readyfile existence).make build,make check-modules, gofmt — clean.Commits are DCO-signed.