RFC: hugepage/memory fit admission for best-of-K placement (minimal viable change)
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 38/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- go
- Domain
- backend, infrastructure
Research direction
Start in packages/api/internal/orchestrator/placement/placement_best_of_K.go, reading sample(), chooseNode(), the existing memoryScore accounting, and BestOfKConfig. Review the proposed placement unit cases for capacity filtering, no-pool nodes, all-full sets, and the K-sampling fallback; done means the agreed admission behavior, configuration and error handling are covered by tests, with the open rollout and widening questions resolved.
Written by the indexing model from the issue text.
Description
RFC: Hugepage/memory fit admission for best-of-K placement (minimal viable change)
Summary
Add a hard capacity (hugepage/memory) admission filter to the best-of-K placement sample() so that a node whose hugepage pool cannot hold the requested sandbox is never selected. Today hugepage headroom only influences ranking (memoryScore), never candidacy, so a full node can still win a sample and the request fails downstream on the orchestrator as 500: Failed to place sandbox. This RFC proposes the smallest change that closes that gap, gated behind a config flag with a shadow-then-enforce rollout.
This is the policy counterpart to #3553 (which asks to expose why placement failed) and the direct fix for the problem described in #3656.
Motivation
packages/api/internal/orchestrator/placement/placement_best_of_K.go --- sample() filters candidate nodes on exactly five predicates:
- excluded set
CanAcceptNewRequests()NodeSatisfiesCPUNodeSatisfiesFeatures- label compatibility
None checks whether the node can physically hold the sandbox. Hugepage-pool load appears only as a soft term in memoryScore, used to rank survivors --- it never removes a node from candidacy. So under memory/hugepage pressure the placement engine can (a) pick a full node that then fails on the node side, or (b) when best-of-K samples only full nodes, fail the whole request. Observed on the dev cluster as batch runs where an entire concurrency level returned SandboxException: 500: Failed to place sandbox.
The failure is a placement-admission gap, not (always) genuine cluster exhaustion: the API had enough information locally to reject fast, but instead spent an API---orchestrator---placement round trip to fail slowly.
Goals
- Never forward a create to a node whose hugepage pool cannot hold it.
- Reuse the exact accounting
memoryScorealready uses, so ranking and admission never disagree. - Zero-risk rollout: ship behind a flag, observe in shadow mode, then enforce.
- Preserve today's behavior for nodes that report no hugepage pool.
Non-Goals
- Not raising per-node density. Higher density needs memory sharing / reclamation (KSM, ballooning, proactive reclaim) --- out of scope here.
- Not changing CPU over-commit policy (
R), which is a soft-score dimension and rarely a hard-failure source. - Not the diagnostics surface --- that is #3553; this RFC only references it for the error-differentiation touchpoint.
Proposed design (minimal viable change)
All changes are confined to the placement package; no proto, no orchestrator, no node-agent changes. Node metrics already carry HugePagesTotal / HugePagesUsed / HugePagesReserved / HugePageSizeBytes.
1. Capacity helper (same accounting as memoryScore)
// nodeHasCapacity reports whether the node's hugepage pool can hold this
// sandbox: pool total minus (used + reserved + in-flight pending) must leave
// room for the request. A node reporting no pool (HugePagesTotal == 0) is
// treated as "unknown" and left to the soft score, preserving current behavior.
func nodeHasCapacity(n *nodemanager.Node, resources nodemanager.SandboxResources) bool {
m := n.Metrics()
if m.HugePagesTotal == 0 || m.HugePageSizeBytes == 0 {
return true
}
var pendingMiB int64
for _, r := range n.PlacementMetrics.InProgress() {
pendingMiB += r.MiBMemory
}
need := hugePagesFor(resources.MiBMemory, m.HugePageSizeBytes) +
hugePagesFor(pendingMiB, m.HugePageSizeBytes)
committed := m.HugePagesUsed + m.HugePagesReserved
if committed >= m.HugePagesTotal {
return false
}
return m.HugePagesTotal-committed >= need
}
2. Wire it into sample()
sample() gains the resources nodemanager.SandboxResources parameter (already available in chooseNode, one-line call-site change), and after the label filter:
if config.HugepageFitFilter && !nodeHasCapacity(n, resources) {
nodeFitRejectedCounter.Add(ctx, 1) // observability
continue
}
3. Config flag (mirrors BEST_OF_K_HUGEPAGE_MEMORY)
type BestOfKConfig struct {
R float64
Alpha float64
K int
ScoreHugepages bool
HugepageFitFilter bool // new; env BEST_OF_K_HUGEPAGE_FIT_FILTER, default false
}
4. K-sampling blind spot (the one non-trivial correctness point)
best-of-K samples only K nodes. With enforcement on, all K sampled nodes could be fit-rejected while free nodes exist but simply were not sampled. Mitigation: when the K-sample yields zero candidates and fit-filter is enabled, widen the search (one extra sampling round, or fall back to a full scan) before declaring failure. Without this, enforcement could fail placements that should succeed.
5. Error differentiation (ties into #3553)
When candidates end up empty, distinguish "no node with capacity" from "no compatible node (CPU/feature/label)". This lets operators tell scale up from misconfiguration --- exactly the ambiguity behind opaque Failed to place sandbox today.
Rollout plan
- Shadow (
HugepageFitFilter=false): helper runs,nodeFitRejectedCounterincrements for would-be rejections, but no node is actually filtered. Measure how often a "would-be-rejected" node was in fact the one that later failed placement --- quantifies benefit with zero behavior change. - Enforce (
HugepageFitFilter=true): flip per environment once the metric validates.
Alternatives considered
- Rank-only (status quo): soft
memoryScorealready exists but does not prevent selecting a full node --- this is the bug. - Reject only at orchestrator: already happens; it is the slow round-trip failure we want to avoid.
- Global bin-packing / scheduler rewrite: much larger blast radius; unnecessary to close this specific gap.
Testing
- Unit: near-full node excluded from candidates; all-full node set --- empty candidates + distinct "no capacity" error; no-pool node unaffected; K-blind-spot fallback finds a free node outside the initial sample.
- A/B on a step-laddered replay (fit-filter off vs on): compare
Failed to placerate and placement latency. - Shadow-mode counter validated before enforcing.
Backwards compatibility
Default false --- behavior identical to today until explicitly enabled. Nodes without a reported pool are never hard-blocked. No API/proto/orchestrator changes.
Open questions
- Preferred widening strategy for the K blind spot: extra sampling round vs. full scan on empty result?
- Should the flag be global or per-cluster/per-pool?
- Should CPU over-commit (
R) get a symmetric hard admission later, or stay soft?
Filed per CONTRIBUTING (issue-first for non-trivial changes). Happy to send the PR once the direction and the two open questions above are confirmed. Refs #3656 (problem), #3553 (diagnostics).
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from e2b-dev/runtime
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
sandbox cache: StartRemoving state transition not broadcast, all allocations see stale Running state Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
Difficulty 1/5 Under an hour Newbie friendliness 86/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Similar issues
-
area/dev-productivity area/disaster-recovery area/ipcei kind/enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
kind/bug status/0-triage
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
🤔 refinement needed
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
equinor/radix-operator#1979 ·