Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

262 lines
9.3 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Unit tests for the pause/resume controller (CPU-only, no scheduler/GPU)."""
from __future__ import annotations
from tokenspeed.runtime.engine.io_struct import (
IsSchedulerPausedReqInput,
IsSchedulerPausedReqOutput,
PauseSchedulerReqInput,
PauseSchedulerReqOutput,
ResumeSchedulerReqInput,
ResumeSchedulerReqOutput,
)
from tokenspeed.runtime.engine.pause import (
PauseController,
PauseState,
scheduler_drained,
)
class _Sender:
def __init__(self):
self.items = []
def send_pyobj(self, x):
self.items.append(x)
class _FakeScheduler:
"""Implements just the four size accessors the drain check reads."""
def __init__(self, waiting=0, decoding=0, prefilling=0, retract=0):
self._w, self._d, self._p, self._r = waiting, decoding, prefilling, retract
def waiting_size(self):
return self._w
def decoding_size(self):
return self._d
def prefilling_size(self):
return self._p
def retract_count(self):
return self._r
def _controller():
sender = _Sender()
return PauseController(sender), sender
# --- scheduler_drained -------------------------------------------------------
def test_scheduler_drained_predicate():
assert scheduler_drained(_FakeScheduler())
assert not scheduler_drained(_FakeScheduler(waiting=1))
assert not scheduler_drained(_FakeScheduler(decoding=1))
assert not scheduler_drained(_FakeScheduler(prefilling=1))
assert not scheduler_drained(_FakeScheduler(retract=1))
# --- keep mode ---------------------------------------------------------------
def test_keep_mode_freezes_and_replies_immediately():
ctrl, sender = _controller()
ctrl.handle_pause(PauseSchedulerReqInput(mode="keep"))
assert ctrl.state == PauseState.PAUSED_ALL
assert ctrl.is_paused
assert ctrl.admit_blocked
assert ctrl.forward_blocked # keep stops all scheduling
# Reply is sent right away — nothing to drain.
assert len(sender.items) == 1
assert isinstance(sender.items[0], PauseSchedulerReqOutput)
assert sender.items[0].success
# keep freezes in place: no abort-all, no grammar-queue cancel.
assert not ctrl.consume_abort_all()
assert not ctrl.consume_cancel_grammar()
# --- abort mode --------------------------------------------------------------
def test_abort_mode_defers_reply_until_drained():
ctrl, sender = _controller()
ctrl.handle_pause(PauseSchedulerReqInput(mode="abort"))
assert ctrl.state == PauseState.PAUSED_NEW
assert ctrl.admit_blocked
assert not ctrl.forward_blocked # running requests keep stepping to drain
# No reply yet — deferred until the scheduler drains.
assert sender.items == []
# abort requests an all-cancel exactly once.
assert ctrl.consume_abort_all()
assert not ctrl.consume_abort_all()
# abort also cancels grammar-queued (still-compiling) requests, once.
assert ctrl.consume_cancel_grammar()
assert not ctrl.consume_cancel_grammar()
# Still busy → no reply.
ctrl.maybe_finish_drain(_FakeScheduler(decoding=2))
assert sender.items == []
# Drained → reply now fires.
ctrl.maybe_finish_drain(_FakeScheduler())
assert len(sender.items) == 1
assert isinstance(sender.items[0], PauseSchedulerReqOutput)
assert sender.items[0].success
# Idempotent: a second drain check does not double-reply.
ctrl.maybe_finish_drain(_FakeScheduler())
assert len(sender.items) == 1
def test_second_pause_while_draining_is_rejected():
# A second abort/wait pause arriving before the first drains must be
# rejected: otherwise it would overwrite the deferred reply and strand the
# first caller forever on its ZMQ await.
ctrl, sender = _controller()
ctrl.handle_pause(PauseSchedulerReqInput(mode="abort"))
ctrl.consume_abort_all() # first pause armed its abort-all
ctrl.consume_cancel_grammar() # ...and its grammar-queue cancel
assert sender.items == [] # first reply still deferred
# Second pause (any mode) is rejected with a failure, leaving the first
# pause's pending reply intact.
for mode in ("abort", "wait", "keep"):
sender.items.clear()
ctrl.handle_pause(PauseSchedulerReqInput(mode=mode))
assert len(sender.items) == 1
assert isinstance(sender.items[0], PauseSchedulerReqOutput)
assert not sender.items[0].success
# Rejected pause does not re-arm abort-all / grammar cancel.
assert not ctrl.consume_abort_all()
assert not ctrl.consume_cancel_grammar()
# The original pause still resolves normally once the scheduler drains.
sender.items.clear()
ctrl.maybe_finish_drain(_FakeScheduler())
assert len(sender.items) == 1
assert sender.items[0].success
# --- wait mode ---------------------------------------------------------------
def test_wait_mode_defers_reply_without_abort_all():
ctrl, sender = _controller()
ctrl.handle_pause(PauseSchedulerReqInput(mode="wait"))
assert ctrl.state == PauseState.PAUSED_NEW
assert not ctrl.forward_blocked
assert sender.items == []
# wait lets running requests finish naturally — no abort-all — but still
# cancels grammar-queued pre-pause requests (they can't finish while paused).
assert not ctrl.consume_abort_all()
assert ctrl.consume_cancel_grammar()
ctrl.maybe_finish_drain(_FakeScheduler())
assert len(sender.items) == 1
assert sender.items[0].success
# --- invalid mode ------------------------------------------------------------
def test_invalid_mode_replies_failure_and_stays_unpaused():
ctrl, sender = _controller()
ctrl.handle_pause(PauseSchedulerReqInput(mode="bogus"))
assert ctrl.state == PauseState.UNPAUSED
assert len(sender.items) == 1
assert isinstance(sender.items[0], PauseSchedulerReqOutput)
assert not sender.items[0].success
# --- resume ------------------------------------------------------------------
def test_resume_rejects_inflight_pause_then_acks():
# A resume arriving while a wait/abort pause is still awaiting its drain
# reply must resolve that pause (separate communicators — resume can't wake
# the pause caller), then ack the resume. The pause reply must be a FAILURE:
# the scheduler had not drained, so acking success would let a weight-swap
# caller proceed while pre-pause requests are still in flight.
ctrl, sender = _controller()
ctrl.handle_pause(PauseSchedulerReqInput(mode="wait"))
ctrl.handle_resume(ResumeSchedulerReqInput())
assert ctrl.state == PauseState.UNPAUSED
assert not ctrl.is_paused
assert not ctrl.admit_blocked
# Pending pause reply resolved (as failure) first, then the resume ack.
assert len(sender.items) == 2
assert isinstance(sender.items[0], PauseSchedulerReqOutput)
assert not sender.items[0].success
assert (
isinstance(sender.items[1], ResumeSchedulerReqOutput)
and sender.items[1].success
)
# The reply is consumed: a later drain check emits nothing more.
ctrl.maybe_finish_drain(_FakeScheduler())
assert len(sender.items) == 2
def test_resume_without_inflight_pause_only_acks():
ctrl, sender = _controller()
ctrl.handle_resume(ResumeSchedulerReqInput())
assert ctrl.state == PauseState.UNPAUSED
assert len(sender.items) == 1
assert isinstance(sender.items[0], ResumeSchedulerReqOutput)
# --- is_paused ---------------------------------------------------------------
def test_is_paused_reflects_state():
ctrl, sender = _controller()
ctrl.handle_is_paused(IsSchedulerPausedReqInput())
assert isinstance(sender.items[-1], IsSchedulerPausedReqOutput)
assert sender.items[-1].is_paused is False
ctrl.handle_pause(PauseSchedulerReqInput(mode="keep"))
ctrl.handle_is_paused(IsSchedulerPausedReqInput())
assert sender.items[-1].is_paused is True
# --- spec buffering ----------------------------------------------------------
def test_spec_buffering_is_fifo():
ctrl, _ = _controller()
ctrl.buffer_specs(["a", "b"])
ctrl.buffer_specs(["c"])
assert ctrl.buffered_specs == ["a", "b", "c"]
assert ctrl.take_buffered_specs() == ["a", "b", "c"]
# Drained after take.
assert ctrl.buffered_specs == []
assert ctrl.take_buffered_specs() == []