Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
f405a5a
model, conductor: refuse max_tokens past the decode loop
Gaurav-Shah05 Oct 21, 2026
75d277b
test: a request past the decode loop is refused
Gaurav-Shah05 Oct 21, 2026
81caa14
kv, model: bagel sinks an unguided row's guidance labels
Gaurav-Shah05 Oct 22, 2026
5e5e265
test: an unguided bagel row holds no guidance pages
Gaurav-Shah05 Oct 22, 2026
e54f647
kv: a page footprint from a label's layout
Gaurav-Shah05 Oct 21, 2026
b5f51c9
model, runtime: a loop's per-request cap becomes its max_iters
Gaurav-Shah05 Oct 21, 2026
38d324a
model, kv: process_prompt's layout reaches the KV manager
Gaurav-Shah05 Oct 21, 2026
6aacca1
kv: PrefixStream becomes a mark with one span cursor
Gaurav-Shah05 Oct 21, 2026
b4d1588
kv: always-on page check and debug span checks
Gaurav-Shah05 Oct 21, 2026
a56f074
model: orpheus caps decode where its stop check ends it
Gaurav-Shah05 Oct 21, 2026
035d013
model: bagel lays out its guidance labels and caps decode
Gaurav-Shah05 Oct 21, 2026
cd7b963
model: chatterbox declares its KV layout
Gaurav-Shah05 Oct 21, 2026
8556544
model: cosmos3 declares its KV layout
Gaurav-Shah05 Oct 21, 2026
5a9de78
model: fish_s2_pro declares its KV layout
Gaurav-Shah05 Oct 21, 2026
5cb722d
model: higgs_audio declares its KV layout
Gaurav-Shah05 Oct 21, 2026
5e52075
model: nemotron_duplex declares its KV layout
Gaurav-Shah05 Oct 21, 2026
40b20d5
model: pi05 declares its KV layout
Gaurav-Shah05 Oct 21, 2026
94e27f0
model: qwen3_5 declares its KV layout through a shared prompt_spans
Gaurav-Shah05 Oct 21, 2026
1e5fddc
model: qwen3_omni declares its KV layout
Gaurav-Shah05 Oct 21, 2026
5f47065
model: qwen3_tts declares its KV layout
Gaurav-Shah05 Oct 21, 2026
ae98163
model: vjepa2 declares its KV layout
Gaurav-Shah05 Oct 21, 2026
4d83e26
model: whisper declares its KV layout
Gaurav-Shah05 Oct 21, 2026
6581e41
worker, conductor: refuse a paged label left undeclared
Gaurav-Shah05 Oct 21, 2026
31dfa9e
test: footprints, span checks, loop caps and model layouts
Gaurav-Shah05 Oct 21, 2026
8aedef5
test: refusals of an undeclared paged label
Gaurav-Shah05 Oct 21, 2026
15c2e4f
docs: paged KV layouts, and prefix keys as a mark on a label
Gaurav-Shah05 Oct 21, 2026
3052673
comment pass
Gaurav-Shah05 Oct 22, 2026
8d4eff1
comment pass
Gaurav-Shah05 Oct 22, 2026
0caa314
Merge fix/bagel-guidance-sink
Gaurav-Shah05 Oct 22, 2026
a971df3
kv: count a window's scratch and its commit once
Gaurav-Shah05 Oct 22, 2026
8afff35
test pass
Gaurav-Shah05 Oct 22, 2026
623fc78
Merge kv-footprints
Gaurav-Shah05 Oct 22, 2026
85bb0df
conductor: refuse a loop cap past its max_iters
Gaurav-Shah05 Oct 22, 2026
b311152
conductor: refuse a request past its pool's context
Gaurav-Shah05 Oct 22, 2026
f9416fb
model: decode loops run to the context length
Gaurav-Shah05 Oct 22, 2026
441e94c
kv: a page footprint from a label's layout
Gaurav-Shah05 Oct 21, 2026
8484a1a
model, runtime: a loop's per-request cap becomes its max_iters
Gaurav-Shah05 Oct 21, 2026
c195820
model, kv: process_prompt's layout reaches the KV manager
Gaurav-Shah05 Oct 21, 2026
fe10c6b
kv: PrefixStream becomes a mark with one span cursor
Gaurav-Shah05 Oct 21, 2026
79ca1bf
kv: always-on page check and debug span checks
Gaurav-Shah05 Oct 21, 2026
e5d052e
model: orpheus caps decode where its stop check ends it
Gaurav-Shah05 Oct 21, 2026
760eba7
model: bagel lays out its guidance labels and caps decode
Gaurav-Shah05 Oct 21, 2026
fb6d718
model: chatterbox declares its KV layout
Gaurav-Shah05 Oct 21, 2026
c62d8e7
model: cosmos3 declares its KV layout
Gaurav-Shah05 Oct 21, 2026
ce8cd23
model: fish_s2_pro declares its KV layout
Gaurav-Shah05 Oct 21, 2026
4fe5963
model: higgs_audio declares its KV layout
Gaurav-Shah05 Oct 21, 2026
06ccb0a
model: nemotron_duplex declares its KV layout
Gaurav-Shah05 Oct 21, 2026
0806f64
model: pi05 declares its KV layout
Gaurav-Shah05 Oct 21, 2026
70e0701
model: qwen3_5 declares its KV layout through a shared prompt_spans
Gaurav-Shah05 Oct 21, 2026
22d6f9b
model: qwen3_omni declares its KV layout
Gaurav-Shah05 Oct 21, 2026
609a51b
model: qwen3_tts declares its KV layout
Gaurav-Shah05 Oct 21, 2026
26b3605
model: vjepa2 declares its KV layout
Gaurav-Shah05 Oct 21, 2026
d7dac61
model: whisper declares its KV layout
Gaurav-Shah05 Oct 21, 2026
7fcf067
worker, conductor: refuse a paged label left undeclared
Gaurav-Shah05 Oct 21, 2026
4da8d72
test: footprints, span checks, loop caps and model layouts
Gaurav-Shah05 Oct 21, 2026
0a5234b
test: refusals of an undeclared paged label
Gaurav-Shah05 Oct 21, 2026
69f8872
docs: paged KV layouts, and prefix keys as a mark on a label
Gaurav-Shah05 Oct 21, 2026
9e5aeac
comment pass
Gaurav-Shah05 Oct 22, 2026
7fb6e2a
kv: count a window's scratch and its commit once
Gaurav-Shah05 Oct 22, 2026
838cba1
test pass
Gaurav-Shah05 Oct 22, 2026
559e44b
kv: a span whose scratch passes the protected prefix counts in the wi…
Gaurav-Shah05 Oct 23, 2026
e40cf16
Merge kv-footprints
Gaurav-Shah05 Oct 23, 2026
bda888f
kv: a kv config refuses a request whose positions pass the context
Gaurav-Shah05 Oct 23, 2026
012cf9d
conductor: a request with no length asked decodes to the context
Gaurav-Shah05 Oct 23, 2026
75a118b
model: cosmos3's reasoner decode loop runs to the reasoner's context
Gaurav-Shah05 Oct 23, 2026
f99f965
model: orpheus and fish leave the context check to the conductor
Gaurav-Shah05 Oct 23, 2026
6820843
test: the context check counts advance, and cuts an unasked length
Gaurav-Shah05 Oct 23, 2026
5c12323
model: fish, qwen3-tts and chatterbox stop at a cut length
Gaurav-Shah05 Oct 23, 2026
977390c
Merge kv-footprints
Gaurav-Shah05 Oct 23, 2026
450641f
Merge main
claude Oct 23, 2026
da65451
Merge kv-footprints
Gaurav-Shah05 Oct 23, 2026
b71865f
Merge main
Gaurav-Shah05 Oct 23, 2026
24a62d6
kv: count the pages eviction can reach as owners change
Gaurav-Shah05 Oct 22, 2026
de9015a
engine: tell a resource which of its spec's nodes this worker runs
Gaurav-Shah05 Oct 22, 2026
677db4f
kv: under CFG parallel the leader's pool admits for the guidance pools
Gaurav-Shah05 Oct 22, 2026
88edfda
worker: continue a request onto another node only once it would run
Gaurav-Shah05 Oct 22, 2026
81ea797
kv: a request reserves its prompt at the head of its pool's queue
Gaurav-Shah05 Oct 22, 2026
7a91c2e
kv: an offload gives up the room its pages used
Gaurav-Shah05 Oct 22, 2026
3dceec8
kv: a grant past a reservation takes only room
Gaurav-Shah05 Oct 22, 2026
69b2643
kv: a kv config refuses and cuts by pages as well as positions
Gaurav-Shah05 Oct 23, 2026
27b01e1
kv: check reservations against the pool under debug asserts
Gaurav-Shah05 Oct 22, 2026
1cea937
docs: admission from footprints
Gaurav-Shah05 Oct 22, 2026
e8eb968
test: the evictable set, the queue, the fit check and the cache paths
Gaurav-Shah05 Oct 22, 2026
1d68f1f
test: reservations stay within the pool through every page move
Gaurav-Shah05 Oct 22, 2026
d772645
test: speculation readiness, offload room and the 400
Gaurav-Shah05 Oct 22, 2026
5a36868
Merge fix/output-limit
Gaurav-Shah05 Oct 24, 2026
9e152e4
kv: the evictable check takes evict's list
Gaurav-Shah05 Oct 23, 2026
5179727
kv: grants evict through _evict
Gaurav-Shah05 Oct 23, 2026
d8750e7
kv: pin a page indexed while others hold it
Gaurav-Shah05 Oct 23, 2026
a36f935
kv: copies from the host take pages under the allotment
Gaurav-Shah05 Oct 23, 2026
c90ca52
kv: reload takes room for the index pages it keeps
Gaurav-Shah05 Oct 23, 2026
1668a33
kv: one root hash and one unstamped lookup
Gaurav-Shah05 Oct 23, 2026
5f5fe17
kv: a copy from the host reuses the join's hashes
Gaurav-Shah05 Oct 23, 2026
b587ec1
kv: every insert drops a parent only the index holds
Gaurav-Shah05 Oct 23, 2026
c220e13
kv: only the device index keeps an evictable set
Gaurav-Shah05 Oct 23, 2026
1748f86
test: host copies, reloads and evicting grants under admission
Gaurav-Shah05 Oct 23, 2026
16fe746
Merge main
Gaurav-Shah05 Oct 24, 2026
fa514e3
kv: above one rank, the index files pages as on main
Gaurav-Shah05 Oct 24, 2026
7a95e25
test: TP ranks evict alike when a lease straddles a commit
Gaurav-Shah05 Oct 24, 2026
3aae934
Merge main
Gaurav-Shah05 Oct 24, 2026
9873896
kv: a copy from the host evicts as a grant does
Gaurav-Shah05 Oct 24, 2026
7567df2
kv: the join and the copy from the host hash through _root_chain
Gaurav-Shah05 Oct 24, 2026
97670ce
test: copies from the host in a pool full of cache
Gaurav-Shah05 Oct 24, 2026
d805a51
docs: admission and preemption design
Gaurav-Shah05 Oct 24, 2026
35f582d
docs: KVSpec.leader and Bagel's example of it
Gaurav-Shah05 Oct 25, 2026
be80595
Remove orphan flag from admission_and_preemption.md
Gaurav-Shah05 Oct 25, 2026
9e7ab34
engine, worker: admit in the runner, and park requests waiting for ca…
Gaurav-Shah05 Oct 28, 2026
7594567
recurrent: reserve a slot at admission
Gaurav-Shah05 Oct 28, 2026
cf5a6d1
docs: admission decided in the runner, the recurrent slot included
Gaurav-Shah05 Oct 28, 2026
61c04e0
test: admission in the runner, recurrent slots and parked requests
Gaurav-Shah05 Oct 28, 2026
bad5937
Merge main
Gaurav-Shah05 Oct 28, 2026
bf30052
runner: try a queue's new first request
Gaurav-Shah05 Oct 28, 2026
251ff94
runner: a request a resource refuses leaves every queue
Gaurav-Shah05 Oct 28, 2026
83bbf73
runner: reserve with what the request published for each node
Gaurav-Shah05 Oct 28, 2026
2153f1c
kv: measure a waiting request against counts that only rise
Gaurav-Shah05 Oct 28, 2026
ce0c6e3
kv: check a waiting request's growth under the lock when asserting
Gaurav-Shah05 Oct 28, 2026
8b9a1b3
test: new first requests, published info, refused requests and offset…
Gaurav-Shah05 Oct 28, 2026
cc06424
recurrent: reserve a slot for each label a request opens
Gaurav-Shah05 Oct 28, 2026
e5e9927
docs: a recurrent pool declares the labels a request opens
Gaurav-Shah05 Oct 28, 2026
2848c9c
test: a forking request is admitted with a slot per label, and an und…
Gaurav-Shah05 Oct 28, 2026
62263a7
runner: move admission state into its own class
Gaurav-Shah05 Oct 28, 2026
b51780d
Merge main
Gaurav-Shah05 Oct 29, 2026
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
16 changes: 12 additions & 4 deletions docs/adding_models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,10 @@ A layout has five fields:
The footprint adds the spans, the growth at each loop's iterations and the scratch, and
rounds up to whole pages per label. A span's own scratch counts only with the spans before
it, so a window's denoise steps and its commit count once. Under a ``RetentionPolicy`` it
takes the window's peak instead.
takes the window's peak instead. ``KVReqConfig.validate`` refuses a request whose footprint
is bigger than its pool with a 400 at intake, and ``KVReqConfig.loop_room`` cuts the default
length of a request that asks for none until it fits. With every loop at 0 iterations, the
same sum is the prompt a request reserves when its pool admits it.

``prompt_spans(plan, texts, text_walk, item_spans)`` builds the common case: a prompt's
text and attachments in ``prefill_plan`` order. Qwen3.5 lays out its one label with it:
Expand Down Expand Up @@ -524,6 +527,9 @@ The spec types are:
``kv_lora_rank`` and ``qk_rope_head_dim``, and sets ``num_kv_heads`` to 1 and
``head_dim`` to their sum. Every rank holds the whole latent; only
``num_qo_heads`` shards, and MLA attention needs it set to the model's query heads.
A spec whose nodes can run on several workers, as Bagel's LLM nodes do under CFG
parallel, sets ``leader`` to the node whose pool admits each request. Left unset, each
worker's pool admits requests on its own.
* - ``KVSpec(config=RingKVConfig(...))``
- A fixed-capacity frame ring. It adds ``tokens_per_frame``, one
``RingKVLayerConfig`` per layer, and the deployment-tunable ``num_sessions``.
Expand Down Expand Up @@ -632,8 +638,10 @@ The spec types are:
* - ``RecurrentStateSpec(config=RecurrentStateConfig(...))``
- Fixed-size per-request state that carries between steps (an LSTM's ``h`` and
``c``, a cursor, a conv tail, a Mamba or gated-DeltaNet layer's conv window and SSM
state, a streaming encoder's caches): one slot per request and label, ``blocks``
naming a shape and dtype for each piece (dtype None: the engine's compute dtype,
state, a streaming encoder's caches): one slot per request and label, ``labels``
naming every label a request's steps address or fork into (``("main",)`` by default)
so that admission reserves a slot for each, ``blocks`` naming a shape and dtype for
each piece (dtype None: the engine's compute dtype,
what its KV caches store), ``num_layers`` copies of each, ``max_slots`` slots
(deployment-tunable) with slot 0 the sink that padding rows address. A recurrent
kernel reads the step's ``addressing``. ``declare_step`` gives each row a
Expand Down Expand Up @@ -2025,7 +2033,7 @@ that its block loop can be CUDA-graph captured:
return [
RaggedAttentionSpec(resource_key=VIT_ATTN, nodes={"vit_encoder"},
config=RaggedAttentionConfig(...)),
KVSpec(resource_key="kv", nodes=nodes, config=self._kv_config()),
KVSpec(resource_key="kv", nodes=nodes, config=self._kv_config(), leader="LLM"),
AttentionSpec(resource_key="attn", nodes=nodes,
config=AttentionConfig(kv_cache="kv")),
PositionSpec(resource_key="rope", nodes=nodes,
Expand Down
355 changes: 355 additions & 0 deletions docs/design/admission_and_preemption.md

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions docs/environment_variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,12 @@ Worker scheduling
worker main loop (speculate, await_gpu, submit_spec, ...).
* - ``MSTAR_KV_DEBUG_ASSERTS``
- ``0``
- ``1``: after every ``admit``, ``commit``, ``reset_request`` and
``remove_request``, check the KV page bookkeeping against the streams
holding the pages, and each commit against its request's layout. A recurrent
snapshot's key is checked against the KV chain's key at its boundary. Logs a
label left under its footprint at removal. Tests and debugging only.
- ``1``: after every ``admit``, ``commit``, ``reset_request``, ``remove_request``
and change to an admitted request's pages, check the KV page bookkeeping against
the streams holding the pages, the reservations against the free and evictable
pages, and each commit against its request's layout. A recurrent snapshot's key is
checked against the KV chain's key at its boundary. Logs a label left under its
footprint at removal. Tests and debugging only.

Compilation
-----------
Expand Down
11 changes: 8 additions & 3 deletions docs/serving.rst
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,14 @@ ranks adopt it. ``prefix_cache_tp`` can't be combined with ``cpu_offload_pages``
cache's nodes must run on one TP group, with no replicas and no walks placed on other
ranks. Loading fails otherwise.

A cached prompt no longer reserves its pages, so the pool no longer limits how many
requests are admitted. It has to hold the decode of every request allowed to run at once,
and a decode step that finds no free page holds its requests until they time out.
On a single rank, a request waits at the head of its pool's queue until its prompt fits
beside the prompts already admitted, and no request behind it starts first. A cache hit on
a page only the prefix index holds counts as a page the request takes. A request that
can't fit in its pool at its full output length gets a 400 if it asked for that length,
and otherwise has the model's default length cut until it fits. The pool
reserves only prompts, so it still has to hold the decode of every request allowed to run
at once, and a decode step that finds no page outside the admitted prompts holds its
requests until they time out.
``max_concurrent_requests`` prevents that. It caps how many requests run at once, and the
pool's pages divided by the pages one request needs, prompt and output together, is a
safe value. ``cpu_offload_pages`` lets a full pool move a request to the host. An offload
Expand Down
40 changes: 25 additions & 15 deletions mstar/conductor/conductor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import time
from collections import defaultdict, deque
from collections.abc import Collection
from dataclasses import dataclass, field
from dataclasses import dataclass, field, replace
from typing import NamedTuple

import numpy as np
Expand Down Expand Up @@ -652,32 +652,42 @@ def _loop_caps(self, model_kwargs: dict) -> dict[str, int]:
)
return {name: caps.get(name, n) for name, n in self._loop_max_iters.items()}

def _resource_specs(self) -> dict:
"""The model's resource specs by key, after this deployment's YAML, for admission-time checks."""
def _resource_specs(self) -> list[dict]:
"""Each worker's resource specs by key, after this deployment's YAML, for admission-time checks.
A spec's ``nodes`` is cut to the worker's own, as the worker's engine does when it builds the resource."""
specs = getattr(self, "_resource_specs_cache", None)
if specs is None:
built = self.model.get_node_resources() or []
apply_yaml_overrides(built, self.model_config)
specs = {s.resource_key: s for s in built}
self._resource_specs_cache = specs
built = self._node_resources()
specs = self._resource_specs_cache = [
{s.resource_key: replace(s, nodes=s.nodes & nodes) for s in built if s.nodes & nodes}
for nodes in {frozenset(self._worker_nodes(worker_id)) for worker_id in self._per_worker_graphs}
]
return specs

def _node_resources(self) -> list:
"""The model's resource specs after this deployment's YAML, each with all of its nodes."""
built = getattr(self, "_node_resources_cache", None)
if built is None:
built = self._node_resources_cache = self.model.get_node_resources() or []
apply_yaml_overrides(built, self.model_config)
return built

def _streamed_partitions(self) -> frozenset[str]:
streamed = getattr(self, "_streamed_partitions_cache", None)
if streamed is None:
streamed = streamed_partitions(list(self._resource_specs().values()), self.model)
streamed = streamed_partitions(self._node_resources(), self.model)
self._streamed_partitions_cache = streamed
return streamed

def _fit_loop_caps(self, model_kwargs: dict, configs: dict, cap: int) -> tuple[int, dict[str, int]]:
"""The largest ``max_output_tokens`` up to ``cap``, and its loop caps, that keeps every loop
within what the request's resources hold. A loop that ``max_output_tokens`` doesn't drive is
left as it is, and ``validate`` then refuses the request if it doesn't fit."""
specs = self._resource_specs()
room: dict[str, int] = {}
for key, cfg in configs.items():
for name, n in cfg.loop_room(specs.get(key)).items():
room[name] = min(n, room.get(name, n))
for specs in self._resource_specs():
for key, cfg in configs.items():
for name, n in cfg.loop_room(specs.get(key)).items():
room[name] = min(n, room.get(name, n))

def overrun(caps: dict[str, int]) -> int:
return max((caps[name] - n for name, n in room.items() if name in caps), default=0)
Expand Down Expand Up @@ -1379,9 +1389,9 @@ def _do_ingest_request(
)
for key, cfg in request_data.resource_configs.items():
cfg.apply_conductor_config(seed=seed, resource_key=key, loop_caps=loop_caps)
specs = self._resource_specs()
for key, cfg in request_data.resource_configs.items():
cfg.validate(specs.get(key))
for specs in self._resource_specs():
for key, cfg in request_data.resource_configs.items():
cfg.validate(specs.get(key))

# Send NewRequest to each worker with the appropriate partition's inputs
request_data.dispatched = True
Expand Down
8 changes: 8 additions & 0 deletions mstar/engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ def load_model(
spec.nodes, relevant_nodes
)
),
nodes=frozenset(relevant_nodes),
dependencies={
key: specs_by_key[key] for key in spec.depends_on()
},
Expand Down Expand Up @@ -2341,6 +2342,13 @@ def check_ready(
published=request_info.resource_publish_info,
)

def admit_waiting(
self, published_of: Callable[[str, str], Mapping[str, PublishedInfo] | None],
) -> list[tuple[str, str]]:
"""The (node, rid) pairs admitted since `check_ready` answered them ``queued``; see
`Admission.admit_waiting`."""
return self._runner.admit_waiting(published_of)

def reserve_replay_slot(self, batch: ExecutingBatch) -> SlotLease | None:
"""Lease the slot this batch will replay on, before it is dispatched.

Expand Down
187 changes: 187 additions & 0 deletions mstar/engine/resources/admission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""Admission across the resources of one runner, see `Resource.ask`."""

from __future__ import annotations

import bisect
import itertools
import logging
from collections.abc import Callable, Collection, Mapping
from dataclasses import dataclass, field

from mstar.engine.resources.base import PublishedInfo, Resource
from mstar.engine.resources.step import FULL_ADMIT_QUEUED, FullAdmitOutcome

logger = logging.getLogger(__name__)


@dataclass(slots=True)
class _Waiting:
"""A waiting request: the node and walk that asked each resource it waits for, and every node
whose readiness check waits on it, with what the request had published for that node then."""
asked: dict[str, tuple[str, str]] = field(default_factory=dict)
nodes: set[str] = field(default_factory=set)
published: dict[str, Mapping[str, PublishedInfo] | None] = field(default_factory=dict)


class Admission:
"""The queues of the resources that ask, and the nodes each request was admitted on.

A waiting request is admitted once it is first in every queue it is in and each resource it
asked reserves for it, all or nothing.
"""

def __init__(self, resources: Mapping[str, Resource], ask_order: Collection[str]) -> None:
self._resources = resources
# `_arrival` numbers each request in the order it first asked.
# `_waiting` holds what each waiting request asked.
# `_queues` holds, per resource, the requests waiting for it in arrival order.
# `_short` maps each resource whose first waiting request didn't fit to the mark it didn't fit
# against, until that request leaves the front of the queue.
# `_untried` is set when a queue has a new first request that `admit_waiting` hasn't tried.
# `_admitted` holds the nodes each request was admitted on.
# `_started` holds, for a request admitted during one node's check, the other nodes waiting on
# it, which `admit_waiting` reports so their parked checks resume.
self._arrivals = itertools.count()
self._arrival: dict[str, int] = {}
self._waiting: dict[str, _Waiting] = {}
self._queues: dict[str, list[str]] = {key: [] for key in ask_order}
self._short: dict[str, object] = {}
self._untried = False
self._admitted: dict[str, set[str]] = {}
self._started: dict[str, set[str]] = {}

def admitted(self, rid: str, node_name: str) -> bool:
return node_name in self._admitted.get(rid, ())

def admit(
self, rid: str, node_name: str, graph_walk: str, published: Mapping[str, PublishedInfo] | None,
keys: Collection[str],
) -> FullAdmitOutcome | None:
"""None once ``rid`` is admitted on ``node_name``, else why it waits or fails. ``keys`` are the
resources of ``node_name`` that ask."""
entry = self._waiting.get(rid)
waits = False
for key in keys:
if entry is not None and key in entry.asked:
waits = True
continue
asked = self._resources[key].ask(rid, node_name, graph_walk)
if asked is None:
continue
if not asked.ok:
logger.warning("Admission for resource %s failed with error: %s", key, asked.reason.message)
if entry is not None:
# a request a resource can't serve waits in no queue
del self._waiting[rid]
self._leave(rid, entry.asked)
return FullAdmitOutcome(asked, key)
entry = self._park_request(rid, key, node_name, graph_walk)
waits = True
if not waits:
self._admitted.setdefault(rid, set()).add(node_name)
return None
entry.nodes.add(node_name)
entry.published[node_name] = published
tried = all(self._queues[key][0] == rid and key not in self._short for key in entry.asked)
if tried and (nodes := self._reserve(rid, entry.published.get)):
if nodes - {node_name}:
self._started.setdefault(rid, set()).update(nodes - {node_name})
return None
return FULL_ADMIT_QUEUED

def admit_waiting(
self, published_of: Callable[[str, str], Mapping[str, PublishedInfo] | None],
) -> list[tuple[str, str]]:
"""Admit each waiting request that is now first in every queue it is in and fits, and return
the (node, rid) pairs that may start. ``published_of(rid, node)`` gives what the request
published for that node's resources.

Nothing is tried until a resource whose first waiting request didn't fit has grown, or a
queue has a new first request, so a pass that changed neither costs one
`Resource.grew_since` for each such resource.
"""
started = []
if self._started:
started = [(node, rid) for rid, nodes in self._started.items() for node in nodes]
self._started.clear()
if not self._waiting:
return started
grown = [key for key, mark in self._short.items() if self._resources[key].grew_since(mark)]
if not grown and not self._untried:
return started
for key in grown:
del self._short[key]
admitted = True
while admitted:
admitted = False
for queue in self._queues.values():
if not queue:
continue
rid = queue[0]
entry = self._waiting[rid]
if any(self._queues[key][0] != rid or key in self._short for key in entry.asked):
continue
nodes = self._reserve(rid, lambda node, rid=rid: published_of(rid, node))
if nodes:
started.extend((node, rid) for node in nodes)
admitted = True
# the last sweep admitted nothing, so it tried every queue's first request
self._untried = False
return started

def remove_request(self, rid: str) -> None:
entry = self._waiting.pop(rid, None)
if entry is not None:
self._leave(rid, entry.asked)
self._admitted.pop(rid, None)
self._started.pop(rid, None)
self._arrival.pop(rid, None)

def _park_request(self, rid: str, key: str, node_name: str, graph_walk: str) -> _Waiting:
"""Put ``rid`` in ``key``'s queue at its arrival, and return what it waits for."""
entry = self._waiting.get(rid)
if entry is None:
entry = self._waiting[rid] = _Waiting()
self._arrival.setdefault(rid, next(self._arrivals))
entry.asked[key] = (node_name, graph_walk)
queue = self._queues[key]
bisect.insort(queue, rid, key=self._arrival.__getitem__)
if queue[0] == rid:
# ahead of the request the mark measured, so `admit` tries it unless another queue holds it back
self._short.pop(key, None)
return entry

def _reserve(
self, rid: str, published_of: Callable[[str], Mapping[str, PublishedInfo] | None],
) -> set[str] | None:
"""Take everything waiting ``rid`` asked for and return the nodes waiting on it, or take
nothing and return None. ``published_of`` gives what ``rid`` published for a node's
resources."""
entry = self._waiting[rid]
taken: list[str] = []
for key, (node_name, graph_walk) in entry.asked.items():
published = published_of(node_name)
mark = self._resources[key].reserve(
rid, node_name, graph_walk, None if published is None else published.get(key),
)
if mark is not None:
for done in taken:
self._resources[done].unreserve(rid)
self._short[key] = mark
return None
taken.append(key)
del self._waiting[rid]
self._leave(rid, entry.asked)
self._admitted.setdefault(rid, set()).update(entry.nodes)
return entry.nodes

def _leave(self, rid: str, keys: Collection[str]) -> None:
"""Take ``rid`` out of the queues of ``keys``. A queue it was first in drops its mark,
which measured ``rid``, and its next request is left for `admit_waiting` to try."""
for key in keys:
queue = self._queues[key]
if queue[0] == rid:
self._short.pop(key, None)
if len(queue) > 1:
self._untried = True
queue.remove(rid)
Loading
Loading