Skip to content

Commit bc52147

Browse files
PR-B3 (ADR 0008 Phase B): Generate server-streaming RPC + greedy session-aware decoding
First Phase-B PR with mandatory Mac M4 integration test report under \u00a79 (Linux CI is necessary but not sufficient). Design choice: PR-B3 ships **greedy decoding only**. Speculative- decoding integration (DLM proposer + AR verifier rejection sampling) is reserved for a later PR. The wire contract is algorithm-agnostic (GenerateResponse oneof on token_id / done / truncated), so the upgrade path lands without breaking clients. Three deliverables: 1. inference_engine/session/generator.py (new, ~240 lines) - VerifierProtocol-driven GenerationCoordinator. - Yields TokenEvent / HistoryTruncatedEvent / DoneEvent. - Strict v0.3 validation: temperature/top_p/top_k must be in greedy no-op defaults; max_tokens must be >= 1; session must have prior AppendTokens. Per \u00a72.10 'no graceful degradation', any deviation raises ValueError -> INVALID_ARGUMENT. - HistoryTruncated emitted at most once per call, BEFORE any TokenEvent, when the cache is in sink+window-truncated state (matches the runtime.proto contract exactly). - INV-1 / INV-2 enforced via SessionStore at every step. - INV-3 byte-exact under greedy: same (session_id, history) -> bit-identical token stream. Tested with FakeVerifier on Linux and asserted in TestDeterminism. 2. inference_engine/server/grpc_app.py (modified, +119 lines) - RuntimeServiceServicer.__init__ takes optional generation_coordinator: GenerationCoordinator. None = PR-B2 UNIMPLEMENTED default preserved (regression-tested). - Generate RPC implements server-streaming with the four typed error mappings: SessionNotFoundError -> NOT_FOUND ValueError -> INVALID_ARGUMENT InvariantViolation -> FAILED_PRECONDITION Plus the success path streams TokenEvent -> token_id, HistoryTruncatedEvent -> truncated, DoneEvent -> done. - Cancellation: polls context.cancelled() between events; on True, emits a final GenerateDone(STOP_REASON_CANCELLED) and returns. Cancellation latency is bounded by one generation step (the in-flight forward pass finishes before the next poll). - create_grpc_server factory plumbed with the new keyword. 3. tests/inference_engine/session/test_generator.py (new, 31 tests) - TestGreedyHappyPath: 4 tests on token-then-done emission, max_tokens cap, single Done event, total_seconds reporting. - TestGreedyAdvancesVerifier: 4 tests confirming verifier state mirrored onto session after every step. - TestEos: 3 tests on EOS detection + STOP_REASON_EOS. - TestHistoryTruncated: 3 tests on at-start emission + at-most- once contract. - TestValidation: 9 tests on max_tokens, sampling params, and no-AppendTokens-prior rejection. - TestInvariants: 2 tests on INV-1 / INV-2 propagation. - TestDeterminism: 1 test on byte-exact greedy output across parallel sessions. - TestConstructorAndEventDataclasses: 4 tests on frozen dataclass contract and constructor. + tests/inference_engine/server/test_grpc_app.py (extended, +11 tests) - Generate streams tokens then done. - EOS triggers STOP_REASON_EOS. - HistoryTruncated frame emitted before tokens. - Unknown session -> NOT_FOUND. - No AppendTokens prior -> INVALID_ARGUMENT. - max_tokens=0 -> INVALID_ARGUMENT. - temperature=0.7 -> INVALID_ARGUMENT. - seed accepted on the wire. - InvariantViolation during generation -> FAILED_PRECONDITION. - Cancellation -> STOP_REASON_CANCELLED Done frame (direct Servicer invocation with FakeContext; cancellation latency bounded as documented). - Factory accepts generation_coordinator keyword. Mac M4 reviewer aids: scripts/smoke_grpc_generator.py 10-scenario smoke walking Generate scenarios with Append + Generate coordinators wired against FakeVerifier. scripts/review_pr_b3_on_mac.sh One-shot Mac M4 reviewer producing 5 JSON artifacts: pr-b3-mac-generator-tests-<unix>.json (31 tests, 100% on generator.py) pr-b3-mac-grpc-tests-<unix>.json (39 tests after PR-B3 additions, 100% on grpc_app.py) pr-b3-mac-grpc-runtime-smoke-<unix>.json (PR-B1 regression) pr-b3-mac-grpc-appender-smoke-<unix>.json (PR-B2 regression) pr-b3-mac-grpc-generator-smoke-<unix>.json (PR-B3 new) Local verification (Linux VM, py3.12): Linux CI gate: 629 passed (was 587 + 31 generator + 11 grpc generate). Coverage 100.00% on 1521 stmts (was 1428 + 63 generator + 30 grpc_app additions - 0 deletions). Per ADR 0008 \u00a79: this PR introduces a NEW MLX-reachable runtime path (verifier.forward_block + commit_or_truncate driven by the coordinator's per-token loop). Even though FakeVerifier covers the Linux-side dispatch logic exhaustively, the v0.3 GA contract requires a Mac M4 integration test report on the PR branch before merge. The carve-out paragraph does NOT apply here. PR is opened as Draft pending the report. Mac M4 verification command for the PR branch: bash scripts/review_pr_b3_on_mac.sh The script writes 5 JSON artifacts under results/platform-tests/; the user commits them back to this branch and the PR description is updated to quote the smoke summary line. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent d343e0c commit bc52147

7 files changed

Lines changed: 1739 additions & 10 deletions

File tree

inference_engine/server/grpc_app.py

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,34 @@
4242
)
4343
from inference_engine.session import (
4444
AppendTokensCoordinator,
45+
DoneEvent,
46+
GenerationCoordinator,
47+
HistoryTruncatedEvent,
4548
InvariantViolation,
4649
SessionNotFoundError,
4750
SessionStore,
51+
STOP_REASON_CANCELLED,
52+
STOP_REASON_EOS,
53+
STOP_REASON_MAX_TOKENS,
54+
STOP_REASON_TRUNCATED,
55+
TokenEvent,
4856
)
4957

58+
59+
# Mapping from GenerationCoordinator's string stop reasons to the
60+
# protobuf enum. Defined at module level so reviewers can audit the
61+
# 1:1 correspondence at a glance.
62+
_STOP_REASON_TO_PROTO = {
63+
STOP_REASON_MAX_TOKENS:
64+
runtime_pb2.GenerateDone.STOP_REASON_MAX_TOKENS,
65+
STOP_REASON_EOS:
66+
runtime_pb2.GenerateDone.STOP_REASON_EOS,
67+
STOP_REASON_CANCELLED:
68+
runtime_pb2.GenerateDone.STOP_REASON_CANCELLED,
69+
STOP_REASON_TRUNCATED:
70+
runtime_pb2.GenerateDone.STOP_REASON_TRUNCATED,
71+
}
72+
5073
_logger = logging.getLogger(__name__)
5174

5275
DEFAULT_BIND_ADDRESS = "127.0.0.1:50051"
@@ -113,18 +136,25 @@ def __init__(
113136
session_store: SessionStore,
114137
*,
115138
append_coordinator: Optional[AppendTokensCoordinator] = None,
139+
generation_coordinator: Optional[GenerationCoordinator] = None,
116140
) -> None:
117141
"""Construct a Servicer.
118142
119-
``append_coordinator`` is the wiring point added in PR-B2. When
120-
``None`` (the PR-B1 mode, preserved for tests that don't need a
121-
verifier), ``AppendTokens`` returns ``UNIMPLEMENTED`` — the same
122-
framework default used in PR-B1. When non-None, ``AppendTokens``
123-
runs the §2.3 byte-exact prefill-incremental contract through
124-
the coordinator and surfaces the typed error mapping above.
143+
``append_coordinator`` is the PR-B2 wiring point: when None
144+
(PR-B1 mode, preserved for tests that don't need a verifier),
145+
``AppendTokens`` returns ``UNIMPLEMENTED``; when non-None,
146+
``AppendTokens`` runs the §2.3 byte-exact prefill-incremental
147+
contract.
148+
149+
``generation_coordinator`` is the PR-B3 wiring point: same
150+
optional-default contract for ``Generate``. When None, the
151+
Generate stream returns ``UNIMPLEMENTED``; when non-None,
152+
Generate streams TokenEvents / HistoryTruncatedEvents /
153+
DoneEvent through the gRPC server-streaming response.
125154
"""
126155
self._store = session_store
127156
self._append = append_coordinator
157+
self._generate = generation_coordinator
128158

129159
async def CreateSession( # noqa: N802 — gRPC-generated method casing
130160
self,
@@ -184,6 +214,111 @@ async def AppendTokens( # noqa: N802 — gRPC-generated method casing
184214
history_length=new_history_length,
185215
)
186216

217+
async def Generate( # noqa: N802 — gRPC-generated method casing
218+
self,
219+
request: runtime_pb2.GenerateRequest,
220+
context: grpc.aio.ServicerContext,
221+
):
222+
"""Stream tokens generated against ``request.session_id``.
223+
224+
Yields ``runtime_pb2.GenerateResponse`` frames carrying one of:
225+
226+
* ``token_id``: a committed token, in generation order.
227+
* ``truncated``: ``HistoryTruncated`` event, emitted at most
228+
once per call before the first ``token_id`` (per the proto
229+
contract).
230+
* ``done``: ``GenerateDone`` terminal frame.
231+
232+
When this Servicer was constructed without a
233+
``generation_coordinator``, returns ``UNIMPLEMENTED`` (PR-B2
234+
regression contract preserved).
235+
236+
Cancellation: the loop polls ``context.cancelled()`` after
237+
every event the coordinator yields. On cancellation we emit
238+
a ``GenerateDone(STOP_REASON_CANCELLED)`` frame and return.
239+
Cancellation latency is bounded by one generation step on
240+
the worst case (the in-flight forward pass finishes before
241+
the next poll).
242+
"""
243+
if self._generate is None:
244+
await context.abort(
245+
grpc.StatusCode.UNIMPLEMENTED,
246+
"Generate not configured on this Servicer "
247+
"(coordinator not provided)",
248+
)
249+
250+
seed = request.seed if request.HasField("seed") else None
251+
temperature = (
252+
request.temperature
253+
if request.HasField("temperature") else None
254+
)
255+
top_p = request.top_p if request.HasField("top_p") else None
256+
top_k = request.top_k if request.HasField("top_k") else None
257+
258+
# GenerationCoordinator.generate is a generator function; the
259+
# call itself returns a generator object without executing
260+
# any of the body, so no exception is raised here. All typed
261+
# errors (SessionNotFoundError, ValueError, InvariantViolation)
262+
# propagate from the inner `for event in event_stream:` loop
263+
# below and are caught there.
264+
event_stream = self._generate.generate(
265+
session_id=request.session_id,
266+
max_tokens=request.max_tokens,
267+
seed=seed,
268+
temperature=temperature,
269+
top_p=top_p,
270+
top_k=top_k,
271+
)
272+
273+
token_count_so_far = 0
274+
275+
try:
276+
for event in event_stream:
277+
if context.cancelled():
278+
yield runtime_pb2.GenerateResponse(
279+
done=runtime_pb2.GenerateDone(
280+
stop_reason=_STOP_REASON_TO_PROTO[
281+
STOP_REASON_CANCELLED
282+
],
283+
generated_token_count=token_count_so_far,
284+
prefill_duration_seconds=0.0,
285+
total_duration_seconds=0.0,
286+
),
287+
)
288+
return
289+
290+
if isinstance(event, TokenEvent):
291+
token_count_so_far += 1
292+
yield runtime_pb2.GenerateResponse(
293+
token_id=event.token_id,
294+
)
295+
elif isinstance(event, HistoryTruncatedEvent):
296+
yield runtime_pb2.GenerateResponse(
297+
truncated=runtime_pb2.HistoryTruncated(
298+
dropped_token_count=event.dropped_token_count,
299+
),
300+
)
301+
else:
302+
# DoneEvent — the only remaining event type per
303+
# the GenerateEvent union.
304+
assert isinstance(event, DoneEvent)
305+
yield runtime_pb2.GenerateResponse(
306+
done=runtime_pb2.GenerateDone(
307+
stop_reason=_STOP_REASON_TO_PROTO[
308+
event.stop_reason
309+
],
310+
generated_token_count=event.generated_token_count,
311+
prefill_duration_seconds=event.prefill_seconds,
312+
total_duration_seconds=event.total_seconds,
313+
),
314+
)
315+
except SessionNotFoundError as exc:
316+
await context.abort(grpc.StatusCode.NOT_FOUND, str(exc))
317+
except ValueError as exc:
318+
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(exc))
319+
except InvariantViolation as exc:
320+
await context.abort(grpc.StatusCode.FAILED_PRECONDITION, str(exc))
321+
187322
async def CloseSession( # noqa: N802
188323
self,
189324
request: runtime_pb2.CloseSessionRequest,
@@ -233,6 +368,7 @@ def create_grpc_server(
233368
*,
234369
session_store: SessionStore,
235370
append_coordinator: Optional[AppendTokensCoordinator] = None,
371+
generation_coordinator: Optional[GenerationCoordinator] = None,
236372
config: Optional[GrpcServerConfig] = None,
237373
) -> grpc.aio.Server:
238374
"""Build, but do not start, a configured gRPC asyncio server.
@@ -264,7 +400,9 @@ def create_grpc_server(
264400
)
265401
runtime_pb2_grpc.add_RuntimeServiceServicer_to_server(
266402
RuntimeServiceServicer(
267-
session_store, append_coordinator=append_coordinator,
403+
session_store,
404+
append_coordinator=append_coordinator,
405+
generation_coordinator=generation_coordinator,
268406
),
269407
server,
270408
)

inference_engine/session/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@
2020
AppendTokensCoordinator,
2121
VerifierProtocol,
2222
)
23+
from inference_engine.session.generator import (
24+
DoneEvent,
25+
GenerateEvent,
26+
GenerationCoordinator,
27+
HistoryTruncatedEvent,
28+
STOP_REASON_CANCELLED,
29+
STOP_REASON_EOS,
30+
STOP_REASON_MAX_TOKENS,
31+
STOP_REASON_TRUNCATED,
32+
TokenEvent,
33+
)
2334
from inference_engine.session.store import (
2435
CacheInspector,
2536
InvariantViolation,
@@ -32,10 +43,19 @@
3243
__all__ = [
3344
"AppendTokensCoordinator",
3445
"CacheInspector",
46+
"DoneEvent",
47+
"GenerateEvent",
48+
"GenerationCoordinator",
49+
"HistoryTruncatedEvent",
3550
"InvariantViolation",
51+
"STOP_REASON_CANCELLED",
52+
"STOP_REASON_EOS",
53+
"STOP_REASON_MAX_TOKENS",
54+
"STOP_REASON_TRUNCATED",
3655
"Session",
3756
"SessionNotFoundError",
3857
"SessionStore",
3958
"SessionStoreError",
59+
"TokenEvent",
4060
"VerifierProtocol",
4161
]

0 commit comments

Comments
 (0)