Khám phá vLLM: Giải phẫu hệ thống suy luận LLM thông lượng cao: Hệ thống phân tán phục vụ vLLM
Trước đó, chúng ta đã tìm hiểu về LLM Engine & Engine Core trong hệ thống suy luận LLM thông lượng cao , Các tính năng nâng cao – mở rộng logic lõi của engine và Scaling Up: Từ một GPU đến nhiều GPU
Tiếp theo đây, chúng ta cùng đến với phần 4: Hệ thống phân tán phục vụ vLLM
Có nhiều cách để thiết lập hạ tầng phục vụ mô hình, nhưng để cụ thể, hãy xem một ví dụ: giả sử chúng ta có hai node H100 và muốn chạy bốn engine vLLM trên chúng.
Nếu mô hình yêu cầu TP=4, chúng ta có thể cấu hình các node như sau.
Trên node đầu tiên, hãy chạy engine ở chế độ headless (không có API server) với các tham số sau:
vllm serve <model-name> –tensor-parallel-size 4 –data-parallel-size 4 –data-parallel-size-local 2 –data-parallel-start-rank 0 –data-parallel-address <master-ip> –data-parallel-rpc-port 13345 –headless
và chạy cùng lệnh đó trên node còn lại với một vài điều chỉnh nhỏ:
bỏ tùy chọn `–headless`
thay đổi rank khởi điểm của DP (DP start rank)
vllm serve <model-name> –tensor-parallel-size 4 –data-parallel-size 4 –data-parallel-size-local 2 –data-parallel-start-rank 2 –data-parallel-address <master-ip> –data-parallel-rpc-port 13345
Lưu ý: Giả định rằng mạng đã được cấu hình để tất cả các nút đều có thể kết nối tới địa chỉ IP và cổng đã chỉ định.
Điều này hoạt động như thế nào trong vLLM?
1. Trên node server không có giao diện (headless)
Trên node headless, một CoreEngineProcManager khởi chạy 2 tiến trình (theo –data-parallel-size-local), mỗi tiến trình chạy EngineCoreProc.run_engine_core. Mỗi hàm này tạo một DPEngineCoreProc (engine core) rồi đi vào vòng lặp bận của nó.
DPEngineCoreProc khởi tạo lớp cha EngineCoreProc (lớp con của EngineCore), lớp này:
- Tạo một input_queue và output_queue (queue.Queue).
- Thực hiện bắt tay ban đầu với frontend trên node khác bằng socket ZMQ kiểu DEALER (thư viện nhắn tin bất đồng bộ), và nhận thông tin địa chỉ điều phối.
- Khởi tạo nhóm DP (ví dụ dùng backend NCCL).
- Khởi tạo EngineCore với MultiProcExecutor (TP=4 trên 4 GPU như đã mô tả trước đó).
- Tạo một ready_event (threading.Event).
- Khởi động một luồng daemon đầu vào (threading.Thread) chạy process_input_sockets(…, ready_event). Tương tự cũng khởi động một luồng đầu ra.
- Vẫn trong luồng chính, chờ trên ready_event cho đến khi tất cả các luồng đầu vào trên cả 4 tiến trình (trải trên 2 node) hoàn thành bắt tay điều phối và cuối cùng gọi ready_event.set().
- Khi được bỏ chặn, gửi một thông điệp “ready” đến frontend kèm metadata (ví dụ: số lượng num_gpu_blocks có sẵn trong bộ nhớ paged KV cache).
- Sau đó luồng chính, luồng đầu vào và luồng đầu ra đều đi vào các vòng lặp bận tương ứng của chúng.
Tóm lại: Chúng ta có 4 tiến trình con (mỗi tiến trình tương ứng một bản sao DP), mỗi tiến trình chạy một luồng chính, một luồng đầu vào và một luồng đầu ra. Chúng hoàn thành bắt tay điều phối với bộ điều phối DP và frontend, sau đó cả ba luồng của mỗi tiến trình đều chạy trong các vòng lặp bận ở trạng thái ổn định.
Trạng thái ổn định hiện tại:
- Luồng đầu vào (Input thread) – chặn trên socket đầu vào cho đến khi một yêu cầu được định tuyến từ API server; khi nhận được, nó giải mã payload, đưa một mục công việc vào hàng đợi thông qua input_queue.put_nowait(…), rồi quay lại trạng thái chặn trên socket.
- Luồng chính (Main thread) – thức dậy khi input_queue.get(…) có dữ liệu, chuyển yêu cầu cho engine; MultiProcExecutor chạy forward pass và đưa kết quả vào output_queue.
- Luồng đầu ra (Output thread) – thức dậy khi output_queue.get(…) có dữ liệu, gửi kết quả trở lại API server, rồi tiếp tục chặn chờ.
Các cơ chế bổ sung:
- Bộ đếm wave của DP (DP wave counter): hệ thống theo dõi các “wave”; khi tất cả các engine trở nên rảnh, chúng sẽ đi vào trạng thái nghỉ, và bộ đếm tăng lên khi có công việc mới đến (hữu ích cho việc điều phối và thu thập số liệu).
- Thông điệp điều khiển (Control messages): API server có thể gửi nhiều thứ hơn là chỉ các yêu cầu suy luận (ví dụ: hủy yêu cầu và các RPC tiện ích/điều khiển).
- Các bước giả để đồng bộ lockstep (Dummy steps for lockstep): nếu bất kỳ bản sao DP nào có công việc, tất cả các bản sao đều thực hiện một bước forward; những bản sao không có yêu cầu sẽ thực hiện một bước giả để tham gia vào các điểm đồng bộ cần thiết (tránh chặn bản sao đang hoạt động).
2 Trên node API server
Chúng ta khởi tạo một đối tượng AsyncLLM (một lớp bọc asyncio quanh engine LLM). Bên trong, nó tạo ra một DPLBAsyncMPClient (client bất đồng bộ, đa tiến trình, cân bằng tải, song song dữ liệu).
Bên trong lớp cha của MPClient, hàm launch_core_engines được chạy và:
- Tạo các địa chỉ ZMQ dùng cho quá trình bắt tay khởi động (như đã thấy trên node headless).
- Khởi tạo một tiến trình DPCoordinator.
- Tạo một CoreEngineProcManager (giống như trên node headless).
Bên trong AsyncMPClient (lớp con của MPClient), chúng ta:
- Tạo một outputs_queue (asyncio.Queue).
- Tạo một tác vụ asyncio process_outputs_socket, tác vụ này giao tiếp (thông qua output socket) với các luồng đầu ra của cả 4 DPEngineCoreProc và ghi vào outputs_queue.
- Sau đó, một tác vụ asyncio khác là output_handler của AsyncLLM đọc từ hàng đợi này và cuối cùng gửi thông tin đến hàm create_completion.
Bên trong DPAsyncMPClient, chúng ta tạo một tác vụ asyncio run_engine_stats_update_task, tác vụ này giao tiếp với bộ điều phối DP.
Bộ điều phối DP đóng vai trò trung gian giữa frontend (API server) và backend (engine core). Nó:
- Định kỳ gửi thông tin cân bằng tải (kích thước hàng đợi, số yêu cầu đang chờ/đang chạy) đến run_engine_stats_update_task của frontend.
- Xử lý các lệnh SCALE_ELASTIC_EP từ frontend bằng cách thay đổi động số lượng engine (chỉ hoạt động với backend Ray).
- Gửi các sự kiện START_DP_WAVE đến backend (khi được frontend kích hoạt) và báo cáo lại các cập nhật trạng thái wave.
Tóm lại, frontend (AsyncLLM) chạy nhiều tác vụ asyncio (nhớ rằng: đồng thời, không phải song song):
- Một nhóm tác vụ xử lý các yêu cầu đầu vào thông qua luồng generate (mỗi yêu cầu mới từ client sẽ tạo ra một tác vụ asyncio mới).
- Hai tác vụ (process_outputs_socket, output_handler) xử lý các thông điệp đầu ra từ các engine bên dưới.
- Một tác vụ (run_engine_stats_update_task) duy trì giao tiếp với bộ điều phối DP: gửi tín hiệu kích hoạt wave, thăm dò trạng thái cân bằng tải và xử lý các yêu cầu mở rộng động.
Cuối cùng, tiến trình server chính tạo một ứng dụng FastAPI và gắn các endpoint như OpenAIServingCompletion và OpenAIServingChat, cung cấp /completion, /chat/completion và các endpoint khác. Toàn bộ stack sau đó được phục vụ thông qua Uvicorn.
Vậy, ghép tất cả lại với nhau, đây là toàn bộ vòng đời của một yêu cầu!
Bạn gửi từ terminal của mình:
curl -X POST http://localhost:8000/v1/completions -H “Content-Type: application/json” -d ‘{ “model”: “TinyLlama/TinyLlama-1.1B-Chat-v1.0”, “prompt”: “The capital of France is”, “max_tokens”: 50, “temperature”: 0.7}’
Điều gì xảy ra tiếp theo:
- Yêu cầu đi vào route create_completion của OpenAIServingCompletion trên API server.
- Hàm sẽ token hóa prompt một cách bất đồng bộ và chuẩn bị metadata (ID yêu cầu, tham số lấy mẫu, dấu thời gian, v.v.).
- Sau đó nó gọi AsyncLLM.generate, hàm này đi theo cùng luồng với engine đồng bộ, và cuối cùng gọi DPAsyncMPClient.add_request_async.
- Tiếp theo, hàm này gọi get_core_engine_for_request, thực hiện cân bằng tải giữa các engine dựa trên trạng thái của bộ điều phối DP (chọn engine có điểm thấp nhất / tải thấp nhất: score = len(waiting) * 4 + len(running)).
- Yêu cầu ADD được gửi đến input_socket của engine được chọn.
- Tại engine đó:
- Luồng đầu vào (Input thread): được bỏ chặn, giải mã dữ liệu từ input socket và đặt một mục công việc vào input_queue cho luồng chính.
- Luồng chính (Main thread): được bỏ chặn trên input_queue, thêm yêu cầu vào engine và lặp lại việc gọi engine_core.step(), đưa các kết quả trung gian vào output_queue cho đến khi đạt điều kiện dừng.
- Lưu ý:
Nhắc lại: step() gọi scheduler, model executor (và model executor này có thể là MultiProcExecutor!), v.v. Chúng ta đã thấy điều này rồi!- Luồng đầu ra (Output thread): được bỏ chặn trên output_queue và gửi kết quả trở lại thông qua output socket.
- Các kết quả đó kích hoạt các tác vụ asyncio đầu ra của AsyncLLM (process_outputs_socket và output_handler), các tác vụ này truyền các token trở lại route create_completion của FastAPI.
- FastAPI gắn thêm metadata (lý do kết thúc, logprobs, thông tin usage, v.v.) và trả về một JSONResponse thông qua Uvicorn đến terminal của bạn!
Và cứ như vậy, kết quả completion của bạn đã được trả về, toàn bộ bộ máy phân tán được ẩn phía sau một lệnh curl đơn giản.
Ghi chú bổ sung:
- Khi thêm nhiều API server, việc cân bằng tải được xử lý ở mức hệ điều hành/socket. Từ góc nhìn của ứng dụng, không có thay đổi đáng kể nào, độ phức tạp được ẩn đi.
- Khi dùng Ray làm backend cho DP, bạn có thể cung cấp một endpoint URL (/scale_elastic_ep) cho phép tự động mở rộng hoặc thu hẹp số lượng bản sao engine.
Phần tiếp theo, chúng ta sẽ đến với: Benchmarks and auto-tuning: Điểm chuẩn và tự động tinh chỉnh.
Phần 1: LLM engine & engine core: nền tảng của vLLM
Phần 2: Các tính năng nâng cao
Phần 3: Scaling up: từ một GPU đến nhiều GPU
Phần 5: Benchmark và tự động tinh chỉnh – độ trễ và thông lượng


