feat(serve): add SSE streaming support for /v1/chat/completions - #504
Open
tangxinyao wants to merge 4 commits into
Open
feat(serve): add SSE streaming support for /v1/chat/completions#504tangxinyao wants to merge 4 commits into
tangxinyao wants to merge 4 commits into
Conversation
tangxinyao
force-pushed
the
fix/serve-stream-support
branch
from
August 24, 2026 12:10
295c6b5 to
f1790c4
Compare
- Support stream=true in the chat completions endpoint - CUDA and MLX paths both use true streaming via the engine async API - CUDA path uses multiprocessing.Pipe from worker decode loop for per-token delivery - MLX path uses engine async call directly for per-token streaming - Handle client disconnect cancellation inline in both paths - Add ChatCompletionStreamDelta and ChatCompletionStreamChoice Pydantic models - Add generate_rollout_stream_async to engine for real-time token streaming - Add StreamTokenStep protocol message for pipe-based token delivery - Add CPU test suite for streaming behavior - Multi-choice (n>1) and tool-call requests fall back to full-response SSE Co-Authored-By: Claude <noreply@anthropic.com>
tangxinyao
force-pushed
the
fix/serve-stream-support
branch
2 times, most recently
from
August 24, 2026 13:31
1477246 to
14cb069
Compare
…A and MLX paths - Add _ServeStreamStep (backend-agnostic type) and _ServeRuntime Protocol - Add generate_rollout_stream_async to _CudaServeRuntime (wrapper pattern) - Update _MlxServeRuntime.generate_rollout_stream_async to yield _ServeStreamStep - Merge _stream_chat_completions_cuda and _stream_chat_completions into one function - Update ServeState.engine type from Any to _CudaServeRuntime | _MlxServeRuntime - Remove isinstance(state.engine, _CudaServeRuntime) dispatch - Fix test mock to yield _ServeStreamStep instead of raw tuples Co-Authored-By: Claude <noreply@anthropic.com>
tangxinyao
force-pushed
the
fix/serve-stream-support
branch
2 times, most recently
from
August 25, 2026 09:03
6dc93d1 to
7affc7f
Compare
Reuse the single Op.INFER_ROLLOUT command and result_queue IPC channel for streaming; drop the dedicated stream op + pass-through pipe so there is no separate IPC execution path. - Route streaming tokens by batch row id through a per-request _StreamTokenRouter; a streaming request admitted via refill into a non-streaming batch now keeps its tokens instead of silently losing them (two refilled requests must not each renumber prompts from zero) - Gate per-step token emission on the router's enabled flag so fully non-streaming batches (incl. training rollouts) skip the decode-loop sync - Reserve streaming batch capacity from max_running_prompts (mirrors the blocking path) so concurrent requests continuous-batch in one decode loop - Propagate worker failures out of TPCluster.call_async_stream via future.result() instead of silently ending the SSE stream with "stop" - Match serve streaming/fallback decode_progress_interval_s to the blocking path so stream and non-stream requests can share a worker batch - Fold stream_loop into the reused loop field (single per-request queue) - Add CPU regression tests for mixed-batch refill routing, capacity sizing and stream error propagation Co-Authored-By: Claude <noreply@anthropic.com>
tangxinyao
force-pushed
the
fix/serve-stream-support
branch
from
August 25, 2026 13:17
7affc7f to
9ed73b6
Compare
Collaborator
|
@tangxinyao Please resolve merge conflicts. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace the hardcoded 400 rejection of
stream=truewith aStreamingResponsethat emits OpenAI-compatible SSE chunks.The engine now supports true token-level streaming through a unified
generate_rollout_stream_asyncinterface. On the CUDA path, worker processes push incrementalStreamTokenStepdata throughmultiprocessing.Pipefrom within the decode loop, coordinated byTPCluster.stream_call_async. On the MLX path, theContinuousBatchSchedulerdaemon thread writes tokens to aqueue.Queue, consumed by the serve layer viarun_in_executor. Both paths deliver tokens as they are produced, providing low time-to-first-token and real-time SSE output. Multi-choice (n>1) and tool-call requests fall back to the synchronous generation path with_build_sse_chunksfor SSE encoding.Changes:
engine/data/batch.py): addStreamTokenStep— incremental token unit withprompt_idx,token_id,finish_reasonengine/protocol.py): addOp.INFER_ROLLOUT_STREAM,StreamTokenPayload, andTPCluster.stream_call_async()— createsmp.Pipe, broadcasts send-end to workers, reads tokens from recv-end in a thread pool as an async generatorengine/worker.py): addArenoWorker.handle_stream_rollout()— extracts pipe send-end, sends ack immediately so coordinator can start reading, drives decode with astream_callbackclosure that writes eachStreamTokenStepto the pipeengine/inference.py): addStreamTokenCallbacktype andstream_callbackparameter toinfer_rollout/_generate_rollout_tokens_no_sync— invoked after prefill and after each decode step (TP rank-0 only)engine/api.py): addArenoEngine.generate_rollout_stream_async()→AsyncGenerator[StreamTokenStep]cli/serve.py):_CudaServeRuntime.generate_rollout_stream_async: convertsareno.api.SamplingParams→ engineSamplingParams, delegates toArenoEngine.generate_rollout_stream_async, adaptsStreamTokenStep→_ServeStreamStep_MlxServeRuntime.generate_rollout_stream_async: consumesqueue.QueuefromContinuousBatchScheduler.submit_stream()viarun_in_executor, wraps as_ServeStreamStepstate.engine.generate_rollout_stream_async()withoutisinstancedispatchcli/serve.py):ChatCompletionStreamDelta/ChatCompletionStreamChoicePydantic models_stream_chat_completions()— unified SSE entry for both CUDA and MLX, performs incremental prefix decoding and emits SSE chunks token-by-token_build_sse_chunks()— fallback SSE builder for multi-choice (n>1) and tool-call requests, using the synchronous generation path/v1/chat/completionsonrequest.stream, returnStreamingResponsefor streaming requestsWhat does this PR do?
This PR adds SSE streaming support to
areno serve's/v1/chat/completionsendpoint.Previously, requests with
stream=truewere rejected with a 400 error. This prevented AReno from being used as a backend by Agent frameworks that rely on the OpenAI streaming API for tool-use and multi-turn agent execution.The implementation adds a true token-level streaming pipeline across the full stack:
stream_callbackis invoked after each decode step (both after prefill and after every subsequent token), pushing incrementalStreamTokenStepdata before the full rollout completes.multiprocessing.Pipecarries tokens from the worker process to the coordinator, with the worker sending an immediate ack so the coordinator can start reading before decode finishes.queue.Queuebridges the daemon decode thread and the serve layer's async event loop viarun_in_executor._stream_chat_completions()coroutine consumes the async generator from either backend, performs incremental prefix decoding, and emits OpenAI-compatible SSE chunks in real time. Multi-choice (n>1) and tool-call requests fall back to the synchronous generation path with_build_sse_chunksfor SSE encoding.This provides both protocol compatibility for Agent frameworks (OpenAI SDK, LangChain, AutoGen, CrewAI) and low time-to-first-token for interactive serving.
Related issue
#503
Type of change
How was it tested?
Added CPU tests covering:
assistantrole chunkfinish_reasononly appearing in the final chunkusageincluded in the final chunkn > 1)data: ...\n\nformattingTest command:
The tests run on CPU and do not require GPU-specific functionality.
Checklist
pytest tests/ -k cpu).Breaking change details