Skip to content

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

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

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

Conversation

@asadjan4611

@asadjan4611 asadjan4611 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind cleanup

What this PR does / why we need it:

MetaxDevices.ScoreNode (the Metax-GPU backend) chose its result based on the scheduler policy string: it read the metax-tech.com/gpu.topology.losses annotation under Binpack and metax-tech.com/gpu.topology.scores under Spread,encoding the policy's sort direction directly in the device layer. This is the same anti-pattern #2404 removed from MetaxSDevices (Metax-SGPU).

This PR makes ScoreNode return a single policy-independent, "higher is a better node" score, and has MetaxDevices implement the existing device.PolicyNeutralScorer marker. The shared scheduler policy layer
(OverrideScore) then applies the weight and the Spread sign inversion in one place. OverrideScore itself is unchanged, and no other backend is affected.

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

Special notes for your reviewer:

I used AI Assistance for verification of test cases and for the PR description and also verified for the solution implementations.

Does this PR introduce a user-facing change?:

NONE

Summary by CodeRabbit

  • Bug Fixes
    • Improved Metax device scoring by prioritizing explicit scores and using topology loss data as a fallback.
    • Correctly distinguishes missing, malformed, and zero-valued topology data.
    • Ensures consistent node scoring across Binpack, Spread, and other scheduling policies.
    • Preserves expected node ordering for Metax GPU scheduling.
  • Tests
    • Expanded coverage for topology annotation parsing, multi-GPU lookup, scoring precedence, and policy-independent behavior.

@hami-robot

hami-robot Bot commented Aug 7, 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

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Metax-GPU scoring no longer branches on scheduler policy. It prefers score annotations, converts loss annotations to higher-is-better scores, handles unusable data as zero, and implements PolicyNeutralScorer. Tests cover parsing, scoring, interface support, weighting, and node ordering.

Changes

Policy-neutral scoring

Layer / File(s) Summary
Backend scoring contracts and implementation
pkg/device/metax/device.go
ScoreNode now ignores policy, prefers score annotations, converts loss annotations, and returns zero for unusable data. MetaxDevices now implements PolicyNeutralScorer.
Metax parsing and scoring validation
pkg/device/metax/device_test.go
Tests cover found status, malformed and missing annotations, score precedence, loss fallback, multi-device lookup, policy-independent results, and marker support.
Scheduler weighting validation
pkg/scheduler/policy/node_policy_test.go
Tests cover weighted Metax-GPU scores and consistent node ordering under Binpack and Spread.

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

Sequence Diagram(s)

sequenceDiagram
  participant MetaxDevices
  participant OverrideScore
  participant SchedulerPolicy
  MetaxDevices->>OverrideScore: return policy-independent node score
  OverrideScore->>SchedulerPolicy: apply Binpack or Spread weighting
  SchedulerPolicy-->>OverrideScore: return weighted score
  OverrideScore-->>MetaxDevices: preserve node ordering
Loading

Possibly related issues

  • Project-HAMi/HAMi issue 2404: It defines the policy-neutral Metax scoring pattern that this change applies to MetaxDevices.
  • Project-HAMi/HAMi issue 2572: It also concerns decoupling MetaxDevices.ScoreNode from scheduler policy and adding PolicyNeutralScorer.

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: archlitchi

Poem

I’m a rabbit with scores in my hat,
Loss becomes points—imagine that!
Binpack and Spread now share the same trail,
Bad data rests at a quiet zero scale.
Hop, hop—policy-neutral wins!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds policy-independent scoring and the marker, but score-first precedence may change Binpack ranking when both annotations exist, violating #2439. Preserve the existing Binpack signal or add a test that proves ordering equivalence when both annotations are present.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: decoupling Metax-GPU ScoreNode from the scheduler policy string.
Out of Scope Changes check ✅ Passed The implementation and test changes support policy-neutral Metax-GPU scoring, scheduler weighting, ranking preservation, and marker validation.
✨ 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 (6)
pkg/scheduler/policy/node_policy_test.go (1)

369-371: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the exported annotation constant instead of the literal.

pkg/device/metax exports MetaxSGPUTopologyAware for "metax-tech.com/sgpu-topology-aware". pkg/device/metax/sdevice_test.go Line 2931 uses the constant. Import and reuse it here so a rename cannot silently break these four cases.

Also applies to: 379-381, 419-421, 429-431

🤖 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 369 - 371, Replace the
repeated literal annotation key in the Pod.Annotations setup for all four cases
with the exported metax.MetaxSGPUTopologyAware constant. Add the required
pkg/device/metax import and keep the existing annotation values and test
behavior unchanged.
pkg/device/metax/sdevice_test.go (1)

2985-2995: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the expected score value, not only policy equality.

The test passes if ScoreNode returns the same value for every policy. A regression that returns 0 for all inputs still passes. Add a want field to the table and assert the concrete score for each case.

♻️ Proposed change
 	for _, ts := range []struct {
 		name       string
 		podDevices device.PodSingleDevice
 		previous   []*device.DeviceUsage
+		want       float32
 	}{
 			if binpack != spread || binpack != empty {
 				t.Errorf("ScoreNode is policy-dependent: binpack=%v, spread=%v, empty=%v",
 					binpack, spread, empty)
 			}
+			if binpack != ts.want {
+				t.Errorf("ScoreNode() = %v, want %v", binpack, ts.want)
+			}
🤖 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/sdevice_test.go` around lines 2985 - 2995, Extend the
test-case table used by the ScoreNode subtests with a want score, then update
the assertions in the ts.name test block to verify the returned score equals
that expected value while retaining the policy-equality check. Populate want for
every case from the intended ScoreNode results.
pkg/device/metax/device_test.go (2)

482-490: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a compile-time assertion for the marker interface.

A package-level var _ device.PolicyNeutralScorer = &MetaxDevices{} fails at build time instead of test time. Also declare dev as device.Devices to match pkg/device/metax/sdevice_test.go Line 3002, which proves both interfaces at once.

🤖 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 482 - 490, Replace the runtime
assertion in TestMetaxDevicesImplementsPolicyNeutralScorer with a package-level
compile-time assertion assigning &MetaxDevices{} to device.PolicyNeutralScorer.
Update the test’s dev declaration to device.Devices, matching the established
interface-check pattern in the surrounding tests so both interfaces are
validated together.

411-460: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a case for a losses annotation without the requested index.

The table does not cover a losses map that omits the requested device count. That path returns 2000 today. See the comment on pkg/device/metax/device.go Lines 208-214 for the root cause. Add the case once the conversion is fixed, so the behavior is locked in.

🤖 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 411 - 460, Add a table-driven
test case to the existing scoring test covering a losses annotation whose map
omits the requested device-count index, with the expected result of 2000; add it
alongside the other losses annotation cases in the test table and ensure it uses
the existing twoDevices fixture.
pkg/scheduler/policy/node_policy.go (1)

67-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the weight into a named constant.

10000 and -10000 are repeated literals that encode a policy contract. Define one exported or package-level constant and negate it for Spread. This also removes the risk of the two literals drifting apart.

♻️ Proposed change
+// policyNeutralScoreWeight makes policy-neutral device scores dominate the
+// base node score.
+const policyNeutralScoreWeight float32 = 10000
+
 func (ns *NodeScore) OverrideScore(previous []*device.DeviceUsage, policy string) {
 		if _, ok := device.GetDevices()[idx].(device.PolicyNeutralScorer); ok {
-			weight := float32(10000)
+			weight := policyNeutralScoreWeight
 			if policy == util.NodeSchedulerPolicySpread.String() {
-				weight = -10000
+				weight = -policyNeutralScoreWeight
 			}
 			score = weight * score
 		}
🤖 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.go` around lines 67 - 73, Define a
package-level named constant for the neutral scorer weight in the scoring logic,
then use that constant for the default value and negate it when policy equals
util.NodeSchedulerPolicySpread.String(). Remove both inline 10000 literals while
preserving the existing score calculation in the device.PolicyNeutralScorer
branch.
pkg/device/metax/device.go (1)

191-194: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the 2000 constant and the cross-node scale.

The scores branch returns the raw annotation value. The losses branch returns 2000 - loss. The two branches produce values on different scales. If some nodes publish scores and other nodes publish losses, OverrideScore compares the two scales directly. Confirm that Metax always publishes the same annotation on every node of a cluster. Also extract 2000 into a named constant with a comment about its origin.

🤖 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 191 - 194, Update OverrideScore to
use a named constant for 2000, with a comment documenting its Metax origin and
the losses-to-scores conversion. Confirm and document the Metax contract that
every node in a cluster publishes the same annotation type, preserving
cross-node comparability when OverrideScore compares values.
🤖 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 208-214: Update parseMetaxAnnos and its caller in the node scoring
logic to distinguish a found loss value from malformed or missing topology data.
When the requested index is unavailable, return the same neutral score used by
the no-annotation path instead of converting zero into the maximum score;
preserve the existing 2000-loss conversion only for known values.

---

Nitpick comments:
In `@pkg/device/metax/device_test.go`:
- Around line 482-490: Replace the runtime assertion in
TestMetaxDevicesImplementsPolicyNeutralScorer with a package-level compile-time
assertion assigning &MetaxDevices{} to device.PolicyNeutralScorer. Update the
test’s dev declaration to device.Devices, matching the established
interface-check pattern in the surrounding tests so both interfaces are
validated together.
- Around line 411-460: Add a table-driven test case to the existing scoring test
covering a losses annotation whose map omits the requested device-count index,
with the expected result of 2000; add it alongside the other losses annotation
cases in the test table and ensure it uses the existing twoDevices fixture.

In `@pkg/device/metax/device.go`:
- Around line 191-194: Update OverrideScore to use a named constant for 2000,
with a comment documenting its Metax origin and the losses-to-scores conversion.
Confirm and document the Metax contract that every node in a cluster publishes
the same annotation type, preserving cross-node comparability when OverrideScore
compares values.

In `@pkg/device/metax/sdevice_test.go`:
- Around line 2985-2995: Extend the test-case table used by the ScoreNode
subtests with a want score, then update the assertions in the ts.name test block
to verify the returned score equals that expected value while retaining the
policy-equality check. Populate want for every case from the intended ScoreNode
results.

In `@pkg/scheduler/policy/node_policy_test.go`:
- Around line 369-371: Replace the repeated literal annotation key in the
Pod.Annotations setup for all four cases with the exported
metax.MetaxSGPUTopologyAware constant. Add the required pkg/device/metax import
and keep the existing annotation values and test behavior unchanged.

In `@pkg/scheduler/policy/node_policy.go`:
- Around line 67-73: Define a package-level named constant for the neutral
scorer weight in the scoring logic, then use that constant for the default value
and negate it when policy equals util.NodeSchedulerPolicySpread.String(). Remove
both inline 10000 literals while preserving the existing score calculation in
the device.PolicyNeutralScorer branch.
🪄 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: 40c5983e-8b05-48cc-9aa8-2bbd3db05596

📥 Commits

Reviewing files that changed from the base of the PR and between c5e99b6 and 9323e6c.

📒 Files selected for processing (7)
  • pkg/device/devices.go
  • pkg/device/metax/device.go
  • pkg/device/metax/device_test.go
  • pkg/device/metax/sdevice.go
  • pkg/device/metax/sdevice_test.go
  • pkg/scheduler/policy/node_policy.go
  • pkg/scheduler/policy/node_policy_test.go

Comment thread pkg/device/metax/device.go
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
pkg/device/metax/device.go 92.85% 1 Missing ⚠️
Flag Coverage Δ
unittests 62.40% <92.85%> (+0.04%) ⬆️

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 78.39% <92.85%> (+2.03%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@FouoF

FouoF commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thanks for working on the MetaX scoring code. This PR is stacked on #2413 and includes that PR's full diff, so it cannot be reviewed as an independent cleanup. More importantly, it changes the MetaX-GPU binpack signal from losses to scores, while the stated acceptance condition is that node ordering remain unchanged. That is a behavioral change rather than a decoupling-only cleanup. We are closing this PR. Please first finish or replace #2413, then propose any independent cleanup from the current master with tests that prove ordering equivalence.

@asadjan4611

Copy link
Copy Markdown
Contributor Author

@FouoF @archlitchi can you please review my PR

Signed-off-by: asadjan4611 <asadjan4611@gmail.com>
@asadjan4611
asadjan4611 force-pushed the feat/metax-gpu-decouple-scorenode-policy branch from 9323e6c to dcd120a Compare August 12, 2026 06:39
@hami-robot hami-robot Bot added size/XL and removed size/L labels Aug 12, 2026

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

🧹 Nitpick comments (3)
pkg/device/metax/device_test.go (2)

104-183: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Separate the annotation input from the subtest name.

The name field carries the annotation JSON and the subtest label. Eight cases share the same JSON, so t.Run produces #01-style suffixed names. A failing subtest does not show the index under test. Add an annos field and give each case a descriptive name.

♻️ Proposed refactor
 	tests := []struct {
 		name      string
+		annos     string
 		index     int
 		value     float32
 		wantFound bool
 	}{
 		{
-			name:      "{\"1\":0,\"2\":110,\"3\":270,\"4\":540,\"5\":580,\"6\":730,\"7\":930,\"8\":1240}",
+			name:      "index 1",
+			annos:     "{\"1\":0,\"2\":110,\"3\":270,\"4\":540,\"5\":580,\"6\":730,\"7\":930,\"8\":1240}",
 			index:     1,
 			value:     0,
 			wantFound: true,
 		},

Then update the call site:

-			value, found := parseMetaxAnnos(tt.name, tt.index)
+			value, found := parseMetaxAnnos(tt.annos, tt.index)
🤖 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 104 - 183, Update the
parseMetaxAnnos table-driven test to add a separate annos field for the
annotation JSON, use descriptive unique values in name for each case (including
the tested index), and pass tt.annos to parseMetaxAnnos instead of tt.name.
Preserve the existing expected values and found-status assertions.

437-548: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive loss expectations from metaxTopologyLossBase and include the policy in assertion failures.

metaxTopologyLossBase is 2000, so replace 1800 and 1950 with metaxTopologyLossBase - float32(200) and metaxTopologyLossBase - float32(50). Add "policy %q", policy to assert.Equal so failures identify the policy.

🤖 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 437 - 548, Update the expected
loss-based scores in the tests around the test table to derive them from
metaxTopologyLossBase, using subtraction of the corresponding loss values
instead of hardcoded 1800 and 1950. In the policy loop’s assertion, include the
current policy with the assertion message so failures identify which policy
produced the mismatch.
pkg/scheduler/policy/node_policy_test.go (1)

563-579: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case where the two annotations disagree.

Both fixtures set losses and scores so that the same node wins. The new backend prefers scores and falls back to losses. The old Binpack rule read losses. A behavior change is therefore only observable when the two annotations rank nodes differently, and no case covers that. Add a fixture with divergent values and assert the intended winner explicitly. This also addresses the ordering-equivalence concern raised in the PR discussion.

The premise assertion on Line 588 also derives both expectations from the fixture, so a wrong helper and a wrong implementation could agree. Consider asserting a literal node name per case.

🧪 Suggested additional fixture
 	tests := []struct {
 		name  string
 		nodes []metaxNode
+		want  string
 	}{
 		{
 			name: "best node has both lowest loss and highest score",
 			nodes: []metaxNode{
 				{name: "node-a", loss: 300, score: 100},
 				{name: "node-b", loss: 100, score: 300},
 				{name: "node-c", loss: 200, score: 200},
 			},
+			want: "node-b",
 		},

Then add a divergent case and assert tt.want directly instead of comparing the two derived winners.

🤖 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 563 - 579, Add a test
fixture in the node-policy test cases where loss and score rankings disagree,
with an explicit intended node name reflecting score preference and loss
fallback behavior. Update the table-driven assertions to compare the selected
result directly against each case’s literal expected winner rather than deriving
expectations from both helpers, preserving the existing cases while making
divergent ordering observable.
🤖 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.

Nitpick comments:
In `@pkg/device/metax/device_test.go`:
- Around line 104-183: Update the parseMetaxAnnos table-driven test to add a
separate annos field for the annotation JSON, use descriptive unique values in
name for each case (including the tested index), and pass tt.annos to
parseMetaxAnnos instead of tt.name. Preserve the existing expected values and
found-status assertions.
- Around line 437-548: Update the expected loss-based scores in the tests around
the test table to derive them from metaxTopologyLossBase, using subtraction of
the corresponding loss values instead of hardcoded 1800 and 1950. In the policy
loop’s assertion, include the current policy with the assertion message so
failures identify which policy produced the mismatch.

In `@pkg/scheduler/policy/node_policy_test.go`:
- Around line 563-579: Add a test fixture in the node-policy test cases where
loss and score rankings disagree, with an explicit intended node name reflecting
score preference and loss fallback behavior. Update the table-driven assertions
to compare the selected result directly against each case’s literal expected
winner rather than deriving expectations from both helpers, preserving the
existing cases while making divergent ordering observable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a1c455ea-575c-4b12-b3b1-febee534d7a5

📥 Commits

Reviewing files that changed from the base of the PR and between 9323e6c and dcd120a.

📒 Files selected for processing (3)
  • pkg/device/metax/device.go
  • pkg/device/metax/device_test.go
  • pkg/scheduler/policy/node_policy_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/device/metax/device.go

@mesutoezdil

Copy link
Copy Markdown
Contributor

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

Pls read the rules.

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

4 participants