Skip to content

fix(cambricon): replace hand-rolled node lock with nodelock delegation - #2252

Closed
manmathbh wants to merge 2 commits into
Project-HAMi:masterfrom
manmathbh:fix/cambricon-nodelock
Closed

fix(cambricon): replace hand-rolled node lock with nodelock delegation#2252
manmathbh wants to merge 2 commits into
Project-HAMi:masterfrom
manmathbh:fix/cambricon-nodelock

Conversation

@manmathbh

@manmathbh manmathbh commented Aug 1, 2026

Copy link
Copy Markdown

What type of PR is this?

/kind bug

What this PR does / why we need it:

Replaced Cambricon's hand-rolled node lock with pkg/util/nodelock. Every
other backend already delegates to nodelock — Cambricon was the only one
rolling its own.

Three bugs in the old code:

  • setNodeLock patched the apiserver but did not write back to the caller's
    Node. ReleaseNodeLock read the stale in-memory object, found no lock
    annotation, logged "Node lock not set", returned nil. Lock stayed.

  • Retry loop sent the same object every attempt. Same resourceVersion,
    no backoff. Could not clear 409 conflicts.

  • delete(n.Annotations, DsmluLockTime) on the shared informer's Node
    object, no deep copy. Race with Filter reading the same map.

Which issue(s) this PR fixes:
Fixes #2251

Special notes for your reviewer:

Old DsmluLockTime (cambricon.com/dsmlu.lock) replaced with the shared
hami.io/mutex.lock annotation. Any stale lock from a previous version
expires via the 5-minute nodelock timeout.

Does this PR introduce a user-facing change?:

Cambricon MLU node locking uses the shared nodelock infrastructure.
Old lock annotations expire naturally (5-minute TTL).

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability and consistency of Cambricon node locking and unlocking.
    • Updated lock handling to use the shared node-locking mechanism.
    • Prevented unnecessary locks for pods without MLU resource requests.
    • Removed obsolete legacy lock annotations during lock processing.
  • Tests

    • Expanded coverage for lock creation, release, annotation handling, and non-MLU pod behavior.

Copilot AI review requested due to automatic review settings August 1, 2026 04:52
@hami-robot hami-robot Bot added the kind/bug Something isn't working label Aug 1, 2026
@hami-robot

hami-robot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: manmathbh
Once this PR has been reviewed and has the lgtm label, please assign wawa0210 for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Cambricon node locking now delegates to the shared nodelock utility. MLU request detection remains in place. Legacy DSMLU annotations are removed. Tests use a fake Kubernetes client to verify lock creation and release.

Changes

Cambricon node locking

Layer / File(s) Summary
Migrate Cambricon locking to nodelock
pkg/device/cambricon/device.go
Adds MLU request detection, delegates lock and unlock operations to nodelock, and removes legacy DSMLU lock annotations.
Validate lock acquisition and release
pkg/device/cambricon/device_test.go
Tests verify that MLU pods create shared node locks, non-MLU pods bypass locking, and release removes the shared annotation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: mesutoezdil, dsfans2014, ouyangluwei163

Sequence Diagram(s)

sequenceDiagram
  participant CambriconDevice
  participant nodelock
  participant KubernetesAPI
  CambriconDevice->>CambriconDevice: Check MLU resource request
  CambriconDevice->>nodelock: LockNode or ReleaseNodeLock
  nodelock->>KubernetesAPI: Create or clear shared node-lock annotation
  CambriconDevice->>KubernetesAPI: Remove legacy DSMLU annotation
Loading

Poem

A rabbit checks the MLU gate,
Then shared locks coordinate.
Old DSMLU marks hop away,
Tests confirm the node’s new state.
Hop, hop—locks release today!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes replacing the Cambricon hand-rolled node lock with shared nodelock delegation.
Linked Issues check ✅ Passed The changes address issue [#2251] by delegating lock and release operations to nodelock and adding tests for annotation cleanup.
Out of Scope Changes check ✅ Passed The implementation and test changes remain within the linked issue scope and support the node-lock delegation fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Replace the custom lock logic in Cambricon with pkg/util/nodelock, matching every other backend (NVIDIA, Ascend, etc.). The old implementation had three bugs: 1) setNodeLock patched the apiserver but never wrote back to the callers Node, so ReleaseNodeLock could not find the annotation and silently returned nil without removing the lock. 2) The retry loop reused the same object every iteration, making it unable to clear 409 conflicts. 3) delete(n.Annotations, DsmluLockTime) mutated a shared informer object without a deep copy, causing a data race. Fixes Project-HAMi#2251. Signed-off-by: Manmath Hatte <manmathcode@gmail.com>

Signed-off-by: Manmath Hatte <manmathcode@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Cambricon MLU node-locking by removing its custom annotation/patch/retry implementation and delegating lock acquisition/release to the shared pkg/util/nodelock infrastructure used by other backends, addressing the failure modes described in #2251.

Changes:

  • Replaced Cambricon’s hand-rolled node lock logic with nodelock.LockNode / nodelock.ReleaseNodeLock.
  • Removed the legacy cambricon.com/dsmlu.lock constant and related patch/update retry code paths.
  • Updated Cambricon unit tests to cover the new nodelock-based behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
pkg/device/cambricon/device.go Switches Cambricon’s node locking to the shared nodelock implementation and drops the old lock annotation code.
pkg/device/cambricon/device_test.go Reworks locking tests to validate the new shared lock annotation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 45 to 48
DsmluProfile = "CAMBRICON_DSMLU_PROFILE"
DsmluResourceAssigned = "CAMBRICON_DSMLU_ASSIGNED"
retry = 5
NodeLockMLU = "hami.io/mutex.lock"
)
Comment on lines +397 to +401
config := CambriconConfig{
ResourceCountName: MLUResourceCount,
ResourceMemoryName: MLUResourceMemory,
ResourceCoreName: MLUResourceCores,
}
Comment thread pkg/device/cambricon/device_test.go Outdated
updated, err := client.KubeClient.CoreV1().Nodes().Get(context.Background(), "test-node", metav1.GetOptions{})
assert.NoError(t, err)
_, ok := updated.Annotations[nodelock.NodeLockKey]
assert.Equal(t, ok, tt.hasLock)
Comment thread pkg/device/cambricon/device_test.go Outdated
}
updated, err := client.KubeClient.CoreV1().Nodes().Get(context.Background(), "test-node", metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, updated.Annotations[nodelock.NodeLockKey], "")

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/device/cambricon/device.go (1)

87-113: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the duplicated MLU-resource check into a shared helper.

LockNode (lines 88-94) and ReleaseNodeLock (lines 102-108) contain the identical loop that checks whether any container in p.Spec.Containers requests MLU resources. Extract this into a private helper, for example hasMLURequest(p *corev1.Pod) bool, and call it from both methods. This avoids the two lock paths silently diverging if one is edited without the other, which was part of the pattern behind the original locking bugs.

♻️ Proposed refactor
+func (dev *CambriconDevices) hasMLURequest(p *corev1.Pod) bool {
+	for _, val := range p.Spec.Containers {
+		if dev.GenerateResourceRequests(&val).Nums > 0 {
+			return true
+		}
+	}
+	return false
+}
+
 func (dev *CambriconDevices) LockNode(n *corev1.Node, p *corev1.Pod) error {
-	found := false
-	for _, val := range p.Spec.Containers {
-		if (dev.GenerateResourceRequests(&val).Nums) > 0 {
-			found = true
-			break
-		}
-	}
-	if !found {
+	if !dev.hasMLURequest(p) {
 		return nil
 	}
 	return nodelock.LockNode(n.Name, NodeLockMLU, p)
 }

 func (dev *CambriconDevices) ReleaseNodeLock(n *corev1.Node, p *corev1.Pod) error {
-	found := false
-	for _, val := range p.Spec.Containers {
-		if (dev.GenerateResourceRequests(&val).Nums) > 0 {
-			found = true
-			break
-		}
-	}
-	if !found {
+	if !dev.hasMLURequest(p) {
 		return nil
 	}
 	return nodelock.ReleaseNodeLock(n.Name, NodeLockMLU, p, false)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/device/cambricon/device.go` around lines 87 - 113, Extract the duplicated
container scan from LockNode and ReleaseNodeLock into a private helper such as
hasMLURequest(p *corev1.Pod) bool, preserving the existing
GenerateResourceRequests check and early return behavior. Replace both methods’
local found-loop logic with calls to this shared helper, while leaving their
respective lock and release operations unchanged.
🧹 Nitpick comments (1)
pkg/device/cambricon/device.go (1)

43-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused Cambricon node-lock constant.

nodelock.SetNodeLock and nodelock.ReleaseNodeLock only read/write NodeLockKey; they ignore the lockname argument, and Cambricon passes NodeLockMLU only as nil-value context. Use nodelock.NodeLockKey directly, or pass the constant through a shared backend constant definition with a comment if the literal must remain local.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/device/cambricon/device.go` around lines 43 - 47, Remove the unused
NodeLockMLU constant from the Cambricon constants block and update any Cambricon
node-lock call sites to use nodelock.NodeLockKey directly instead. Preserve the
existing behavior of nodelock.SetNodeLock and nodelock.ReleaseNodeLock, which
already ignore the lockname argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pkg/device/cambricon/device.go`:
- Around line 87-113: Extract the duplicated container scan from LockNode and
ReleaseNodeLock into a private helper such as hasMLURequest(p *corev1.Pod) bool,
preserving the existing GenerateResourceRequests check and early return
behavior. Replace both methods’ local found-loop logic with calls to this shared
helper, while leaving their respective lock and release operations unchanged.

---

Nitpick comments:
In `@pkg/device/cambricon/device.go`:
- Around line 43-47: Remove the unused NodeLockMLU constant from the Cambricon
constants block and update any Cambricon node-lock call sites to use
nodelock.NodeLockKey directly instead. Preserve the existing behavior of
nodelock.SetNodeLock and nodelock.ReleaseNodeLock, which already ignore the
lockname argument.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b1d9ccda-e9a2-47ed-a70e-e0b0c96795b9

📥 Commits

Reviewing files that changed from the base of the PR and between c7891de and 8dad8a2.

📒 Files selected for processing (2)
  • pkg/device/cambricon/device.go
  • pkg/device/cambricon/device_test.go

@mesutoezdil mesutoezdil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no ai assistance disclosure is present. if any ai tool was used, it must be disclosed per CONTRIBUTING.md: https://github.com/Project-HAMi/HAMi/blob/master/CONTRIBUTING.md#ai-assistance-notice

Comment thread pkg/device/cambricon/device.go Outdated
DsmluProfile = "CAMBRICON_DSMLU_PROFILE"
DsmluResourceAssigned = "CAMBRICON_DSMLU_ASSIGNED"
retry = 5
NodeLockMLU = "hami.io/mutex.lock"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving mlu onto the shared lock pulls it into the #2243 multi device pod problem (mixed mlu + other vendor pods now contend on the same annotation), are u aware of that tradeoff? pls drop a note in #2243 so the central fix covers this too

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aware, but the old cambricon.com/dsmlu.lock was already a single
annotation on the whole node, so the contention surface didn't change.
It was just invisible because it wasn't sharing the key with anyone.
Now at least the central fix in #2243 covers it. Happy to leave a note
there.

@manmathbh

Copy link
Copy Markdown
Author

no ai assistance disclosure is present. if any ai tool was used, it must be disclosed per CONTRIBUTING.md: https://github.com/Project-HAMi/HAMi/blob/master/CONTRIBUTING.md#ai-assistance-notice

Used Copilot for autocomplete while writing the change, no generated blocks.
Read the issue, read nodelock.go, matched the NVIDIA pattern by hand.

@iemAnshuman

Copy link
Copy Markdown
Contributor

@manmathbh the body says an old cambricon.com/dsmlu.lock times out after 5 minutes, but that timeout belongs to hami.io/mutex.lock. nothing reads or deletes dsmlu.lock once this lands, so whatever is sitting on a node at upgrade time stays there for good. does cambricon's device plugin read that key? the cambricon.com/ prefix and the CAMBRICON_DSMLU_* envs right below it make me think it might, but I havent looked at the plugin side. would also wait for #2255 before this merges, otherwise a pod asking for mlu and nvidia takes the lock on the first call and then blocks on it on the second

Remove NodeLockMLU constant, use nodelock.NodeLockKey directly. Extract hasMLURequest helper to deduplicate container loop. Add cleanupLegacyLock to strip orphaned cambricon.com/dsmlu.lock annotations at upgrade time. Fix test assertion arg order and empty-string check.

Signed-off-by: Manmath Hatte <manmathcode@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/device/cambricon/device.go (1)

29-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Group local imports after external imports.

Lines 29-30 place github.com/Project-HAMi/HAMi/... imports before Kubernetes imports. Run goimports -local github.com/Project-HAMi/HAMi and place the local imports last.

Proposed import order
-	"github.com/Project-HAMi/HAMi/pkg/util/client"
-	"github.com/Project-HAMi/HAMi/pkg/util/nodelock"
-
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+
+	"github.com/Project-HAMi/HAMi/pkg/util/client"
+	"github.com/Project-HAMi/HAMi/pkg/util/nodelock"

As per coding guidelines, “Go import blocks must be grouped as standard library imports first, then external imports, then github.com/Project-HAMi/HAMi/... imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/device/cambricon/device.go` around lines 29 - 35, Reorder the imports in
device.go so Kubernetes and other external imports appear before the
github.com/Project-HAMi/HAMi/... imports, with the local client and nodelock
imports in the final group; match goimports -local github.com/Project-HAMi/HAMi
ordering.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pkg/device/cambricon/device.go`:
- Around line 29-35: Reorder the imports in device.go so Kubernetes and other
external imports appear before the github.com/Project-HAMi/HAMi/... imports,
with the local client and nodelock imports in the final group; match goimports
-local github.com/Project-HAMi/HAMi ordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c27ae495-ea72-4d72-9333-48436e3a7d59

📥 Commits

Reviewing files that changed from the base of the PR and between 8dad8a2 and 8c3619c.

📒 Files selected for processing (2)
  • pkg/device/cambricon/device.go
  • pkg/device/cambricon/device_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/device/cambricon/device_test.go

@manmathbh

Copy link
Copy Markdown
Author

@iemAnshuman @mesutoezdil Thanks for the catches! I've pushed a commit addressing the feedback:

Added cleanupLegacyLock() to strip the orphaned dsmlu.lock via JSON patch so nodes don't stay locked after an upgrade.
Applied the CodeRabbit refactors (hasMLURequest() helper, removed duplicate constants, fixed test assertions).
Totally agree on waiting for #2255 to merge first so we don't deadlock mixed-resource pods. Let me know if the cleanup logic looks good!

@iemAnshuman

Copy link
Copy Markdown
Contributor

@iemAnshuman @mesutoezdil Thanks for the catches! I've pushed a commit addressing the feedback:

Added cleanupLegacyLock() to strip the orphaned dsmlu.lock via JSON patch so nodes don't stay locked after an upgrade. Applied the CodeRabbit refactors (hasMLURequest() helper, removed duplicate constants, fixed test assertions). Totally agree on waiting for #2255 to merge first so we don't deadlock mixed-resource pods. Let me know if the cleanup logic looks good!

also I checked the current Cambricon device plugin code and found a separate lifecycle mismatch. in DynamicSmlu mode, its allocation, startup, and pod cleanup paths still remove only cambricon.com/dsmlu.lock; they never release hami.io/mutex.lock. HAMi doesnt release the node lock after a successful Bind, so a Cambricon only DynamicSmlu pod can leave the new shared lock behind and block later accelerator pods on that node until the lock times out at five minutes by default.

could we either keep the legacy key until the plugin supports the shared lock, or land a coordinated, owner safe plugin change first? also a focused test proving that a successful DynamicSmlu allocation releases the exact key written by LockNode() would be useful. The legacy cleanup helps with upgrades, but it doesnt fix this release path mismatch.

@manmathbh

Copy link
Copy Markdown
Author

@iemAnshuman You're right, this is a real lifecycle mismatch that can't be fixed solely on the scheduler side. nodelock.SetNodeLock and ReleaseNodeLock both hardcode hami.io/mutex.lock (the lockname parameter is accepted but unused), so there's no way to keep using cambricon.com/dsmlu.lock through the shared infra.

The Cambricon device plugin's DynamicSmlu paths need to release hami.io/mutex.lock after allocation. Since the plugin isn't in this repo, two options:

  1. Keep the hand-rolled lock for now, fix the original 3 bugs (stale Node reference, no-backoff retry, informer map mutation) in-place without migrating to nodelock, then switch to shared lock later once the plugin catches up.
  2. Coordinate a plugin-side PR first, add hami.io/mutex.lock release to the device plugin's DynamicSmlu cleanup, then merge this.

Preference? I lean toward option 2; it avoids keeping dead code around. Happy to help draft the plugin-side change.

@mesutoezdil

Copy link
Copy Markdown
Contributor

You know the rule from previous prs.
For future:

Reminder: Answers must be written by human being. You can view the relevant rule here.
https://github.com/Project-HAMi/HAMi/blob/master/CONTRIBUTING.md#contribution-gates
"4. Review replies. The reply you post must be written by you and must address the specific point raised. Verbatim or canned AI replies, or replies that do not engage the comment, lead to the PR being closed."

@mesutoezdil mesutoezdil closed this Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cambricon ReleaseNodeLock can return nil without removing the node lock

4 participants