-
-
Notifications
You must be signed in to change notification settings - Fork 20.2k
[core][optimization] use a pool of numpy ndarray to hold seq data #5942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
4f58917
e864ed7
713de37
66f5154
567850d
d2defac
1ebebdb
12f1d54
eead604
d1db7bd
0470012
9271190
58da18d
c5b2926
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| """Sequence and its related classes.""" | ||
| import copy | ||
| import enum | ||
| import hashlib | ||
| import math | ||
| import weakref | ||
| from abc import ABC, abstractmethod | ||
| from dataclasses import dataclass, field | ||
| from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union | ||
|
|
||
| import numpy as np | ||
| import torch | ||
|
|
||
| from vllm.inputs import LLMInputs | ||
|
|
@@ -100,6 +103,33 @@ class RequestMetrics: | |
| finished_time: Optional[float] = None | ||
|
|
||
|
|
||
| class SequenceDataPool: | ||
| """A pool of numpy array to hold sequence data. | ||
| """ | ||
|
|
||
| def __init__(self, max_tokens: int, initial_pool_size: int) -> None: | ||
| self.max_tokens = max_tokens | ||
| self.pool: List[np.ndarray] = [] | ||
| if initial_pool_size > 0: | ||
| self.pool = [ | ||
| np.zeros(max_tokens, dtype=np.int64) | ||
| for _ in range(initial_pool_size) | ||
| ] | ||
|
|
||
| def alloc_array(self) -> np.ndarray: | ||
| if self.pool: | ||
| return self.pool.pop() | ||
| return np.zeros(self.max_tokens, dtype=np.int64) | ||
|
|
||
| def del_array(self, arr: np.ndarray) -> None: | ||
| assert arr.size == self.max_tokens | ||
| self.pool.append(arr) | ||
|
|
||
|
|
||
| # for 128k context size | ||
| _SEQUENCE_DATA_POOL = SequenceDataPool(128 * 1024, 32) | ||
|
|
||
|
|
||
| class SequenceData: | ||
| """Data associated with a sequence. | ||
|
|
||
|
|
@@ -112,50 +142,66 @@ class SequenceData: | |
| prompt_token_ids: The token IDs of the prompt. | ||
| output_token_ids: The token IDs of the output. | ||
| cumulative_logprob: The cumulative log probability of the output. | ||
| tokens: array of all the tokens (prompt + output) | ||
|
|
||
| NOTE: special care must be taken regarding data copy and returning `list` | ||
| or `np.ndarray` from this class. `get_prompt_token_ids` and | ||
| `get_output_token_ids` return `list`. They are used to construct | ||
| request output that will be returned to the user. They need to be | ||
| Python lists. And they are actually quite cheap, because the function | ||
| returns a reference to the list that is already stored in the object. | ||
| `get_token_ids` returns a view of `np.ndarray`. It avoids data copy, | ||
| and also allows array operations to be performed on the data. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| prompt_token_ids: List[int], | ||
| output_token_ids: Optional[List[int]] = None, | ||
| ) -> None: | ||
| self.tokens = _SEQUENCE_DATA_POOL.alloc_array() | ||
| self.prompt_token_ids_list = prompt_token_ids | ||
| self.num_prompt_tokens = len(prompt_token_ids) | ||
| self.tokens[:self.num_prompt_tokens] = prompt_token_ids | ||
| if output_token_ids is None: | ||
| output_token_ids = [] | ||
|
|
||
| self.prompt_token_ids = prompt_token_ids | ||
| self._prompt_token_ids_tuple = tuple(prompt_token_ids) | ||
| self.output_token_ids = output_token_ids | ||
| self.num_output_tokens = len(output_token_ids) | ||
| self.output_token_ids_list = output_token_ids | ||
| self.tokens[self.num_prompt_tokens:self.num_prompt_tokens + | ||
| self.num_output_tokens] = output_token_ids | ||
| self.cumulative_logprob = 0.0 | ||
| # The number of tokens that are computed (that run against the model). | ||
| self._num_computed_tokens = 0 | ||
| self._stage: SequenceStage = SequenceStage.PREFILL | ||
| self._finalizer = weakref.finalize(self, _SEQUENCE_DATA_POOL.del_array, | ||
| self.tokens) | ||
|
|
||
| def append_token_id(self, token_id: int, logprob: float) -> None: | ||
| self.output_token_ids.append(token_id) | ||
| self.tokens[self.num_prompt_tokens + self.num_output_tokens] = token_id | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we should have an assertion to check the boundary, even 128k should always be sufficient atm. Let's add an assert if it doesn't hurt performance; otherwise we could just comment that we assume the context length won't go beyond 128k.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think numpy array indexing already has boundary check.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to somehow know the max seq length in seqdata, but don't know how to pass that information across so many levels.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting a fixed length makes sense to me considering the simplicity. Hmm maybe it's ok to keep the current implementation then. If someone really hits the boundary and see the numpy error, we could know what's going on... |
||
| self.output_token_ids_list.append(token_id) | ||
| self.num_output_tokens += 1 | ||
| self.cumulative_logprob += logprob | ||
|
|
||
| def get_len(self) -> int: | ||
| return len(self.output_token_ids) + len(self.prompt_token_ids) | ||
| return self.num_prompt_tokens + self.num_output_tokens | ||
|
|
||
| def get_prompt_len(self) -> int: | ||
| return len(self.prompt_token_ids) | ||
| return self.num_prompt_tokens | ||
|
|
||
| def get_output_len(self) -> int: | ||
| return len(self.output_token_ids) | ||
| return self.num_output_tokens | ||
|
|
||
| def get_token_ids(self) -> List[int]: | ||
| return self.prompt_token_ids + self.output_token_ids | ||
| def get_token_ids(self) -> np.ndarray: | ||
| return self.tokens[:self.num_prompt_tokens + self.num_output_tokens] | ||
|
|
||
| def get_prefix_token_ids( | ||
| self, num_tokens: int | ||
| ) -> Tuple[Tuple[int, ...], Optional[Tuple[int, ...]]]: | ||
| def hash_prefix_token_ids(self, num_tokens: int) -> bytes: | ||
| """Get prefix tokens, and make the return value hashable""" | ||
| prompt_length = len(self.prompt_token_ids) | ||
| if num_tokens > prompt_length: | ||
| return (self._prompt_token_ids_tuple, | ||
| tuple(self.output_token_ids[:num_tokens - prompt_length])) | ||
| else: | ||
| return (self._prompt_token_ids_tuple[:num_tokens], None) | ||
| data = self.tokens[:num_tokens] | ||
| # get a memory view of the underlying data | ||
| buffer = memoryview(data) # type: ignore | ||
| # hash the memory view | ||
| hash_value = hashlib.sha256(buffer).digest() | ||
| return hash_value | ||
|
|
||
| def get_num_computed_tokens(self) -> int: | ||
| """Return the number of prefill tokens that are already computed.""" | ||
|
|
@@ -186,24 +232,23 @@ def get_num_uncomputed_tokens(self) -> int: | |
| return self.get_len() - self.get_num_computed_tokens() | ||
|
|
||
| def get_last_token_id(self) -> int: | ||
| if not self.output_token_ids: | ||
| return self.prompt_token_ids[-1] | ||
| return self.output_token_ids[-1] | ||
| return int(self.tokens[self.num_prompt_tokens + | ||
| self.num_output_tokens - 1]) | ||
|
|
||
| def get_prompt_token_ids(self) -> List[int]: | ||
| return self.prompt_token_ids | ||
| return self.prompt_token_ids_list | ||
|
|
||
| def get_output_token_ids(self) -> List[int]: | ||
| return self.output_token_ids | ||
| return self.output_token_ids_list | ||
|
|
||
| @property | ||
| def stage(self) -> SequenceStage: | ||
| return self._stage | ||
|
|
||
| def __repr__(self) -> str: | ||
| return (f"SequenceData(" | ||
| f"prompt_token_ids={self.prompt_token_ids}, " | ||
| f"output_token_ids={self.output_token_ids}, " | ||
| f"prompt_token_ids={self.get_prompt_token_ids()}, " | ||
| f"output_token_ids={self.get_output_token_ids()}, " | ||
| f"cumulative_logprob={self.cumulative_logprob})") | ||
|
|
||
|
|
||
|
|
@@ -232,10 +277,13 @@ def __init__( | |
| self.eos_token_id = eos_token_id | ||
| self.lora_request = lora_request | ||
|
|
||
| self.data = SequenceData(self.prompt_token_ids) | ||
| self.data = SequenceData(self.inputs["prompt_token_ids"]) | ||
| self.prompt_token_ids: List[int] = self.inputs["prompt_token_ids"] | ||
| self.prompt: Optional[str] = self.inputs.get("prompt") | ||
| self.output_logprobs: SampleLogprobs = [] | ||
| self.output_text = "" | ||
|
|
||
| # Initialize the logical token blocks with the prompt token ids. | ||
| self.status = SequenceStatus.WAITING | ||
| self.stop_reason: Union[int, str, None] = None | ||
|
|
||
|
|
@@ -247,15 +295,7 @@ def __init__( | |
|
|
||
| @property | ||
| def n_blocks(self) -> int: | ||
| return math.ceil(self.get_len() / self.block_size) | ||
|
|
||
| @property | ||
| def prompt(self) -> Optional[str]: | ||
| return self.inputs.get("prompt") | ||
|
|
||
| @property | ||
| def prompt_token_ids(self) -> List[int]: | ||
| return self.inputs["prompt_token_ids"] | ||
| return math.ceil(self.data.get_len() / self.block_size) | ||
|
|
||
| @property | ||
| def multi_modal_data(self) -> Optional["MultiModalData"]: | ||
|
|
@@ -278,8 +318,8 @@ def hash_of_block(self, logical_idx: int) -> int: | |
| # TODO: The current hashing function is O(L^2). We should optimize | ||
| # this in the future. | ||
| num_tokens = self.num_hashed_tokens_of_block(logical_idx) | ||
| hashed_tokens = self.data.get_prefix_token_ids(num_tokens) | ||
| return hash((hashed_tokens, self.lora_int_id)) | ||
| tokens_hash = self.data.hash_prefix_token_ids(num_tokens) | ||
| return hash((tokens_hash, self.lora_int_id)) | ||
|
|
||
| def num_hashed_tokens_of_block(self, logical_idx: int): | ||
| return logical_idx * self.block_size + self.block_size | ||
|
|
@@ -306,7 +346,7 @@ def get_prompt_len(self) -> int: | |
| def get_output_len(self) -> int: | ||
| return self.data.get_output_len() | ||
|
|
||
| def get_token_ids(self) -> List[int]: | ||
| def get_token_ids(self) -> np.ndarray: | ||
| return self.data.get_token_ids() | ||
|
|
||
| def get_prompt_token_ids(self) -> List[int]: | ||
|
|
@@ -316,7 +356,7 @@ def get_last_token_id(self) -> int: | |
| return self.data.get_last_token_id() | ||
|
|
||
| def get_output_token_ids(self) -> List[int]: | ||
| return self.data.output_token_ids | ||
| return self.data.get_output_token_ids() | ||
|
|
||
| def get_cumulative_logprob(self) -> float: | ||
| return self.data.cumulative_logprob | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any opportunity to get rid of this list (and output token ids list)? This is completely duplicated to the numpy arrays and we should avoid that as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to delete it, too. However, sometimes we need to get the list of int of prompt token ids because users want list of int. If we don't store it here, we need to create a copy from numpy array, which is expensive.
Fortunately, this is just a reference, performance-wise it is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I searched the code base and seems like only batch expansion uses
get_prompt_token_ids()andget_output_token_ids(), so it should be possible, as batch expansion is going to be removed by @LiuXiaoxuanPKUThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good to know.