Proposal to improve performance
For flexibility, lots of code in vLLM uses Python list.
The memory layout for a Python list of [1, 2, 3, 4, 5], is:
----
PyObject pointer --> PyLong(1)
----
PyObject pointer --> PyLong(2)
----
PyObject pointer --> PyLong(3)
----
PyObject pointer --> PyLong(4)
----
PyObject pointer --> PyLong(5)
----
This is because a Python list can hold arbitrary Python object.
When we use torch.tensor([1, 2, 3, 4, 5], dtype=torch.int, device="cuda"), there's two copy operation happening:
- PyTorch has to collect all the data from scattered memory into a continuous memory area, i.e. a CPU memory segment holding
1, 2, 3, 4, 5 consecutively (40 bytes)
- PyTorch launches an operation to copy the CPU memory to GPU memory, wraps it into a GPU tensor
There is a better alternative in Python, called array.array. It is very similar to vector type in C++, which can hold variable length data with the same type. Since the memory layout is already compact, we can directly create pytorch tensor from it, without copying, and then copy it to GPU. i.e., we can reduce the copy in step 1.
Here is some microbenchmark:
import array
import torch
# print header
print("N\tlist\tarray")
for N in [100, 1000, 10000, 100000, 1000000]:
list_data = list(range(N))
array_data = array.array('l', list_data)
def create_from_list():
tensor = torch.tensor(list_data, dtype=torch.int, device='cuda')
torch.cuda.synchronize()
return tensor
def create_from_array():
tensor = torch.frombuffer(array_data, dtype=torch.int).to('cuda')
torch.cuda.synchronize()
return tensor
import time
for _ in range(10):
# warmup
create_from_list()
start = time.time()
for _ in range(100):
create_from_list()
elapsed_list = (time.time() - start) / 100 * 1000 # ms
for _ in range(10):
create_from_array()
start = time.time()
for _ in range(100):
create_from_array()
elapsed_array = (time.time() - start) / 100 * 1000 # ms
print(f"{N}\t{elapsed_list:.3f}\t{elapsed_array:.3f}")
The output is:
| N |
list to GPU (ms) |
array to GPU (ms) |
| 100 |
0.020 |
0.016 |
| 1000 |
0.046 |
0.017 |
| 10000 |
0.300 |
0.024 |
| 100000 |
2.793 |
0.071 |
| 1000000 |
27.219 |
0.512 |
As we can see, use array to copy to GPU is always faster. When the input is large, the difference is even larger.
However, how can we get an array object? If we do array_data = array.array('l', list_data) , it is another copy, and will not give us any benefit.
The answer is, we should try to start with array, and use array.append / array.extend to replace list.append / list.extend. Then, we should replace torch.tensor(data, dtype=torch.int, device="cuda") to torch.frombuffer(data, dtype=torch.int).to(device="cuda").
This will require rewrite lots of the code in prepare-input and block table preparation, one of the main performance bottleneck.
cc @comaniac for prepare input
cc @alexm-neuralmagic @cadedaniel for block manager
Report of performance regression
No response
Misc discussion on performance
No response
Your current environment (if you think it is necessary)
The output of `python collect_env.py`
Proposal to improve performance
For flexibility, lots of code in vLLM uses Python list.
The memory layout for a Python list of
[1, 2, 3, 4, 5], is:This is because a Python list can hold arbitrary Python object.
When we use
torch.tensor([1, 2, 3, 4, 5], dtype=torch.int, device="cuda"), there's two copy operation happening:1, 2, 3, 4, 5consecutively (40 bytes)There is a better alternative in Python, called
array.array. It is very similar tovectortype inC++, which can hold variable length data with the same type. Since the memory layout is already compact, we can directly create pytorch tensor from it, without copying, and then copy it to GPU. i.e., we can reduce the copy in step 1.Here is some microbenchmark:
The output is:
As we can see, use array to copy to GPU is always faster. When the input is large, the difference is even larger.
However, how can we get an array object? If we do
array_data = array.array('l', list_data), it is another copy, and will not give us any benefit.The answer is, we should try to start with
array, and usearray.append/array.extendto replacelist.append/list.extend. Then, we should replacetorch.tensor(data, dtype=torch.int, device="cuda")totorch.frombuffer(data, dtype=torch.int).to(device="cuda").This will require rewrite lots of the code in prepare-input and block table preparation, one of the main performance bottleneck.
cc @comaniac for prepare input
cc @alexm-neuralmagic @cadedaniel for block manager
Report of performance regression
No response
Misc discussion on performance
No response
Your current environment (if you think it is necessary)