Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ description = "SiEval - Model Delivery Quality Verification System"
authors = [{ name = "ScitiX" }]
dynamic = ["version"]
dependencies = [
"anyio>=4.11.0",
# 4.15 hands a released CapacityLimiter token to the waiter it wakes; before,
# a task scheduled ahead of that waiter took the token too, so every stage
# and model concurrency limit could run one task over.
"anyio>=4.15.0",
# Not imported here: anyio 4.15's own floor, declared so the lock bump it
# forces is reviewable (`check_deps` does not credit a moved package).
"typing-extensions>=4.16.0",
# Direct, not just via typer: sieval/cli/output.py imports click for the
# exception hierarchy typer does not re-export (ClickException, UsageError,
# get_current_context). Floor matches typer 0.24's own requirement.
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/core/utils/test_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,36 @@ async def test_returns_self_on_aenter(self):
result = await composite.__aenter__()
assert result is composite
await composite.__aexit__(None, None, None)


class TestCapacityLimiterHandoff:
"""The stage and model limits rest on ``anyio.CapacityLimiter`` holding its
bound, which anyio before 4.15 did not: a release woke the next waiter
without handing it the token, so a task scheduled before that waiter ran
found the queue empty, took the token, and the limit held one more task
than it allowed. That is how ``concurrency_limits={"infer": 4}`` was seen
to run 5 inferences at once."""

@pytest.mark.anyio
async def test_a_release_hands_the_token_to_the_waiter(self):
limiter = anyio.CapacityLimiter(1)
holding = peak = 0

async def hold():
nonlocal holding, peak
async with limiter:
holding += 1
peak = max(peak, holding)
await anyio.sleep(0.01)
holding -= 1

async with anyio.create_task_group() as tg:
await limiter.acquire()
tg.start_soon(hold)
await anyio.sleep(0.01) # the first task now waits in the queue
# One step, no checkpoint between: a newcomer is scheduled ahead of
# the waiter's wake-up, then the token is released.
tg.start_soon(hold)
limiter.release()

assert peak == 1
Loading