|
31 | 31 | DEFAULT_BIND_ADDRESS, |
32 | 32 | GrpcServerConfig, |
33 | 33 | RuntimeServiceServicer, |
| 34 | + _ThreadedEventStream, |
34 | 35 | create_grpc_server, |
35 | 36 | ) |
36 | 37 | from inference_engine.server.proto_gen.kakeya.v1 import ( |
@@ -883,6 +884,232 @@ def generate(self, session_id, *, max_tokens, **kw): |
883 | 884 | await server.stop(grace=0.1) |
884 | 885 |
|
885 | 886 |
|
| 887 | +async def test_threaded_event_stream_stops_putting_after_cancel(): |
| 888 | + cancel_event = threading.Event() |
| 889 | + stream = _ThreadedEventStream((), cancel_event) |
| 890 | + stream._queue.put(("occupied", None)) |
| 891 | + cancel_event.set() |
| 892 | + stream._put(("ignored", None)) |
| 893 | + assert stream._queue.get_nowait() == ("occupied", None) |
| 894 | + assert stream._queue.empty() |
| 895 | + |
| 896 | + |
| 897 | +async def test_threaded_event_stream_breaks_after_cancelled_event(): |
| 898 | + cancel_event = threading.Event() |
| 899 | + |
| 900 | + class CancelOnFirst: |
| 901 | + def __iter__(self): |
| 902 | + return self |
| 903 | + |
| 904 | + def __next__(self): |
| 905 | + cancel_event.set() |
| 906 | + return "first" |
| 907 | + |
| 908 | + stream = _ThreadedEventStream(CancelOnFirst(), cancel_event) |
| 909 | + stream._run() |
| 910 | + assert stream._queue.get_nowait() == ("first", None) |
| 911 | + |
| 912 | + |
| 913 | +class _DirectAbort(Exception): |
| 914 | + pass |
| 915 | + |
| 916 | + |
| 917 | +class _DirectContext: |
| 918 | + def __init__(self): |
| 919 | + self.code = None |
| 920 | + self.detail = "" |
| 921 | + self.callback = None |
| 922 | + |
| 923 | + async def abort(self, code, detail): |
| 924 | + self.code = code |
| 925 | + self.detail = detail |
| 926 | + raise _DirectAbort |
| 927 | + |
| 928 | + def cancelled(self): |
| 929 | + return False |
| 930 | + |
| 931 | + def add_done_callback(self, callback): |
| 932 | + self.callback = callback |
| 933 | + |
| 934 | + |
| 935 | +async def test_watch_context_removes_cancelled_session(): |
| 936 | + store = SessionStore(capacity=1) |
| 937 | + session = store.create_session() |
| 938 | + servicer = RuntimeServiceServicer(store) |
| 939 | + context = _DirectContext() |
| 940 | + cancel_event = threading.Event() |
| 941 | + servicer._watch_context(context, cancel_event, session.session_id) |
| 942 | + assert context.callback is not None |
| 943 | + context.callback(type("Done", (), {"cancelled": lambda self: True})()) |
| 944 | + assert cancel_event.is_set() |
| 945 | + assert store.active_count == 0 |
| 946 | + |
| 947 | + |
| 948 | +async def test_create_session_rejects_memory_drain(): |
| 949 | + store = SessionStore(capacity=1) |
| 950 | + governor = type("Governor", (), {"draining": True})() |
| 951 | + servicer = RuntimeServiceServicer(store, memory_governor=governor) |
| 952 | + context = _DirectContext() |
| 953 | + with pytest.raises(_DirectAbort): |
| 954 | + await servicer.CreateSession(runtime_pb2.CreateSessionRequest(), context) |
| 955 | + assert context.code == grpc.StatusCode.RESOURCE_EXHAUSTED |
| 956 | + assert store.active_count == 0 |
| 957 | + |
| 958 | + |
| 959 | +async def test_append_rejects_concurrent_operation(): |
| 960 | + store = SessionStore(capacity=1) |
| 961 | + session = store.create_session() |
| 962 | + coordinator = type( |
| 963 | + "Coordinator", |
| 964 | + (), |
| 965 | + {"append_tokens": lambda self, session_id, token_ids: 0}, |
| 966 | + )() |
| 967 | + servicer = RuntimeServiceServicer(store, append_coordinator=coordinator) |
| 968 | + assert servicer._acquire_operation(session.session_id) |
| 969 | + context = _DirectContext() |
| 970 | + with pytest.raises(_DirectAbort): |
| 971 | + await servicer.AppendTokens( |
| 972 | + runtime_pb2.AppendTokensRequest( |
| 973 | + session_id=session.session_id, |
| 974 | + token_ids=[1], |
| 975 | + ), |
| 976 | + context, |
| 977 | + ) |
| 978 | + assert context.code == grpc.StatusCode.ABORTED |
| 979 | + servicer._release_operation(session.session_id) |
| 980 | + |
| 981 | + |
| 982 | +async def test_append_passes_cancel_event_and_resets_liveness(): |
| 983 | + observed = {} |
| 984 | + |
| 985 | + class Coordinator: |
| 986 | + def append_tokens(self, session_id, token_ids, cancel_event): |
| 987 | + observed["session_id"] = session_id |
| 988 | + observed["tokens"] = token_ids |
| 989 | + observed["cancel_event"] = cancel_event |
| 990 | + return len(token_ids) |
| 991 | + |
| 992 | + class Liveness: |
| 993 | + def update(self, phase): |
| 994 | + observed["phase"] = phase |
| 995 | + |
| 996 | + store = SessionStore(capacity=1) |
| 997 | + session = store.create_session() |
| 998 | + servicer = RuntimeServiceServicer( |
| 999 | + store, |
| 1000 | + append_coordinator=Coordinator(), |
| 1001 | + liveness=Liveness(), |
| 1002 | + ) |
| 1003 | + response = await servicer.AppendTokens( |
| 1004 | + runtime_pb2.AppendTokensRequest( |
| 1005 | + session_id=session.session_id, |
| 1006 | + token_ids=[4, 5], |
| 1007 | + ), |
| 1008 | + _DirectContext(), |
| 1009 | + ) |
| 1010 | + assert response.history_length == 2 |
| 1011 | + assert observed["session_id"] == session.session_id |
| 1012 | + assert observed["tokens"] == [4, 5] |
| 1013 | + assert isinstance(observed["cancel_event"], threading.Event) |
| 1014 | + assert observed["phase"] == "idle" |
| 1015 | + |
| 1016 | + |
| 1017 | +async def test_append_asyncio_cancellation_removes_session(): |
| 1018 | + class Coordinator: |
| 1019 | + def append_tokens(self, session_id, token_ids): |
| 1020 | + raise asyncio.CancelledError |
| 1021 | + |
| 1022 | + store = SessionStore(capacity=1) |
| 1023 | + session = store.create_session() |
| 1024 | + servicer = RuntimeServiceServicer(store, append_coordinator=Coordinator()) |
| 1025 | + with pytest.raises(asyncio.CancelledError): |
| 1026 | + await servicer.AppendTokens( |
| 1027 | + runtime_pb2.AppendTokensRequest( |
| 1028 | + session_id=session.session_id, |
| 1029 | + token_ids=[1], |
| 1030 | + ), |
| 1031 | + _DirectContext(), |
| 1032 | + ) |
| 1033 | + assert store.active_count == 0 |
| 1034 | + |
| 1035 | + |
| 1036 | +async def test_append_worker_cancellation_maps_cancelled(): |
| 1037 | + from inference_engine.session import OperationCancelledError |
| 1038 | + |
| 1039 | + class Coordinator: |
| 1040 | + def append_tokens(self, session_id, token_ids): |
| 1041 | + raise OperationCancelledError("worker cancelled") |
| 1042 | + |
| 1043 | + store = SessionStore(capacity=1) |
| 1044 | + session = store.create_session() |
| 1045 | + servicer = RuntimeServiceServicer(store, append_coordinator=Coordinator()) |
| 1046 | + context = _DirectContext() |
| 1047 | + with pytest.raises(_DirectAbort): |
| 1048 | + await servicer.AppendTokens( |
| 1049 | + runtime_pb2.AppendTokensRequest( |
| 1050 | + session_id=session.session_id, |
| 1051 | + token_ids=[1], |
| 1052 | + ), |
| 1053 | + context, |
| 1054 | + ) |
| 1055 | + assert context.code == grpc.StatusCode.CANCELLED |
| 1056 | + assert store.active_count == 0 |
| 1057 | + |
| 1058 | + |
| 1059 | +async def test_generate_rejects_concurrent_operation(): |
| 1060 | + class Coordinator: |
| 1061 | + def generate(self, session_id, **kwargs): |
| 1062 | + return iter(()) |
| 1063 | + |
| 1064 | + store = SessionStore(capacity=1) |
| 1065 | + session = store.create_session() |
| 1066 | + servicer = RuntimeServiceServicer( |
| 1067 | + store, |
| 1068 | + generation_coordinator=Coordinator(), |
| 1069 | + ) |
| 1070 | + assert servicer._acquire_operation(session.session_id) |
| 1071 | + context = _DirectContext() |
| 1072 | + with pytest.raises(_DirectAbort): |
| 1073 | + async for _ in servicer.Generate( |
| 1074 | + runtime_pb2.GenerateRequest( |
| 1075 | + session_id=session.session_id, |
| 1076 | + max_tokens=1, |
| 1077 | + ), |
| 1078 | + context, |
| 1079 | + ): |
| 1080 | + pass |
| 1081 | + assert context.code == grpc.StatusCode.ABORTED |
| 1082 | + servicer._release_operation(session.session_id) |
| 1083 | + |
| 1084 | + |
| 1085 | +async def test_generate_busy_error_maps_aborted(): |
| 1086 | + from inference_engine.session import SessionGenerationBusyError |
| 1087 | + |
| 1088 | + class Coordinator: |
| 1089 | + def generate(self, session_id, **kwargs): |
| 1090 | + if False: |
| 1091 | + yield |
| 1092 | + raise SessionGenerationBusyError(session_id) |
| 1093 | + |
| 1094 | + store = SessionStore(capacity=1) |
| 1095 | + session = store.create_session() |
| 1096 | + servicer = RuntimeServiceServicer( |
| 1097 | + store, |
| 1098 | + generation_coordinator=Coordinator(), |
| 1099 | + ) |
| 1100 | + context = _DirectContext() |
| 1101 | + with pytest.raises(_DirectAbort): |
| 1102 | + async for _ in servicer.Generate( |
| 1103 | + runtime_pb2.GenerateRequest( |
| 1104 | + session_id=session.session_id, |
| 1105 | + max_tokens=1, |
| 1106 | + ), |
| 1107 | + context, |
| 1108 | + ): |
| 1109 | + pass |
| 1110 | + assert context.code == grpc.StatusCode.ABORTED |
| 1111 | + |
| 1112 | + |
886 | 1113 | async def test_factory_wires_prefill_cache_service(): |
887 | 1114 | from inference_engine.distributed.capability import CacheCompatibility |
888 | 1115 | from inference_engine.distributed.prefill_cache import PrefixCacheStore |
|
0 commit comments