tigerquoll opened a new pull request, #1122:
URL: https://github.com/apache/yunikorn-core/pull/1122

   
   Part of YUNIKORN-3350, which splits one change into three independent levers.
   
   **Depends on YUNIKORN-3351** (hoist the per-ask backoff getter). GitHub 
requires a pull request's base
   to be a branch in this repository, so until 3351 merges this PR's diff 
necessarily contains its commit
   as well. **The commits belonging to this change are the last two**; the 
first, "Hoist the per-ask
   backoff getter out of tryAllocate", is 3351 and is a four-line change. I 
will rebase once 3351 lands.
   
   The third lever, YUNIKORN-3353 (keep only pending asks in sortedRequests), 
is deliberately held back
   until these two are settled — it carries an ordering change that deserves to 
be reviewed on its own.
   
   ## What
   
   `Application.updateAskMaxPriority` iterates the whole `sa.requests` map to 
recompute the highest
   priority among asks that are not yet allocated. It is called from 
`allocateAsk` on every allocation
   whose priority is at or above the current maximum, and again from 
`removeAsksInternal` on ask removal.
   Each call is O(number of asks), so scheduling N asks costs O(N^2), and the 
cost grows as an
   application gets larger.
   
   It cannot stop early, because pending and allocated asks share one map — it 
has to test
   `IsAllocated()` on every entry, and that takes a read lock each time. In a 
CPU profile of master the
   function is **55% of `Application.tryAllocate`**, split 56% map iteration, 
22% `IsAllocated`, 15%
   `GetPriority`.
   
   Replace it with a per-priority histogram of pending asks (`pendingPriorities 
map[int32]int`),
   maintained incrementally as asks enter and leave the pending set, so 
`askMaxPriority` is available in
   O(1). `updateAskMaxPriority` is deleted.
   
   This PR touches `application.go` only. It changes no ordering semantics 
anywhere.
   
   ## Design notes
   
   A map of counts rather than a set, because many asks share a priority and 
the structure has to know
   when a priority band actually empties. Every operation is O(1) except one: 
when the current top bucket
   empties, the new maximum is recomputed by scanning the map keys, which is 
O(distinct priority values
   in use) — a handful in practice, not O(number of asks).
   
   `setAskMaxPriority` propagates to the queue only when the value actually 
changes, so this also issues
   strictly fewer `UpdateApplicationPriority` calls than the rescan it replaces.
   
   The non-O(1) path is worth quantifying, since it is the obvious question. 
`askMaxPriority` is read by
   all four application sort policies (`considerPriority` decides whether 
priority is the primary or
   secondary key, not whether it is consulted), so reads sit on the scheduling 
loop and scale with
   applications per queue. Recomputation happens only when the *top* band 
empties. Measured on
   BenchmarkScheduling, 5000 nodes, 10000 asks, with both applications in one 
queue so the sort
   comparator actually runs:
   
   ```
   GetAskMaxPriority = 10,000    inc = 10,000    dec = 10,000
   recomputations    = 2         map keys scanned across both = 0
   ```
   
   Two recomputations, one per application, when its last pending ask is 
allocated — a 5,000:1 ratio of
   reads to recomputations. Under an adversarial mix (the property fuzzer, 11 
distinct priorities, heavy
   add/allocate/deallocate churn) it rises to 10.5% of decrements, still 
averaging only 2.0 map keys
   each. Even the pathological shape — a high-priority ask interleaved with 
lower ones so the top band
   drains on every allocation — is O(asks x distinct priorities), against the 
O(asks^2) it replaces.
   
   One guard comes with it. `deallocateAsk` returns an ask to the histogram 
only when it is still the
   tracked object for its key. `removeAsksInternal("")` wipes `sa.requests` 
while `sa.allocations`
   survives until the shim confirms the releases, and a release arriving in 
that window reaches
   `RollbackAllocation`. The rescan derived the maximum by scanning 
`sa.requests`, so an ask absent from
   it never influenced the converged result; the guard preserves that.
   
   ## Measured effect
   
   `BenchmarkScheduling`, allocate phase, 10000 pods, alternating A/B, 3 
samples, medians. Linux 6.8
   aarch64, 6 CPUs, GOMAXPROCS=6. Cumulative figures — the stack up to and 
including this PR, against
   master:
   
   | Configuration | master (c/s) | this PR (c/s) | cumulative | this PR alone |
   |---|---:|---:|---:|---:|
   | 500 nodes  | 5,481 | 16,832 | 3.07x | 2.49x |
   | 1000 nodes | 5,517 | 16,351 | 2.96x | 2.49x |
   | 2000 nodes | 5,396 | 16,419 | 3.04x | 2.49x |
   | 5000 nodes | 5,353 | 16,333 | 3.05x | 2.44x |
   
   "This PR alone" is the ratio against the previous PR in the stack. Spread up 
to 7%.
   
   Scope: mock resource manager, no real bind, so this is core scheduling 
headroom rather than a
   real-cluster pods/second figure. The workload is 2 applications x 5,000 
asks; the cost removed is
   per-application and quadratic in that application's ask count, so this shape 
is favourable — note
   master's throughput is flat across node counts, which is the per-app rescan 
dominating everything
   else. A cluster of many small applications sees a proportionally smaller 
absolute gain.
   
   ## Behaviour
   
   Unchanged. The golden decision-trace tests added in YUNIKORN-3338 reproduce 
byte-for-byte, goldens not
   regenerated. They pin the ordered sequence of decisions the resource manager 
observes through
   `UpdateAllocation`, so reproducing them is the claim.
   
   One edge worth stating precisely: `cleanupAsks` now also resets the 
histogram and `askMaxPriority`.
   That happens only on entering a terminal state (Completed/Failed), where the 
application is no longer
   schedulable, so it changes no scheduling decision — but it does mean a 
terminal application no longer
   retains a stale `askMaxPriority`.
   
   A property fuzzer (`application_property_test.go`) drives an Application 
through long randomized
   interleavings of the real entry points — `AddAllocationAsk` including the 
replace-an-existing-ask
   branch, `AllocateAsk`, `DeallocateAsk`, `RemoveAllocationAsk`, 
`RecoverAllocationAsk`,
   `AddAllocation`, `RollbackAllocation`, and FSM-driven cleanup — and after 
every single step
   re-derives the expected histogram and `askMaxPriority` from an independent 
reference model. Coverage
   high-water marks are asserted so an operation that becomes a silent no-op 
fails rather than passing
   trivially.
   
   ## Concurrency
   
   No new lock acquisition and no new ordering. `setAskMaxPriority` takes the 
queue lock under the
   application lock exactly as the rescan it replaces did, and does so less 
often. All histogram
   mutation happens under the application write lock; the helpers are unlocked 
internals whose callers
   all hold it. Verified green under `-race`, and under go-deadlock with 
lock-order detection armed
   (`DEADLOCK_DETECTION_ENABLED=true DEADLOCK_DISABLE_LOCK_ORDER=false`).
   
   ## Scope and rollback
   
   No configuration, REST, scheduler-interface, or metrics changes. No new 
public API. No persisted or
   serialized state — `pendingPriorities` is core-internal and never crosses a 
process or version
   boundary, so there is nothing to migrate and no mixed-version concern during 
a rolling upgrade.
   Rollback is a single revert.
   
   Generated by Author with assistance from Claude Code.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to