[unsupervised AI] Schedule resource-restricted tasks against free resources - #9341
[unsupervised AI] Schedule resource-restricted tasks against free resources#9341vedjaw wants to merge 1 commit into
Conversation
valid_workers() compared a task's resource requirements against each worker's declared total, ignoring what that worker had already been given. The scheduler therefore committed more of a resource than exists: with a worker declaring A: 10 and fifteen tasks asking for A: 3 each, ws.used_resources["A"] reaches 45 while the worker itself only ever runs three of them. Subtracting used_resources is not sufficient on its own. Tasks held back that way land in no-worker, and the only event that revisited no-worker tasks was a worker joining, so they stayed there forever. This adds stimulus_resources_maybe_released(), a sibling of stimulus_queue_slots_maybe_opened() called from the same sites, which retries those tasks one at a time as running tasks release what they hold. no-workers-timeout also needed teaching: a task waiting for a busy resource is not a task with unsatisfiable restrictions, and would otherwise be failed with NoValidWorkerError. valid_workers() grows an only_available argument so the timeout can ask whether the restrictions are satisfiable at all. Resolves dask#9108 Assisted-by: Claude Fable 5
Unit Test ResultsSee test report for an extended history of previous test failures. This is useful for diagnosing flaky tests. 40 files ± 0 40 suites ±0 15h 9m 46s ⏱️ + 24m 32s For more details on these failures, see this check. Results for commit 7aac210. ± Comparison against base commit 40fcd99. |
|
CI settles the design question this PR raised, and not in this PR's favour. Recording it here rather than papering over it.
@gen_cluster(client=True, nthreads=[("", 1, {"resources": {"A": 1}})])
...
fut1 = c.submit(block_execution, ..., resources={"A": 1}) # holds the only A
await executing.wait()
fut2 = c.submit(inc, 1, resources={"A": 1})
while fut2.key not in a.state.tasks: # <-- spins forever now
await asyncio.sleep(0.01)It waits for That makes four existing tests that depend on the scheduler over-assigning resource-restricted tasks — Combined with what I noted at the top of this PR — So I don't think this PR should be pursued. Rewriting a fourth deliberate test to accommodate it would be bending the project around a premise that doesn't hold. If #9108 is worth acting on, the tractable part is that the dashboard can display 45 of 10 in use — a reporting fix, not an admission-control change. Leaving the branch up in case the analysis is useful. Recommend closing. The two other red signals here are unrelated to this diff: |
Warning
This PR was written autonomously by an AI agent and has not been reviewed
by a human yet. Maintainers should ignore it until the human author has reviewed,
understood, and approved everything that the AI agent wrote.
Resolves #9108.
Read this part first
This is a behaviour change, not a self-evident bug fix, and I want to be upfront about what the investigation actually found rather than argue only the side that supports the diff.
The issue's second claim does not hold. It says the scheduler "makes poor decisions based on wrong resource state".
ws.used_resourceshas exactly six references in the tree: its declaration, its initialisation, the increment inacquire_resources, the decrement inrelease_resources, the reset inadd_resources, anddistributed/http/templates/worker.html. It is never read by any scheduling decision.valid_workersreadsself.resources, the declared totals. So the over-commitment is not feeding back into placement.The over-assignment looks deliberate.
test_dont_steal_resource_restrictionsand (before this PR)test_steal_resource_restrictionsboth assert that 100 tasks asking forA: 1are all assigned to a worker declaringA: 2. The worker throttles execution itself in_resource_restrictions_satisfied, so handing it more than it can run at once is how it stays fed without a scheduler round-trip between tasks.Put together: the concrete harm from #9108 is that
used_resourcesreports assigned rather than executing, so the dashboard can show 45 of 10 in use. This PR fixes that by making the scheduler stop over-committing, which is the interpretation the issue asks for — but it does so by changing scheduling behaviour that appears to be intended, and it required rewriting one existing test. A maintainer may well prefer to keep the current admission behaviour and change what the counter or the dashboard means instead. I have no way to judge which of those you want, and @jacobtomlinson said in the issue that there is nobody available to guide the direction, so I would rather hand over the evidence than quietly pick.What the change does
Three coordinated pieces. Each is load-bearing; I verified the tests fail with any one of them removed.
1.
valid_workersconsiders free resources. A worker only counts for a resource restriction when it has enough of that resource unused.2. Held-back tasks get retried. Subtracting
used_resourceson its own is not enough: tasks that no longer fit land inno-worker, and the only event that revisitedno-workertasks was a worker joining, so they stayed there forever. This is the failure mode @g199209 hit with the monkeypatch in the issue thread.stimulus_resources_maybe_released()is a sibling ofstimulus_queue_slots_maybe_opened(), called from the same sites, and retries those tasks as running tasks release what they hold. Tasks are transitioned one at a time so each takes its resources before the next is considered, which both avoids bouncing tasks back tono-workerand keeps the retry bounded by what actually fits.3.
no-workers-timeoutno longer kills them. A task waiting for a busy resource is not a task with unsatisfiable restrictions, but_check_unrunnable_task_timeoutswould have failed it withNoValidWorkerError.valid_workersgrows anonly_availableargument so the timeout can ask the different question — are these restrictions satisfiable at all — and skip tasks that are merely waiting.The test I had to change
test_steal_resource_restrictionsusedA: 2andA: 4and expected all 100 tasks to be assigned to one worker, which only held because of the over-commitment this PR removes. I raised both capacities toA: 100so the assignment is legitimate and the test still checks what it is named for, that stealing rebalances resource-restricted tasks.test_dont_steal_resource_restrictionsstill covers a thief that cannot satisfy the restriction at all, andtest_steal_resource_restrictions_asym_diffpasses untouched.If you would rather that test keep its original capacities, that is a signal this PR is the wrong direction and I would close it rather than argue.
Tests added
test_resources_not_over_allocated— the scenario from the issue. Assertsused_resourcesnever exceeds what the worker declared and that the surplus tasks are visible as unrunnable instead of being reported as processing. Fails onmain.test_resource_tasks_rescheduled_when_resources_released— a held-back task becomes runnable when a blocker finishes, with no new worker joining. Hangs until timeout without piece 2.test_no_workers_timeout_does_not_fail_tasks_awaiting_resources— withno-workers-timeout: 100ms, a task waiting on a busy resource survives. Fails onmain.test_no_workers_timeout_still_fails_unsatisfiable_resources— a genuinely impossible restriction still times out.What I ran
test_resources.pyandtest_steal.pyin full: 108 passed, 18 skipped (skips are missing pandas/numpy locally).test_scheduler.py: 527 passed. Eight failures (test_scheduler_file,test_profile_metadata,test_async_context_manager,test_finished,test_no_dangling_asyncio_tasks,test_multiple_listeners×2,test_transition_failure_triggers_log_event) reproduce identically on an unmodified checkout on this machine — macOS, Python 3.14, no bokeh — so they are environmental, not from this change. I would want CI to confirm that.I have not benchmarked the throughput effect of piece 2. Resource-restricted tasks now get a scheduler round-trip per released slot instead of sitting pre-assigned in a worker's queue, and on short tasks that could measurably hurt. That is the strongest argument against this direction and it deserves a number before anyone merges it.