Skip to content

cleanup(metax): decouple MetaxDevices.ScoreNode (Metax-GPU) from the scheduler policy string - #2579

Closed
asadjan4611 wants to merge 1 commit into
Project-HAMi:masterfrom
asadjan4611:fix/metax-gpu-decouple-scorenode-2572
Closed

cleanup(metax): decouple MetaxDevices.ScoreNode (Metax-GPU) from the scheduler policy string#2579
asadjan4611 wants to merge 1 commit into
Project-HAMi:masterfrom
asadjan4611:fix/metax-gpu-decouple-scorenode-2572

Conversation

@asadjan4611

@asadjan4611 asadjan4611 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What this PR does

MetaxDevices.ScoreNode (the Metax-GPU backend) branched on the scheduler policy string, reading a different topology annotation for each policy:

  • Binpack2000 - losses[n]
  • Spread2000 - scores[n]

This is the same coupling that was removed for the sibling Metax-SGPU backend in #2404 / #2413. The device layer should not know scheduler policy names, and the 2000 - score subtraction only existed to flip the sign so the
best node wins under both sort directions.

This PR makes ScoreNode return a single policy-independent, "higher is a better node" score:

  • Prefer the metax-tech.com/gpu.topology.scores annotation.
  • Fall back to metax-tech.com/gpu.topology.losses (converted onto the same
    "higher is better" scale via 2000 - loss) when scores is absent.

MetaxDevices now implements the policy-neutral scorer marker, so the shared OverrideScore layer owns the ±10000 weighting and the Spread-policy sign inversion — exactly as it already does for Metax-SGPU. OverrideScore itself
is unchanged.

Fixes #2572

Why the node ranking is unchanged

When a node advertises both annotations, they describe the same underlyingtopology preference, so the lowest-loss node is also the highest-score node. Because OverrideScore inverts the sign under Spread (which selects the lowest
score) and keeps it under Binpack (which selects the highest), the winning node is the same under both policies as before.

A new test, TestOverrideScoreMetaxGPUOrderingUnchanged, asserts this directly:
it builds nodes with consistent annotations, computes the winner each original
per-policy rule would have chosen, then weights and sorts the nodes exactly as the scheduler does (OverrideScoresort.SortNodeList[len-1]) and checks the same node wins under both Binpack and Spread.

Note for reviewers: this preserves ordering
under the assumption that a node exposing both annotations encodes one
consistent topology preference (min-loss node = max-score node). The issue
invites maintainer input on whether losses and scores can ever rank nodes
inconsistently, and on whether the two annotations should ultimately be
unified. Happy to adjust the fallback direction (or gate it) based on that.

Scope

Exactly three files, no changes to the shared scheduler infrastructure that landed with #2413:

  • pkg/device/metax/device.go — the fix
  • pkg/device/metax/device_test.go — policy-independence + marker-interface tests
  • pkg/scheduler/policy/node_policy_test.go — weighted Binpack/Spread cases + ordering-equivalence test

Acceptance criteria (from #2572)

  • MetaxDevices.ScoreNode no longer reads the policy parameter
  • MetaxDevices implements the policy-neutral scorer marker
  • Binpack and Spread give the same node ranking for Metax-GPU as before (test)
  • Policy-independence test added
  • Marker-interface test added
  • Existing tests still pass
  • No scheduling behavior change for Metax-GPU (aside from the intended fix)

Testing

  • go test ./pkg/device/metax/... ./pkg/scheduler/policy/... -short --race -count=1 — pass
  • go vet clean, go build ./... clean
  • goimports (local prefix) clean, import aliases correct, license headers present
  • golangci-lint run on the changed packages — 0 issues

Summary by CodeRabbit

  • Improvements
    • Improved accelerator topology scoring to provide consistent, policy-independent results.
    • Score-based and loss-based topology annotations are now interpreted consistently, with clear precedence rules and fallback behavior.
    • Preserved expected node ordering under both Binpack and Spread scheduling policies.
    • Improved handling of missing topology information and multi-device configurations.

…licy string

MetaxDevices.ScoreNode (Metax-GPU) branched on the scheduler policy
string: under Binpack it scored 2000 - losses[n] and under Spread it
scored 2000 - scores[n]. The device layer should not know scheduler
policy names, and the 2000 - score subtraction only existed to flip the
sign so the best node wins under both sort directions. The sibling
Metax-SGPU backend was already decoupled this way in Project-HAMi#2413.

ScoreNode now returns a single policy-independent, "higher is a better
node" score: it prefers the scores annotation and falls back to the
losses annotation (converted onto the same scale via 2000 - loss) when
scores is absent. MetaxDevices implements the policy-neutral scorer
marker, so the shared OverrideScore layer owns the ±10000 weighting and
the Spread-policy sign inversion, mirroring Metax-SGPU. OverrideScore
itself is unchanged.

When a node advertises both annotations they describe the same topology
preference, so the lowest-loss node is also the highest-score node and
the winning node is unchanged under both Binpack and Spread. A new
ordering-equivalence test asserts this by weighting and sorting nodes
exactly as the scheduler does.

Tests: ScoreNode is asserted identical across binpack/spread/"" policies;
a marker-interface test is added; the OverrideScore cases cover the
weighted Binpack and inverted Spread results; and the ordering-
equivalence test covers node selection.

Fixes Project-HAMi#2572

Signed-off-by: asadjan4611 <asadjan4611@gmail.com>
@hami-robot

hami-robot Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: asadjan4611
Once this PR has been reviewed and has the lgtm label, please assign shouren 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

@hami-robot hami-robot Bot added the size/L label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Metax-GPU scoring no longer depends on the scheduler policy. Score annotations take precedence, loss annotations provide fallback values, and the shared scheduler layer applies policy weighting and ordering. Tests cover scoring behavior, marker implementation, weighted scores, and node-ordering stability.

Changes

Metax scoring

Layer / File(s) Summary
Policy-neutral Metax scoring
pkg/device/metax/device.go, pkg/device/metax/device_test.go
ScoreNode uses score annotations first, converts loss annotations with 2000 - loss, and returns zero without topology annotations. Tests verify identical results across policies and the PolicyNeutralScore marker.
Scheduler weighting and ordering validation
pkg/scheduler/policy/node_policy_test.go
Tests verify weighted Metax-GPU scores under Binpack and Spread and confirm that node ordering remains unchanged.

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

Possibly related issues

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: mesutoezdil, archlitchi

Poem

A rabbit checks the scores in line,
Loss turns bright at 2000 - loss time.
Binpack weighs, Spread flips the view,
Yet Metax picks the same node too.
Hop, hop—policy code is clear!

🚥 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 summarizes the primary change: decoupling MetaxDevices.ScoreNode from scheduler policy strings.
Linked Issues check ✅ Passed The changes satisfy issue #2572 by making scoring policy-independent, adding the marker, preserving ranking, and covering the required behavior with tests.
Out of Scope Changes check ✅ Passed All changes are limited to the Metax scoring implementation and related tests required by issue #2572.
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.

@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.

Actionable comments posted: 1

🧹 Nitpick comments (5)
pkg/scheduler/policy/node_policy_test.go (2)

581-591: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The premise assertion at Line 588 hides which case failed.

assert.Equal(t, wantBinpack, wantSpread) guards the test data, not the production code. If it fails, the message does not say that the fixture is inconsistent. Add a message so a future edit to tests is diagnosed quickly.

♻️ Proposed message
-			assert.Equal(t, wantBinpack, wantSpread)
+			assert.Equal(t, wantBinpack, wantSpread,
+				"test fixture is inconsistent: the lowest-loss node must also be the highest-score node")
🤖 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/scheduler/policy/node_policy_test.go` around lines 581 - 591, Add a clear
assertion message to the wantBinpack-versus-wantSpread check in the test loop,
explicitly identifying inconsistent test fixture annotations or rankings; leave
the production winner assertions unchanged.

477-486: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share the config.Config fixture with TestOverrideScore.

TestOverrideScore at Line 202 builds an almost identical config.Config. Extract one helper so both tests register the same device set.

🤖 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/scheduler/policy/node_policy_test.go` around lines 477 - 486, Extract the
shared config.Config fixture from TestOverrideScore into a helper that registers
the device configuration, then reuse that helper in TestOverrideScore and
TestOverrideScoreMetaxGPUOrderingUnchanged. Ensure both tests use the same
resource names and DefaultGPUNum values.
pkg/device/metax/device.go (2)

205-205: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Lower the scoring log level.

klog.InfoS writes at verbosity 0. ScoreNode runs for every candidate node on every scheduling attempt, so this produces one log line per node per pod. Other backends use klog.V(4) or klog.V(3) for the same purpose, for example pkg/device/ascend/device.go:408 and pkg/device/kunlun/device.go:174.

♻️ Proposed log level change
-		klog.InfoS("Detected annotations", "key", MetaxAnnotationScore, "value", scoreAnno, "requesting", sum, "extract", score)
+		klog.V(4).InfoS("Detected annotations", "key", MetaxAnnotationScore, "value", scoreAnno, "requesting", sum, "extract", score)

Also applies to: 213-213

🤖 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/metax/device.go` at line 205, Lower the verbosity of the “Detected
annotations” scoring logs in ScoreNode from unconditional klog.InfoS to a
guarded verbosity level consistent with the other device backends, applying the
same change to both occurrences.

209-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the 2000 loss offset into a named constant and confirm the two annotation scales are comparable.

Two points on the loss fallback:

  1. 2000 is a magic number. Give it a name so the intent of the conversion is visible.
  2. The two branches return values on different scales. A node that publishes only scores returns the raw score (for example 200). A node that publishes only losses returns 2000 - loss (for example 1800). If a cluster contains nodes of both kinds, the loss-only nodes almost always outrank the score-only nodes. Confirm that Metax publishes the same annotation set on every node in a cluster, or document that assumption here.
♻️ Proposed constant extraction
+// MetaxMaxTopologyLoss is the upper bound of the values published in the
+// "gpu.topology.losses" annotation. It converts a loss (lower is better) onto
+// the same "higher is better" scale used by "gpu.topology.scores".
+const MetaxMaxTopologyLoss = 2000
+
 	if lossAnno, ok := node.Annotations[MetaxAnnotationLoss]; ok {
 		// it's preferred to select the node with lower loss, so convert the
 		// loss onto a "higher is better" scale.
 		loss := parseMetaxAnnos(lossAnno, sum)
 		klog.InfoS("Detected annotations", "key", MetaxAnnotationLoss, "value", lossAnno, "requesting", sum, "extract", loss)
-		return 2000 - loss
+		return MetaxMaxTopologyLoss - loss
 	}
🤖 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/metax/device.go` around lines 209 - 215, Extract the loss
conversion offset currently hard-coded as 2000 in the annotation-selection logic
into a descriptive named constant, and use that constant when returning the
converted loss. In the same logic around parseMetaxAnnos and the score/loss
branches, confirm or document the assumption that Metax nodes in a cluster
publish the same annotation set so both return scales remain comparable.
pkg/device/metax/device_test.go (1)

402-460: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider two more table cases for the loss conversion.

The four cases cover the main paths. Two boundary behaviours of the new 2000 - loss conversion are untested:

  1. A loss value above 2000, which makes ScoreNode return a negative score.
  2. A malformed or index-missing annotation, where parseMetaxAnnos returns 0.

Both are cheap to add and pin the conversion contract.

💚 Proposed additional cases
 		{
 			name:       "no topology annotation scores zero",
 			node:       &corev1.Node{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}},
 			podDevices: twoDevices,
 			want:       float32(0),
 		},
+		{
+			name: "loss above the conversion offset yields a negative score",
+			node: &corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						MetaxAnnotationLoss: "{\"2\":2500}",
+					},
+				},
+			},
+			podDevices: twoDevices,
+			want:       float32(-500),
+		},
+		{
+			name: "malformed scores annotation scores zero",
+			node: &corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						MetaxAnnotationScore: "not-json",
+					},
+				},
+			},
+			podDevices: twoDevices,
+			want:       float32(0),
+		},
 	}
🤖 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/metax/device_test.go` around lines 402 - 460, Add table-driven
cases to the tests around ScoreNode for the loss conversion: verify a loss above
2000 produces the corresponding negative score, and verify malformed or
missing-index loss annotations cause parseMetaxAnnos to return 0 and ScoreNode
to use that result. Keep the existing score-precedence and zero-score cases
unchanged.
🤖 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.

Inline comments:
In `@pkg/device/metax/device.go`:
- Around line 202-207: Update parseMetaxAnnos and ScoreNode so an invalid scores
annotation or one missing the requested sum is distinguishable from a valid
score of 0. In ScoreNode, only return the scores result when the lookup
succeeds; otherwise continue to the MetaxAnnotationLoss fallback while
preserving safe zero scoring when neither annotation provides a usable value.

---

Nitpick comments:
In `@pkg/device/metax/device_test.go`:
- Around line 402-460: Add table-driven cases to the tests around ScoreNode for
the loss conversion: verify a loss above 2000 produces the corresponding
negative score, and verify malformed or missing-index loss annotations cause
parseMetaxAnnos to return 0 and ScoreNode to use that result. Keep the existing
score-precedence and zero-score cases unchanged.

In `@pkg/device/metax/device.go`:
- Line 205: Lower the verbosity of the “Detected annotations” scoring logs in
ScoreNode from unconditional klog.InfoS to a guarded verbosity level consistent
with the other device backends, applying the same change to both occurrences.
- Around line 209-215: Extract the loss conversion offset currently hard-coded
as 2000 in the annotation-selection logic into a descriptive named constant, and
use that constant when returning the converted loss. In the same logic around
parseMetaxAnnos and the score/loss branches, confirm or document the assumption
that Metax nodes in a cluster publish the same annotation set so both return
scales remain comparable.

In `@pkg/scheduler/policy/node_policy_test.go`:
- Around line 581-591: Add a clear assertion message to the
wantBinpack-versus-wantSpread check in the test loop, explicitly identifying
inconsistent test fixture annotations or rankings; leave the production winner
assertions unchanged.
- Around line 477-486: Extract the shared config.Config fixture from
TestOverrideScore into a helper that registers the device configuration, then
reuse that helper in TestOverrideScore and
TestOverrideScoreMetaxGPUOrderingUnchanged. Ensure both tests use the same
resource names and DefaultGPUNum values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2994854f-4658-4c07-9561-724e9e9731cd

📥 Commits

Reviewing files that changed from the base of the PR and between 634bf2b and 9a5920f.

📒 Files selected for processing (3)
  • pkg/device/metax/device.go
  • pkg/device/metax/device_test.go
  • pkg/scheduler/policy/node_policy_test.go

Comment on lines +202 to +207
if scoreAnno, ok := node.Annotations[MetaxAnnotationScore]; ok {
// it's preferred to select the node with higher score
score := parseMetaxAnnos(scoreAnno, sum)
klog.InfoS("Detected annotations", "key", MetaxAnnotationScore, "value", scoreAnno, "requesting", sum, "extract", score)
return score
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A malformed or incomplete scores annotation now suppresses the losses fallback.

parseMetaxAnnos returns 0 when the JSON fails to unmarshal or when the map has no entry for sum. In that case ScoreNode returns 0 and never reads MetaxAnnotationLoss. A node that publishes both annotations, but whose scores map omits the requested device count, therefore loses its topology preference.

The scoring behaviour is safe (0), so this is not a blocker. If you want the fallback to stay effective, let parseMetaxAnnos report whether the lookup succeeded.

🤖 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/metax/device.go` around lines 202 - 207, Update parseMetaxAnnos
and ScoreNode so an invalid scores annotation or one missing the requested sum
is distinguishable from a valid score of 0. In ScoreNode, only return the scores
result when the lookup succeeds; otherwise continue to the MetaxAnnotationLoss
fallback while preserving safe zero scoring when neither annotation provides a
usable value.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/device/metax/device.go 90.00% 1 Missing ⚠️
Flag Coverage Δ
unittests 62.34% <90.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/device/metax/device.go 75.37% <90.00%> (-0.98%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mesutoezdil

Copy link
Copy Markdown
Contributor

This is being closed because it does not comply with the contribution guidelines.

@asadjan4611

Copy link
Copy Markdown
Contributor Author

@mesutoezdil
can you please highlight the issues bcz i spent a lot of time in this PR and i can't waste it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decouple MetaxDevices.ScoreNode (Metax-GPU) from the scheduler policy string

2 participants