diff --git a/pdm.lock b/pdm.lock index 7a0eb86c..c47164fe 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "bfcl-v3", "code-eval", "dev", "drop", "hle", "ifbench", "ifeval", "iheval", "math", "multi-if", "nl2sh-alfa", "ruler", "ruler-gen", "scicode", "spider", "spider2", "t-eval", "test", "wikisql"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:c6f3a369359f801ffecefd39231481cf798fd2cee34c9b39fa5a0ffde12db7b0" +content_hash = "sha256:e866a7d5a816db521257f4dc9ac9dc12e37d43dd7d667f47da49c0e1e49b09bb" [[metadata.targets]] requires_python = ">=3.12,<3.15" @@ -175,18 +175,18 @@ files = [ [[package]] name = "anyio" -version = "4.12.0" -requires_python = ">=3.9" +version = "4.15.1" +requires_python = ">=3.10" summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["default", "code-eval"] dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", - "typing-extensions>=4.5; python_version < \"3.13\"", + "typing-extensions>=4.16.0; python_version < \"3.15\"", ] files = [ - {file = "anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb"}, - {file = "anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0"}, + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, ] [[package]] @@ -3939,13 +3939,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" requires_python = ">=3.9" summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "code-eval", "dev", "ruler", "ruler-gen", "spider2", "t-eval", "test"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 4396a314..0139072e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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. diff --git a/tests/unit/core/utils/test_concurrency.py b/tests/unit/core/utils/test_concurrency.py index 27291344..a599eb85 100644 --- a/tests/unit/core/utils/test_concurrency.py +++ b/tests/unit/core/utils/test_concurrency.py @@ -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