Skip to content

feat(scheduler): support template node simulation filtering - #2046

Merged
hami-robot[bot] merged 5 commits into
Project-HAMi:masterfrom
spencercjh:feat/dry-run-filter
Jul 13, 2026
Merged

feat(scheduler): support template node simulation filtering#2046
hami-robot[bot] merged 5 commits into
Project-HAMi:masterfrom
spencercjh:feat/dry-run-filter

Conversation

@spencercjh

@spencercjh spencercjh commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Most of this Pull Request was generated or revised with the assistance of AI tools, including Codex and Raft(Slock). I have reviewed the resulting content and take full responsibility for its accuracy, security, licensing compliance, and inclusion in this project.

What type of PR is this?

/kind feature

What this PR does / why we need it:

This PR adds a simulation-only template-node filter path to the HAMi scheduler extender.

When the extender receives ExtenderArgs.Nodes, it now:

  • builds transient NodeInfo and DeviceUsage from the provided template/synthetic nodes
  • reuses the existing scoring and vendor fit pipeline for feasibility checks
  • returns fit or unfit results without mutating pod annotations, scheduling caches, or quota usage

The normal scheduling path that consumes ExtenderArgs.NodeNames is unchanged.

Which issue(s) this PR fixes:

Related to #1099 and kubernetes/autoscaler#9786

Special notes for your reviewer:

Implementation notes:

  • Filter() now branches on args.Nodes != nil and uses a simulation-only path for template-node evaluation.
  • Template-node adaptation reuses existing register-annotation parsing through GetNodeDevices().
  • Detailed failure reasons are returned for simulation requests, while event recording and scheduling side effects stay disabled.

Integration validation:

  • Environment: Azure Kubernetes Service with two nodes.
  • Deployed the scheduler extender built from this branch to the hami-scheduler deployment
  • Used the existing mock-device-plugin in the cluster as the fixture source
  • Port-forwarded svc/hami-scheduler and sent real HTTPS POST /filter requests against the live extender
  • Verified three cases with template nodes:
    • warm template node with hami.io/node-nvidia-register returns fit
    • oversized request returns CardInsufficientMemory
    • missing register annotation returns node unregistered

Local validation already run before opening this PR:

  • make lint
  • make tidy
  • make verify

Does this PR introduce a user-facing change?:

HAMi scheduler extender can now evaluate template nodes passed through ExtenderArgs.Nodes for simulation use cases, reusing existing device-fit logic without mutating live scheduling state.

Summary by CodeRabbit

  • New Features

    • Scheduler filtering now supports temporary or template nodes that are not pre-registered.
    • The scheduler evaluates available devices on demand and selects the best-fitting node.
    • Requests without device resources now preserve the submitted node list or names.
  • Bug Fixes

    • Improved failure messages identify specific resource shortages and unregistered nodes.
    • Simulation-based filtering avoids modifying pod metadata or scheduling and quota state during evaluation.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds deep-copy support for device and node usage data, constructs transient node usage from Kubernetes nodes, and introduces simulation-based extender filtering with configurable scoring diagnostics. Tests cover template-node selection, failure reporting, unregistered nodes, cache isolation, and nested copy independence.

Changes

Scheduler node simulation filtering

Layer / File(s) Summary
DeviceInfo and NodeUsage deep copies
pkg/device/devices.go, pkg/scheduler/nodes.go, pkg/scheduler/nodes_test.go
Device and NodeUsage copies now clone nested maps, slices, node metadata, and device information.
NodeUsage and transient node builders
pkg/scheduler/scheduler.go
Node usage is built from device information, while template nodes are copied and probed to create transient device metadata.
Configurable scoring and failure reporting
pkg/scheduler/score.go
Scoring accepts controls for event recording and detailed failure reasons, and reuses embedded NodeInfo when available.
Simulation-based Filter path
pkg/scheduler/scheduler.go
NodeList requests use simulated usage and return the highest-scoring node, failures, or request-shape-preserving empty results.
Template-node and copy validation
pkg/scheduler/scheduler_test.go, pkg/scheduler/nodes_test.go
Tests verify template-node selection, cache isolation, detailed failures, unregistered nodes, and nested deep-copy independence.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SchedulerFilter
  participant filterSimulation
  participant getSimulationNodesUsage
  participant buildTransientNodeInfo
  participant calcScoreWithOptions
  SchedulerFilter->>filterSimulation: pass template NodeList
  filterSimulation->>getSimulationNodesUsage: build simulated candidates
  getSimulationNodesUsage->>buildTransientNodeInfo: discover devices
  buildTransientNodeInfo-->>getSimulationNodesUsage: return NodeInfo or failure
  getSimulationNodesUsage-->>filterSimulation: return usage and FailedNodes
  filterSimulation->>calcScoreWithOptions: score candidates
  calcScoreWithOptions-->>filterSimulation: return scores
  filterSimulation-->>SchedulerFilter: return selected node and failures
Loading

Suggested labels: approved

Suggested reviewers: wawa0210, ouyangluwei163, archlitchi

Poem

A rabbit hops through nodes so bright,
Cloning cards with care and light.
Templates queue, scores softly sing,
No cache is touched by the filtering.
“Node unregistered!” thumps the ground—
Then the best-scored node is found.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding template node simulation filtering in the scheduler.
✨ 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.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces simulation-based node filtering in the scheduler by adding NodeInfo to NodeUsage, refactoring node usage building, and implementing filterSimulation along with corresponding unit tests. The review feedback highlights two important issues: first, the DeepCopy method for NodeUsage performs a shallow copy of device.DeviceInfo elements containing reference types, which can lead to concurrent mutation issues; second, buildTransientNodeInfo lacks defensive nil checks for node and deviceInfo, risking nil pointer dereferences.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/scheduler/nodes.go
Comment thread pkg/scheduler/scheduler.go

@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/scheduler/scheduler.go (1)

798-817: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Simulation requests still record a pod Event on the zero-resource path.

recordScheduleFilterResultEvent(...) at Line 801 runs before the args.Nodes != nil check, so a simulation (template-node) request that happens to request no resources will still emit an Event on the pod. This contradicts the PR goal of keeping event recording and other mutations disabled for the simulation path. Gate the event on the non-simulation branch.

🛠️ Proposed reorder
 	if resourceReqTotal == 0 {
 		klog.V(1).InfoS("Pod does not request any resources",
 			"pod", args.Pod.Name)
-		s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", fmt.Errorf("does not request any resource"))
 		if args.Nodes != nil {
 			return &extenderv1.ExtenderFilterResult{
 				Nodes:       args.Nodes,
 				FailedNodes: nil,
 				Error:       "",
 			}, nil
 		}
+		s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", fmt.Errorf("does not request any resource"))
 		return &extenderv1.ExtenderFilterResult{
 			NodeNames:   args.NodeNames,
 			FailedNodes: nil,
 			Error:       "",
 		}, nil
 	}
🤖 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/scheduler.go` around lines 798 - 817, Simulation requests on
the zero-resource path are still emitting pod events because
recordScheduleFilterResultEvent in scheduler.go runs before the args.Nodes nil
check. Move the event recording into the non-simulation branch of the
zero-resource handling in the scheduler filter logic, so only the args.NodeNames
path records the event and the args.Nodes template-node path remains side-effect
free. Use the existing resourceReqTotal zero check and the args.Nodes /
args.NodeNames split in the filter path to keep the fix localized.
🧹 Nitpick comments (2)
pkg/scheduler/score.go (1)

105-109: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider an options struct over positional booleans.

calcScoreWithOptions(..., recordEvents, detailedFailureReason) with call sites like true, false is easy to misread/transpose as more callers are added (e.g., the simulation path). An options struct would self-document at call sites.

♻️ Optional refactor
-func (s *Scheduler) calcScore(nodes *map[string]*NodeUsage, resourceReqs device.PodDeviceRequests, task *corev1.Pod, failedNodes map[string]string) (*policy.NodeScoreList, error) {
-	return s.calcScoreWithOptions(nodes, resourceReqs, task, failedNodes, true, false)
+type calcScoreOptions struct {
+	recordEvents           bool
+	detailedFailureReason  bool
+}
+
+func (s *Scheduler) calcScore(nodes *map[string]*NodeUsage, resourceReqs device.PodDeviceRequests, task *corev1.Pod, failedNodes map[string]string) (*policy.NodeScoreList, error) {
+	return s.calcScoreWithOptions(nodes, resourceReqs, task, failedNodes, calcScoreOptions{recordEvents: true, detailedFailureReason: 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/scheduler/score.go` around lines 105 - 109, Refactor the boolean flags on
calcScoreWithOptions into a small options struct so call sites no longer pass
ambiguous positional values like true, false. Update calcScore to construct and
pass the options, and adjust all call sites (including any simulation path) to
use the new self-documenting struct fields instead of raw booleans; keep the
behavior of recordEvents and detailedFailureReason unchanged.
pkg/scheduler/scheduler_test.go (1)

1430-1682: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared test setup into a helper to reduce duplication.

All four new tests repeat identical boilerplate: NewScheduler(), fake clientset wiring, podLister construction, and the sConfig/InitDevicesWithConfig block. Consider a small helper (e.g. newTestSchedulerWithNvidiaConfig(t)) returning a ready *Scheduler to cut repetition and keep future config changes in one place.

♻️ Example helper
func newTestSchedulerForTemplateFilterTests(t *testing.T) *Scheduler {
	s := NewScheduler()
	client.KubeClient = fake.NewSimpleClientset()
	s.kubeClient = client.KubeClient
	s.podLister = informers.NewSharedInformerFactoryWithOptions(client.KubeClient, time.Hour).Core().V1().Pods().Lister()

	sConfig := &config.Config{
		NvidiaConfig: nvidia.NvidiaConfig{
			ResourceCountName:            "hami.io/gpu",
			ResourceMemoryName:           "hami.io/gpumem",
			ResourceMemoryPercentageName: "hami.io/gpumem-percentage",
			ResourceCoreName:             "hami.io/gpucores",
			DefaultGPUNum:                1,
		},
	}
	require.NoError(t, config.InitDevicesWithConfig(sConfig))
	return s
}
🤖 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/scheduler_test.go` around lines 1430 - 1682, All four
template-filter tests duplicate the same scheduler initialization boilerplate,
so extract that setup into a shared helper to keep the tests smaller and
centralize config changes. Create a helper such as
newTestSchedulerForTemplateFilterTests(t) that performs the NewScheduler, fake
clientset wiring, podLister creation, and config.InitDevicesWithConfig setup,
then update TestFilterUsesTemplateNodesWithoutSideEffects,
TestFilterTemplateNodesDoesNotTouchSchedulingCaches,
TestFilterTemplateNodesReturnsDetailedFailureReason, and
TestFilterTemplateNodesMissingRegisterAnnotation to use it.
🤖 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/scheduler/scheduler.go`:
- Around line 798-817: Simulation requests on the zero-resource path are still
emitting pod events because recordScheduleFilterResultEvent in scheduler.go runs
before the args.Nodes nil check. Move the event recording into the
non-simulation branch of the zero-resource handling in the scheduler filter
logic, so only the args.NodeNames path records the event and the args.Nodes
template-node path remains side-effect free. Use the existing resourceReqTotal
zero check and the args.Nodes / args.NodeNames split in the filter path to keep
the fix localized.

---

Nitpick comments:
In `@pkg/scheduler/scheduler_test.go`:
- Around line 1430-1682: All four template-filter tests duplicate the same
scheduler initialization boilerplate, so extract that setup into a shared helper
to keep the tests smaller and centralize config changes. Create a helper such as
newTestSchedulerForTemplateFilterTests(t) that performs the NewScheduler, fake
clientset wiring, podLister creation, and config.InitDevicesWithConfig setup,
then update TestFilterUsesTemplateNodesWithoutSideEffects,
TestFilterTemplateNodesDoesNotTouchSchedulingCaches,
TestFilterTemplateNodesReturnsDetailedFailureReason, and
TestFilterTemplateNodesMissingRegisterAnnotation to use it.

In `@pkg/scheduler/score.go`:
- Around line 105-109: Refactor the boolean flags on calcScoreWithOptions into a
small options struct so call sites no longer pass ambiguous positional values
like true, false. Update calcScore to construct and pass the options, and adjust
all call sites (including any simulation path) to use the new self-documenting
struct fields instead of raw booleans; keep the behavior of recordEvents and
detailedFailureReason unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ac88118-c7cb-4612-822d-5f9ebd17d834

📥 Commits

Reviewing files that changed from the base of the PR and between 4e23779 and 1b4b0ec.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • pkg/scheduler/nodes.go
  • pkg/scheduler/scheduler.go
  • pkg/scheduler/scheduler_test.go
  • pkg/scheduler/score.go

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.49462% with 40 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/device/devices.go 0.00% 18 Missing ⚠️
pkg/scheduler/scheduler.go 87.32% 13 Missing and 5 partials ⚠️
pkg/scheduler/score.go 69.23% 3 Missing and 1 partial ⚠️
Flag Coverage Δ
unittests 59.90% <78.49%> (+0.23%) ⬆️

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

Files with missing lines Coverage Δ
pkg/scheduler/nodes.go 88.57% <100.00%> (+1.68%) ⬆️
pkg/scheduler/score.go 92.80% <69.23%> (+0.42%) ⬆️
pkg/device/devices.go 89.73% <0.00%> (-4.47%) ⬇️
pkg/scheduler/scheduler.go 58.04% <87.32%> (+5.53%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@spencercjh

spencercjh commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Manual integration evidence for the template-node /filter path on my AKS cluster.

Setup:

  • Deployed the branch image as the scheduler extender in-cluster.
  • Port-forwarded svc/hami-scheduler and called the real HTTPS /filter endpoint.
  • Used a real template node shape from the cluster, with hami.io/node-nvidia-register coming from the existing mock-device-plugin.

Template node excerpt used in the warm-path cases:

{
  "metadata": {
    "name": "aks-userpool-85868546-vmss000000",
    "annotations": {
      "hami.io/node-nvidia-register": "[{\"id\":\"GPU-MOCK-0\",\"count\":10,\"devmem\":11441,\"devcore\":100,\"type\":\"NVIDIA-Tesla-K80\",\"mode\":\"hami-core\"},{\"id\":\"GPU-MOCK-1\",\"count\":10,\"devmem\":11441,\"devcore\":100,\"type\":\"NVIDIA-Tesla-K80\",\"mode\":\"hami-core\"}]"
    }
  }
}
  1. Fit case

Request excerpt:

{
  "pod": {
    "metadata": {"name": "template-fit"},
    "spec": {
      "schedulerName": "hami-scheduler",
      "containers": [{
        "resources": {
          "limits": {
            "nvidia.com/gpu": "1",
            "nvidia.com/gpumem": "300",
            "nvidia.com/gpucores": "40"
          }
        }
      }]
    }
  },
  "nodes": {
    "items": [{"metadata": {"name": "aks-userpool-85868546-vmss000000"}}]
  }
}

Response excerpt observed from /filter:

{
  "Nodes": {
    "items": [
      {"metadata": {"name": "aks-userpool-85868546-vmss000000"}}
    ]
  },
  "FailedNodes": null,
  "Error": ""
}
  1. Unfit case (gpumem=12000 > per-device devmem=11441)

Request excerpt:

{
  "pod": {
    "metadata": {"name": "template-unfit"},
    "spec": {
      "containers": [{
        "resources": {
          "limits": {
            "nvidia.com/gpu": "1",
            "nvidia.com/gpumem": "12000",
            "nvidia.com/gpucores": "40"
          }
        }
      }]
    }
  },
  "nodes": {
    "items": [{"metadata": {"name": "aks-userpool-85868546-vmss000000"}}]
  }
}

Response excerpt observed from /filter:

{
  "Nodes": null,
  "FailedNodes": {
    "aks-userpool-85868546-vmss000000": "2/2 CardInsufficientMemory"
  },
  "Error": ""
}
  1. Unregistered case (same template node, but hami.io/node-nvidia-register removed)

Request excerpt:

{
  "pod": {
    "metadata": {"name": "template-unregistered"},
    "spec": {
      "containers": [{
        "resources": {
          "limits": {
            "nvidia.com/gpu": "1",
            "nvidia.com/gpumem": "300",
            "nvidia.com/gpucores": "40"
          }
        }
      }]
    }
  },
  "nodes": {
    "items": [{
      "metadata": {
        "name": "aks-userpool-85868546-vmss000000",
        "annotations": {}
      }
    }]
  }
}

Response excerpt observed from /filter:

{
  "Nodes": null,
  "FailedNodes": {
    "aks-userpool-85868546-vmss000000": "node unregistered"
  },
  "Error": ""
}

These three cases were the manual pre-e2e validation for this PR:

  • warm template node can be parsed from args.Nodes
  • fit/unfit decision reuses existing HAMi scoring/fit logic
  • missing register annotation stays explicitly distinguishable as node unregistered
  • the simulation path returns filter results without requiring a live allocatable device state on the node object itself

@spencercjh

Copy link
Copy Markdown
Contributor Author

/assign fishman

@spencercjh

Copy link
Copy Markdown
Contributor Author

/assign wawa0210

Signed-off-by: spencercjh <jiahao.cai@dynamia.ai>
Signed-off-by: spencercjh <jiahao.cai@dynamia.ai>
@hami-robot hami-robot Bot added the lgtm label Jul 10, 2026
@spencercjh
spencercjh marked this pull request as draft July 10, 2026 10:03
@spencercjh

spencercjh commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@mesutoezdil
I have added a statement at the top of the PR summary. I think we need to update the PR&issue template to require everyone to provide a disclosure for AI-generated content.

@spencercjh
spencercjh marked this pull request as ready for review July 10, 2026 10:11
@hami-robot
hami-robot Bot requested a review from mesutoezdil July 10, 2026 10:11
@mesutoezdil

Copy link
Copy Markdown
Contributor

@mesutoezdil I have added a statement at the top of the PR summary. I think we need to update the PR&issue template to require everyone to provide a disclosure for AI-generated content.

thx, can you open an pr for that? we have done few things last week (meant new rules for contributions), sometimes some things are being overlooked cc @archlitchi

@mesutoezdil

Copy link
Copy Markdown
Contributor

/lgtm

@spencercjh

spencercjh commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

We ran a dedicated validation for cold-zero node group scale-up and the current result is:

Without additional changes, HAMi does not yet support cold-zero node group scale-up.

What is already validated is the warm-path case: when the node group already has an existing node template that can be evaluated with the expected GPU resource surface, Cluster Autoscaler can make the scale-up decision and the workload can eventually land after the new node finishes HAMi/device-plugin registration.

What is not working yet is the cold-zero path, where the target node group starts from 0 nodes.

Our experiments narrowed the problem down as follows:

  • For a CPU-only zero-size pool, the CA template node is missing nvidia.com/gpu, nvidia.com/gpucores, and nvidia.com/gpumem, so the pod is rejected immediately and CA reports NotTriggerScaleUp.
  • For a real GPU zero-size pool, the raw nvidia.com/gpu signal is present, so this is not a generic “no GPU at all” issue.
  • However, the template-time resource surface is still missing HAMi-relevant extended resources, most importantly nvidia.com/gpucores (and in some variants nvidia.com/gpumem).
  • Even when the pod is authored as GPU-only, HAMi admission mutates it before scheduling by automatically injecting nvidia.com/gpucores=100, so the request that reaches CA/template-time scheduling still depends on that extended-resource contract.
image

So the current boundary is:

  • Warm path: validated
  • Cold-zero path: still unsupported
  • Main gap: CA/template-node evaluation for a zero-size GPU node group still cannot see the full HAMi GPU resource profile required by the admitted pod request

A simplified sequence is shown below:

sequenceDiagram
    participant Pod as GPU Pod
    participant Webhook as HAMi Admission Webhook
    participant CA as Cluster Autoscaler
    participant Template as Zero-size Template Node
    participant HAMi as HAMi Scheduler Logic

    Pod->>Webhook: Create pod (author may request only nvidia.com/gpu)
    Webhook->>Pod: Mutate request (inject nvidia.com/gpucores=100)
    Pod->>CA: Unschedulable pod reaches CA evaluation
    CA->>Template: Build/evaluate zero-size nodegroup template
    Template-->>CA: GPU SKU may expose nvidia.com/gpu
    Template-->>CA: Missing nvidia.com/gpucores (and sometimes gpumem)
    CA-->>Pod: NotTriggerScaleUp
    Note over CA,HAMi: Cold-zero fails before a real node is created
Loading

In short, this work shows that supporting cold-zero scale-up requires additional changes so that the zero-size template path exposes the HAMi GPU resource contract expected after admission, rather than only the base GPU presence.

@archlitchi

Copy link
Copy Markdown
Member

We ran an end-to-end validation for warm nodegroup scale-up in an AKS cluster with HAMi enabled, and the current result is:

Warm nodegroup scale-up is validated.

What this test proves is not just that Cluster Autoscaler can increase the VMSS size, but that a GPU workload can complete the full path from unschedulable on the original node to successfully landing on a newly created node after HAMi-visible GPU resources become available there.

The workload pattern was intentionally chosen to match HAMi's real resource semantics. We used pods requesting nvidia.com/gpu=1, nvidia.com/gpucores=100, and nvidia.com/gpumem=11000. Two such pods (pack-a and pack-b) were first scheduled onto the original userpool node aks-userpool-85868546-vmss000000, and then a third pod (scaleup-c) with the same request was submitted. At that point HAMi correctly rejected the original node because GPU memory was exhausted, with logs showing CardInsufficientMemory and NodeUnfitPod ... reason="2/2 CardInsufficientMemory".

After the original node was saturated, Cluster Autoscaler treated scaleup-c as unschedulable and triggered scale-up for the userpool VMSS. The key HAMi-side observation came after the new node appeared. A fresh node (aks-userpool-85868546-vmss000005) becoming merely Ready was not sufficient for GPU placement. In this setup, the new node still had to expose the minimum HAMi/mock-device registration surface before scheduling could succeed. In practice, that meant the node needed the registration annotation (hami.io/node-nvidia-register) and a nonzero nvidia.com/gpu resource, and then the hami-mock-device-plugin still needed to actually run on that node after bootstrap constraints were gone.

The final success happened only after that bootstrap phase completed and the plugin finished registering the expected allocatable resources on the new node. At that point the node began advertising allocatable nvidia.com/gpu=20, allocatable nvidia.com/gpucores=200, and allocatable nvidia.com/gpumem=22882, and the plugin logs started reporting successful registration such as Device Registered gpumem 22882, Device Registered gpumem-percentage 200, and Device Registered gpucores 200. Once those HAMi-visible resources became available, HAMi successfully bound ca-hami-pr-scaleup-c to aks-userpool-85868546-vmss000005, completing the full warm-nodegroup scale-up loop.

So the current boundary is:

  • Warm nodegroup path: validated
  • What was required on the fresh node in this test: not just Ready, but successful HAMi/mock-device registration
  • Main HAMi-side conclusion: end-to-end scale-up succeeds once the newly created node exposes the minimum HAMi-visible GPU resource surface and the device plugin finishes registering allocatable resources there

A simplified sequence is shown below:

sequenceDiagram
    participant Pod as scaleup-c
    participant HAMi as HAMi Scheduler
    participant Node0 as Original Node
    participant CA as Cluster Autoscaler
    participant Node1 as New Node
    participant Plugin as hami-mock-device-plugin

    Pod->>HAMi: Request gpu=1, gpucores=100, gpumem=11000
    HAMi->>Node0: Try scheduling on original node
    Node0-->>HAMi: CardInsufficientMemory
    HAMi-->>Pod: Pod remains unschedulable
    Pod->>CA: Enter autoscaler evaluation
    CA->>CA: Trigger warm-nodegroup scale-up
    CA->>Node1: Create new VMSS node
    Node1-->>CA: Node becomes Ready
    Note over Node1,Plugin: Ready alone is not sufficient
    Plugin->>Node1: Register HAMi-visible GPU resources
    Node1-->>HAMi: Expose allocatable gpu/gpucores/gpumem
    HAMi->>Node1: Re-evaluate placement
    HAMi-->>Pod: Bind pod successfully
Loading

In short, this test demonstrates that warm-nodegroup scale-up works. The remaining requirement on the newly created node is that HAMi/device-plugin registration must complete so the node exposes the GPU resource surface expected by HAMi scheduling.

If the real device-plugin is used instead of the mock-device-plugin, and the labelSelector condition is guaranteed to match (for example agentpool=userpool), I expect this fresh-node registration step to be more natural and less likely to be the limiting factor seen in this validation.

image image

can you describe the spec of your user pool and node (aks-userpool-85868546-vmss000000) here? it seems it has two GPUs, but i'm not quite certain

@archlitchi

Copy link
Copy Markdown
Member

I think we can accept this design, but we need to note some risks here:

  1. this implementations rely on 'args.Nodes!=nil' to determine if this request if simulate or not, it's not a standard protocol.
  2. We still need a design document about this feature on docs/design folder

@archlitchi archlitchi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

/lgtm

@hami-robot

hami-robot Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: archlitchi, spencercjh

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

The pull request process is described 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 merged commit 5f17e8a into Project-HAMi:master Jul 13, 2026
16 checks passed
@spencercjh
spencercjh deleted the feat/dry-run-filter branch July 13, 2026 09:55
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.

5 participants