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
233 lines
7.1 KiB
Python
233 lines
7.1 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 OutputProcesser.post_process_forward_op after a flat retract.
|
|
|
|
Flat retract rebases the victim's generated tokens into its prefill window
|
|
(C++ RebasePrefill), so on re-admission the op's ``prefill_lengths`` exceeds
|
|
the original prompt length that RequestState.prefill_finished compares
|
|
against. A mid-chunk re-prefill slot must emit NO ExtendResultEvent (the C++
|
|
FSM is still Prefilling and would throw) and stream NO token (the sampled
|
|
token is garbage). The gate is the op's own chunking criterion:
|
|
``extend_prefix_lens[i] + input_lengths[i] < prefill_lengths[i]``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from tokenspeed.runtime.engine.generation_output_processor import (
|
|
OutputProcesser,
|
|
RequestState,
|
|
)
|
|
from tokenspeed.runtime.sampling.sampling_params import SamplingParams
|
|
|
|
|
|
class _Sender:
|
|
def __init__(self):
|
|
self.items = []
|
|
|
|
def send_pyobj(self, obj):
|
|
self.items.append(obj)
|
|
|
|
|
|
class _Tokenizer:
|
|
eos_token_id = None
|
|
additional_stop_token_ids = None
|
|
|
|
def decode(self, ids):
|
|
return "".join(str(i) for i in ids)
|
|
|
|
|
|
class _Metrics:
|
|
enabled = False
|
|
|
|
def record_nan_abort(self):
|
|
pass
|
|
|
|
|
|
class _Scalar:
|
|
def __init__(self, v):
|
|
self._v = v
|
|
|
|
def item(self):
|
|
return self._v
|
|
|
|
|
|
class _Vec:
|
|
"""Minimal tensor stand-in: indexable scalars plus tolist()."""
|
|
|
|
def __init__(self, vals):
|
|
self._vals = list(vals)
|
|
|
|
def __getitem__(self, i):
|
|
return _Scalar(self._vals[i])
|
|
|
|
def tolist(self):
|
|
return list(self._vals)
|
|
|
|
|
|
class _Results:
|
|
"""ModelExecutionResult stub: one sampled token per slot."""
|
|
|
|
def __init__(self, output_tokens, output_lengths):
|
|
self.output_tokens = _Vec(output_tokens)
|
|
self.output_lengths = _Vec(output_lengths)
|
|
self.output_logprobs = None
|
|
self.output_nan_flags = None
|
|
self.grammar_completion = None
|
|
self.next_input_ids = None
|
|
|
|
def sync(self):
|
|
pass
|
|
|
|
|
|
class _ForwardOp:
|
|
"""Forward-op stub exposing the flat binding's per-slot prefill_lengths."""
|
|
|
|
def __init__(
|
|
self,
|
|
request_ids,
|
|
extend_prefix_lens,
|
|
input_lengths,
|
|
num_extends,
|
|
prefill_lengths=None,
|
|
):
|
|
self.request_ids = request_ids
|
|
self.extend_prefix_lens = extend_prefix_lens
|
|
self.input_lengths = input_lengths
|
|
self.request_pool_indices = list(range(len(request_ids)))
|
|
self._num_extends = num_extends
|
|
if prefill_lengths is not None:
|
|
self.prefill_lengths = prefill_lengths
|
|
|
|
def num_extends(self):
|
|
return self._num_extends
|
|
|
|
|
|
def _state(input_ids: list[int]) -> RequestState:
|
|
return RequestState(
|
|
prompt_input_ids=input_ids,
|
|
sampling_params=SamplingParams(max_new_tokens=8, stop=[], ignore_eos=True),
|
|
stream=False,
|
|
tokenizer=_Tokenizer(),
|
|
)
|
|
|
|
|
|
def _proc(sender: _Sender) -> OutputProcesser:
|
|
return OutputProcesser(sender, attn_tp_rank=0, metrics=_Metrics())
|
|
|
|
|
|
def _kinds(events) -> list[str]:
|
|
return [type(e).__name__ for e in events]
|
|
|
|
|
|
def test_mid_chunk_readmit_slot_emits_nothing():
|
|
# Retract-rebase shape: prompt is 3 tokens but the op's prefill length is 9
|
|
# (prompt + 6 generated tokens rebased into the prefill window). First
|
|
# re-admission chunk covers 4+4=8 < 9 — mid-chunk, C++ owes no result.
|
|
sender = _Sender()
|
|
proc = _proc(sender)
|
|
state = _state([1, 2, 3])
|
|
proc.rid_to_state["victim"] = state
|
|
|
|
op = _ForwardOp(
|
|
request_ids=["victim"],
|
|
extend_prefix_lens=[4],
|
|
input_lengths=[4],
|
|
num_extends=1,
|
|
prefill_lengths=[9],
|
|
)
|
|
changes = proc.post_process_forward_op(op, _Results([777], [1]))
|
|
|
|
# The old prompt-length gate saw computed(8) >= prompt(3) and emitted an
|
|
# ExtendResultEvent (C++ Prefilling FSM throws) plus one garbage token.
|
|
assert "ExtendResult" not in _kinds(changes)
|
|
assert changes == []
|
|
assert state.output_ids == []
|
|
assert sender.items == []
|
|
assert "victim" in proc.rid_to_state
|
|
|
|
|
|
def test_final_chunk_readmit_slot_emits_result():
|
|
# Same rebased request, last chunk: 8+1 == 9 — the op's criterion matches
|
|
# the C++ PrefillDone decision, byte-identical to a fresh request whose
|
|
# final chunk reaches its prompt length.
|
|
sender = _Sender()
|
|
proc = _proc(sender)
|
|
state = _state([1, 2, 3])
|
|
proc.rid_to_state["victim"] = state
|
|
|
|
op = _ForwardOp(
|
|
request_ids=["victim"],
|
|
extend_prefix_lens=[8],
|
|
input_lengths=[1],
|
|
num_extends=1,
|
|
prefill_lengths=[9],
|
|
)
|
|
changes = proc.post_process_forward_op(op, _Results([777], [1]))
|
|
|
|
assert "ExtendResult" in _kinds(changes)
|
|
assert state.output_ids == [777]
|
|
assert len(sender.items) == 1
|
|
|
|
|
|
def test_decode_slot_unaffected_by_prefill_lengths():
|
|
# Decode slots never consult the extend gate; result flows as before.
|
|
sender = _Sender()
|
|
proc = _proc(sender)
|
|
state = _state([1, 2, 3])
|
|
state.computed_length = 3 # prefill already done
|
|
proc.rid_to_state["dec"] = state
|
|
|
|
op = _ForwardOp(
|
|
request_ids=["dec"],
|
|
extend_prefix_lens=[],
|
|
input_lengths=[1],
|
|
num_extends=0,
|
|
prefill_lengths=[],
|
|
)
|
|
changes = proc.post_process_forward_op(op, _Results([555], [1]))
|
|
|
|
kinds = _kinds(changes)
|
|
assert "ExtendResult" in kinds
|
|
assert "UpdateReserveNumTokens" in kinds
|
|
assert state.output_ids == [555]
|
|
|
|
|
|
def test_op_without_prefill_lengths_falls_back_to_prompt_gate():
|
|
# Older/radix binding without the field: the prefill_finished gate still
|
|
# suppresses mid-chunk output for fresh requests.
|
|
sender = _Sender()
|
|
proc = _proc(sender)
|
|
state = _state([1, 2, 3, 4, 5, 6, 7, 8])
|
|
proc.rid_to_state["fresh"] = state
|
|
|
|
op = _ForwardOp(
|
|
request_ids=["fresh"],
|
|
extend_prefix_lens=[0],
|
|
input_lengths=[4],
|
|
num_extends=1,
|
|
prefill_lengths=None,
|
|
)
|
|
changes = proc.post_process_forward_op(op, _Results([888], [1]))
|
|
|
|
assert changes == []
|
|
assert state.output_ids == []
|
|
assert sender.items == []
|