207 lines
10 KiB
Markdown
207 lines
10 KiB
Markdown
---
|
||
orphan: true
|
||
---
|
||
(serve-streaming-tutorial)=
|
||
|
||
# Serve a Chatbot with Request and Response Streaming
|
||
|
||
This example deploys a chatbot that streams output back to the user. It shows:
|
||
|
||
* How to stream outputs from a Serve application
|
||
* How to use WebSockets in a Serve application
|
||
* How to combine batching requests with streaming outputs
|
||
|
||
This tutorial should help you with following use cases:
|
||
|
||
* You want to serve a large language model and stream results back token-by-token.
|
||
* You want to serve a chatbot that accepts a stream of inputs from the user.
|
||
|
||
This tutorial serves the [DialoGPT](https://huggingface.co/microsoft/DialoGPT-small) language model. Install the Hugging Face library to access it:
|
||
|
||
```
|
||
pip install "ray[serve]" transformers torch
|
||
```
|
||
|
||
## Create a streaming deployment
|
||
|
||
Open a new Python file called `textbot.py`. First, add the imports and the [Serve logger](serve-logging).
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __textbot_setup_start__
|
||
:end-before: __textbot_setup_end__
|
||
```
|
||
|
||
Create a [FastAPI deployment](serve-fastapi-http), and initialize the model and the tokenizer in the constructor:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __textbot_constructor_start__
|
||
:end-before: __textbot_constructor_end__
|
||
```
|
||
|
||
Note that the constructor also caches an `asyncio` loop. This behavior is useful when you need to run a model and concurrently stream its tokens back to the user.
|
||
|
||
Add the following logic to handle requests sent to the `Textbot`:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __textbot_logic_start__
|
||
:end-before: __textbot_logic_end__
|
||
```
|
||
|
||
`Textbot` uses three methods to handle requests:
|
||
|
||
* `handle_request`: the entrypoint for HTTP requests. FastAPI automatically unpacks the `prompt` query parameter and passes it into `handle_request`. This method then creates a `TextIteratorStreamer`. Hugging Face provides this streamer as a convenient interface to access tokens generated by a language model. `handle_request` then kicks off the model in a background thread using `self.loop.run_in_executor`. This behavior lets the model generate tokens while `handle_request` concurrently calls `self.consume_streamer` to stream the tokens back to the user. `self.consume_streamer` is a generator that yields tokens one by one from the streamer. Lastly, `handle_request` passes the `self.consume_streamer` generator into a Starlette `StreamingResponse` and returns the response. Serve unpacks the Starlette `StreamingResponse` and yields the contents of the generator back to the user one by one.
|
||
* `generate_text`: the method that runs the model. This method runs in a background thread kicked off by `handle_request`. It pushes generated tokens into the streamer constructed by `handle_request`.
|
||
* `consume_streamer`: a generator method that consumes the streamer constructed by `handle_request`. This method keeps yielding tokens from the streamer until the model in `generate_text` closes the streamer. This method avoids blocking the event loop by calling `asyncio.sleep` with a brief timeout whenever the streamer is empty and waiting for a new token.
|
||
|
||
Bind the `Textbot` to a language model. For this tutorial, use the `"microsoft/DialoGPT-small"` model:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __textbot_bind_start__
|
||
:end-before: __textbot_bind_end__
|
||
```
|
||
|
||
Run the model with `serve run textbot:app`, and query it from another terminal window with this script:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __stream_client_start__
|
||
:end-before: __stream_client_end__
|
||
```
|
||
|
||
You should see the output printed token by token.
|
||
|
||
## Stream inputs and outputs using WebSockets
|
||
|
||
WebSockets let you stream input into the application and stream output back to the client. Use WebSockets to create a chatbot that stores a conversation with a user.
|
||
|
||
Create a Python file called `chatbot.py`. First add the imports:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __chatbot_setup_start__
|
||
:end-before: __chatbot_setup_end__
|
||
```
|
||
|
||
Create a FastAPI deployment, and initialize the model and the tokenizer in the constructor:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __chatbot_constructor_start__
|
||
:end-before: __chatbot_constructor_end__
|
||
```
|
||
|
||
Add the following logic to handle requests sent to the `Chatbot`:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __chatbot_logic_start__
|
||
:end-before: __chatbot_logic_end__
|
||
```
|
||
|
||
The `generate_text` and `consume_streamer` methods are the same as they were for the `Textbot`. The `handle_request` method has been updated to handle WebSocket requests.
|
||
|
||
The `handle_request` method is decorated with a `fastapi_app.websocket` decorator, which lets it accept WebSocket requests. First it `awaits` to accept the client's WebSocket request. Then, until the client disconnects, it does the following:
|
||
|
||
* gets the prompt from the client with `ws.receive_text`
|
||
* starts a new `TextIteratorStreamer` to access generated tokens
|
||
* runs the model in a background thread on the conversation so far
|
||
* streams the model's output back using `ws.send_text`
|
||
* stores the prompt and the response in the `conversation` string
|
||
|
||
Each time `handle_request` gets a new prompt from a client, it runs the whole conversation–with the new prompt appended–through the model. When the model finishes generating tokens, `handle_request` sends the `"<<Response Finished>>"` string to inform the client that the model has generated all tokens. `handle_request` continues to run until the client explicitly disconnects. This disconnect raises a `WebSocketDisconnect` exception, which ends the call.
|
||
|
||
Read more about WebSockets in the [FastAPI documentation](https://fastapi.tiangolo.com/advanced/websockets/).
|
||
|
||
Bind the `Chatbot` to a language model. For this tutorial, use the `"microsoft/DialoGPT-small"` model:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __chatbot_bind_start__
|
||
:end-before: __chatbot_bind_end__
|
||
```
|
||
|
||
Run the model with `serve run chatbot:app`. Query it using the `websockets` package, using `pip install websockets`:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __ws_client_start__
|
||
:end-before: __ws_client_end__
|
||
```
|
||
|
||
You should see the outputs printed token by token.
|
||
|
||
## Batch requests and stream the output for each
|
||
|
||
Improve model utilization and request latency by batching requests together when running the model.
|
||
|
||
Create a Python file called `batchbot.py`. First add the imports:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __batchbot_setup_start__
|
||
:end-before: __batchbot_setup_end__
|
||
```
|
||
|
||
:::{warning}
|
||
Hugging Face's support for `Streamers` is still under development and may change in the future. `RawQueue` is compatible with the `Streamers` interface in Hugging Face 4.30.2. However, the `Streamers` interface may change, making the `RawQueue` incompatible with Hugging Face models in the future.
|
||
:::
|
||
|
||
Similar to `Textbot` and `Chatbot`, the `Batchbot` needs a streamer to stream outputs from batched requests, but Hugging Face `Streamers` don't support batched requests. Add this custom `RawStreamer` to process batches of tokens:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __raw_streamer_start__
|
||
:end-before: __raw_streamer_end__
|
||
```
|
||
|
||
Create a FastAPI deployment, and initialize the model and the tokenizer in the constructor:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __batchbot_constructor_start__
|
||
:end-before: __batchbot_constructor_end__
|
||
```
|
||
|
||
Unlike `Textbot` and `Chatbot`, the `Batchbot` constructor also sets a `pad_token`. You need to set this token to batch prompts with different lengths.
|
||
|
||
Add the following logic to handle requests sent to the `Batchbot`:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __batchbot_logic_start__
|
||
:end-before: __batchbot_logic_end__
|
||
```
|
||
|
||
`Batchbot` uses four methods to handle requests:
|
||
|
||
* `handle_request`: the entrypoint method. This method simply takes in the request's prompt and calls the `run_model` method on it. `run_model` is a generator method that also handles batching the requests. `handle_request` passes `run_model` into a Starlette `StreamingResponse` and returns the response, so the bot can stream generated tokens back to the client.
|
||
* `run_model`: a generator method that performs batching. Since `run_model` is decorated with `@serve.batch`, it automatically takes in a batch of prompts. See the [batching guide](serve-batch-tutorial) for more info. `run_model` creates a `RawStreamer` to access the generated tokens. It calls `generate_text` in a background thread, and passes in the `prompts` and the `streamer`, similar to the `Textbot`. Then it iterates through the `consume_streamer` generator, repeatedly yielding a batch of tokens generated by the model.
|
||
* `generate_text`: the method that runs the model. It's mostly the same as `generate_text` in `Textbot`, with two differences. First, it takes in and processes a batch of prompts instead of a single prompt. Second, it sets `padding=True`, so prompts with different lengths can be batched together.
|
||
* `consume_streamer`: a generator method that consumes the streamer constructed by `handle_request`. It's mostly the same as `consume_streamer` in `Textbot`, with one difference. It uses the `tokenizer` to decode the generated tokens. Usually, the Hugging Face streamer handles the decoding. Because this implementation uses the custom `RawStreamer`, `consume_streamer` must handle the decoding.
|
||
|
||
:::{tip}
|
||
Some inputs within a batch may generate fewer outputs than others. When a particular input has nothing left to yield, pass a `StopIteration` object into the output iterable to terminate that input's request. See [Streaming batched requests](serve-streaming-batched-requests-guide) for more details.
|
||
:::
|
||
|
||
Bind the `Batchbot` to a language model. For this tutorial, use the `"microsoft/DialoGPT-small"` model:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __batchbot_bind_start__
|
||
:end-before: __batchbot_bind_end__
|
||
```
|
||
|
||
Run the model with `serve run batchbot:app`. Query it from two other terminal windows with this script:
|
||
|
||
```{literalinclude} ../doc_code/streaming_tutorial.py
|
||
:language: python
|
||
:start-after: __stream_client_start__
|
||
:end-before: __stream_client_end__
|
||
```
|
||
|
||
You should see the output printed token by token in both windows.
|