Prefill và Giải mã cho các Yêu cầu Đồng thời - Tối ưu hóa Hiệu suất LLM

Prefill và Giải mã cho các Yêu cầu Đồng thời - Tối ưu hóa Hiệu suất LLM

  • 12 min read
Prefill và Giải mã cho các Yêu cầu Đồng thời - Tối ưu hóa Hiệu suất LLM
Prefill và Giải mã cho các Yêu cầu Đồng thời - Tối ưu hóa Hiệu suất LLM

Prefill và Decode cho Yêu cầu Đồng thời - Tối ưu hóa Hiệu suất LLM

Việc xử lý đồng thời tải từ nhiều người dùng là rất quan trọng đối với hiệu suất của các ứng dụng LLM. Trong phần trước của chuỗi bài viết về hiệu suất LLM, chúng tôi đã thảo luận về các chiến lược xếp hàng để ưu tiên các người dùng khác nhau. Trong phần thứ hai này, chúng ta sẽ tập trung vào việc xử lý đồng thời các yêu cầu và cách nó ảnh hưởng đến các chỉ số quan trọng như độ trễ và thông lượng, cũng như việc sử dụng tài nguyên GPU.

Tại TNG, chúng tôi tự lưu trữ nhiều Mô hình Ngôn ngữ Lớn trên cụm 24 GPU H100 của mình. Nó hỗ trợ 50 ứng dụng khác nhau, xử lý hơn 5.000 lượt suy luận mỗi giờ và tạo ra hơn mười triệu token mỗi ngày.

Hai Giai đoạn Tạo Token: Prefill và Decode

Hầu hết các LLM tạo văn bản từng token, điều này đảm bảo rằng mọi token mới được tính toán dựa trên tất cả các token trước đó (thuộc tính mô hình này được gọi là tự hồi quy). Token đầu ra đầu tiên phụ thuộc vào tất cả các token trong prompt, nhưng token đầu ra thứ hai đã phụ thuộc vào tất cả các token trong prompt cộng với token đầu tiên, v.v. Do đó, việc tạo token không thể song song hóa ở cấp độ của từng yêu cầu riêng lẻ.

Trong các LLM có cơ chế chú ý, việc tính toán một token mới đòi hỏi phải tính toán các vector key, valuequery cho mỗi token trước đó. May mắn thay, kết quả của một số phép tính cụ thể có thể được tái sử dụng cho các token sau đó. Khái niệm này được gọi là bộ nhớ đệm key-value (KV cache). Đối với mỗi token đầu ra bổ sung, chỉ cần tính toán một bộ vector keyvalue nữa và thêm vào KV cache. Tuy nhiên, đối với token đầu ra đầu tiên, chúng ta bắt đầu với một KV cache trống và cần tính toán số lượng bộ vector keyvalue tương ứng với số lượng token trong prompt đầu vào. May mắn thay, và trái ngược với bất kỳ lần tạo token nào sau này, tất cả các token đầu vào đều đã biết từ đầu và chúng ta có thể song song hóa việc tính toán các vector keyvalue tương ứng của chúng. Sự khác biệt này thúc đẩy sự phân biệt giữa prefill (tính toán token đầu ra đầu tiên)giai đoạn decode (tính toán bất kỳ token nào sau đó).

Trong giai đoạn prefill, các phép tính cho tất cả các token đầu vào có thể được thực thi song song, trong khi trong giai đoạn decode, không thể song song hóa ở cấp độ yêu cầu riêng lẻ.

Metrics

Sự khác biệt giữa giai đoạn prefill và decode cũng được phản ánh trong hai chỉ số chính cho việc tạo văn bản: Thời gian đến token đầu tiênthời gian cho mỗi token đầu ra. Thời gian đến token đầu tiên được cho bởi độ trễ của giai đoạn prefill, trong khi thời gian cho mỗi token đầu ra là độ trễ của một bước decode duy nhất. Mặc dù giai đoạn prefill cũng chỉ tạo ra một token, nhưng nó mất nhiều thời gian hơn một bước decode duy nhất, vì tất cả các token đầu vào cần được xử lý. Mặt khác, giai đoạn prefill nhanh hơn nhiều đối với số lượng token đầu vào so với giai đoạn decode cho cùng số lượng token đầu ra (sự khác biệt này là lý do tại sao các API LLM thương mại tính phí token đầu vào ở mức thấp hơn nhiều so với token đầu ra).

Cả hai độ trễ đều là các chỉ số quan trọng đối với các ứng dụng tương tác như chatbot. Nếu người dùng phải đợi hơn 5 giây trước khi họ thấy phản hồi, họ có thể nghĩ rằng ứng dụng bị lỗi và rời đi. Tương tự, nếu việc tạo văn bản chậm như 1 token mỗi giây, họ sẽ không đủ kiên nhẫn để đợi cho đến khi hoàn thành. Các mục tiêu độ trễ điển hình cho các ứng dụng tương tác là 100-300ms cho mỗi token đầu ra (tức là tốc độ tạo token từ 3-10 token mỗi giây, ít nhất nhanh bằng tốc độ đọc, lý tưởng cho phép xem lướt qua văn bản khi nó đang được tạo), và thời gian đến token đầu tiên là 3 giây trở xuống. Cả hai mục tiêu độ trễ này đều có thể khá khó khăn để đạt được, tùy thuộc vào kích thước mô hình, phần cứng, độ dài prompt và tải đồng thời.

Các trường hợp sử dụng khác, không tương tác, có thể không quan tâm đến độ trễ của các yêu cầu riêng lẻ, mà chỉ quan tâm đến thông lượng token tổng thể (token mỗi giây, tổng hợp trên tất cả các yêu cầu đồng thời). Điều này có thể liên quan khi bạn muốn tạo bản dịch cho sách hoặc tóm tắt các tệp mã trong một kho lưu trữ lớn.

Như chúng ta sẽ thấy trong phần sau, nhìn chung có sự đánh đổi giữa việc tối đa hóa thông lượng tổng thể và giảm thiểu độ trễ cho mỗi yêu cầu riêng lẻ.

Sử dụng Tài nguyên

Do phép tính song song cho tất cả các token đầu vào, giai đoạn prefill rất chuyên sâu về tính toán GPU. Ngược lại, bước decode cho một token đầu ra riêng lẻ sử dụng rất ít sức mạnh tính toán; ở đây, tốc độ thường bị giới hạn bởi băng thông bộ nhớ GPU, tức là mức độ nhanh chóng mà trọng số mô hình và các kích hoạt (bao gồm cả vector keyvalue) có thể được tải từ và truy cập trong bộ nhớ GPU.

Nhìn chung, thông lượng token có thể tăng cho đến khi việc sử dụng GPU (liên quan đến sức mạnh tính toán) bị bão hòa. Trong giai đoạn prefill, một yêu cầu duy nhất với prompt dài đã có thể đạt được việc sử dụng GPU tối đa. Trong giai đoạn decode, việc sử dụng GPU có thể tăng lên bằng cách xử lý hàng loạt nhiều yêu cầu. Do đó, khi bạn vẽ biểu đồ thông lượng token theo số lượng yêu cầu đồng thời, bạn thấy sự gia tăng thông lượng gần như tuyến tính ở mức độ đồng thời thấp, vì chế độ giới hạn bộ nhớ này hưởng lợi từ kích thước lô lớn hơn. Khi việc sử dụng GPU bão hòa và chế độ giới hạn tính toán được nhập, thông lượng sẽ không đổi với sự gia tăng độ đồng thời.

Xử lý Đồng thời

Bây giờ chúng ta sẽ xem xét chính xác cách một engine suy luận xử lý nhiều yêu cầu đến trong một khoảng thời gian ngắn.

Cả giai đoạn prefill và decode đều có thể tận dụng các chiến lược batching để áp dụng cùng một tập hợp các hoạt động cho các yêu cầu khác nhau. Nhưng hậu quả của việc chạy prefill và decode của các yêu cầu khác nhau cùng một lúc là gì?

Static Batching so với Continuous Batching

Hình thức batching ngây thơ nhất được gọi là static batching. (1) Bạn bắt đầu với một batch trống, (2) bạn điền batch với càng nhiều mục đang chờ xử lý và phù hợp với batch càng tốt, (3) bạn xử lý batch cho đến khi tất cả các mục trong batch hoàn thành, và (4) bạn lặp lại quy trình với một batch trống mới.

Tất cả các yêu cầu bắt đầu giai đoạn prefill của chúng cùng một lúc. Vì prefill chỉ là một hoạt động GPU duy nhất, nhưng được song song hóa mạnh mẽ (coi nó như một phép nhân ma trận rất lớn), các giai đoạn prefill của tất cả các yêu cầu đồng thời sẽ hoàn thành cùng một lúc. Sau đó, tất cả các giai đoạn decode bắt đầu đồng thời. Các yêu cầu có ít token đầu ra hơn sẽ hoàn thành sớm hơn, nhưng vì static batching, yêu cầu chờ tiếp theo chỉ có thể bắt đầu khi yêu cầu được batch lâu nhất hoàn thành.

*Static batching tối ưu hóa thời gian cho mỗi token đầu ra, vì giai đoạn decode không bị gián đoạn. Nhược điểm là việc sử dụng tài nguyên rất kém hiệu quả. Vì một prompt dài đã có thể làm bão hòa sức mạnh tính toán trong giai đoạn prefill, việc xử lý nhiều giai đoạn prefill song song không mang lại tốc độ nhanh hơn và chắc chắn sẽ tối đa hóa việc sử dụng GPU. Ngược lại, GPU có khả năng bị sử dụng dưới mức tối ưu trong giai đoạn decode, vì ngay cả một số lượng lớn các giai đoạn decode đồng thời cũng không chuyên sâu về tính toán như giai đoạn prefill cho một prompt dài.

Tuy nhiên, nhược điểm lớn nhất là thời gian đến token đầu tiên có thể kéo dài. Ngay cả khi một số yêu cầu ngắn hoàn thành sớm, yêu cầu xếp hàng tiếp theo phải chờ cho giai đoạn decode dài nhất trong batch hoàn thành trước khi giai đoạn prefill của nó có thể bắt đầu. Do lỗi này của static batching, các engine suy luận thường triển khai các chiến lược continuous batching. Ở đây, bất kỳ yêu cầu nào hoàn thành đều được xóa ngay lập tức khỏi batch và khoảng trống trong batch được lấp đầy bằng yêu cầu tiếp theo trong hàng đợi. Do đó, mọi chiến lược continuous batching đều phải đối phó với sự đồng thời giữa các giai đoạn prefill và decode.

Prefill-First

Để giảm thời gian chờ đợi của các yêu cầu, các engine suy luận như vLLM và TGI lên lịch giai đoạn prefill của các yêu cầu mới ngay khi chúng đến và phù hợp với batch hiện tại. Có thể chạy giai đoạn prefill của các yêu cầu mới song song với một bước decode duy nhất cho mỗi yêu cầu trước đó, nhưng vì mọi thứ đều được thực thi trong cùng một hoạt động GPU, thời lượng của nó bị chi phối bởi giai đoạn prefill, và đối với mỗi yêu cầu trong giai đoạn decode, chỉ có thể tạo ra một token đầu ra trong khoảng thời gian đó. Do đó, việc *ưu tiên prefill này giảm thiểu thời gian đến token đầu tiên nhưng làm gián đoạn giai đoạn decode của các yêu cầu đã chạy. Trong một ứng dụng trò chuyện, người dùng có thể trải nghiệm điều này như một sự tạm dừng của việc tạo token được truyền tải khi những người dùng khác gửi prompt dài.

Trong phép đo sau đây, bạn có thể thấy hiệu quả của continuous batching với chiến lược prefill-first.

Chunked Prefill

Một cách tiếp cận để giảm tác động của các giai đoạn prefill bị gián đoạn đối với các giai đoạn decode đang chạy là chunked prefill. Thay vì xử lý toàn bộ prompt trong một bước prefill duy nhất, nó có thể được phân phối trên nhiều chunk. Khi đó có thể có nhiều bước decode đồng thời trong giai đoạn prefill như có số lượng chunk prefill (so với chỉ một bước decode cho mỗi yêu cầu đồng thời trong toàn bộ giai đoạn prefill). Một bước prefill được chunked sẽ vẫn mất nhiều thời gian hơn một bước decode riêng biệt, nhưng với kích thước chunk nhỏ, người dùng hiện chỉ trải nghiệm sự chậm lại của việc tạo token thay vì tạm dừng hoàn toàn; điều này làm giảm trung bình thời gian cho mỗi token đầu ra. Từ góc độ của yêu cầu bị gián đoạn, một giai đoạn prefill được chunked đi kèm với một số chi phí và mất nhiều thời gian hơn một giai đoạn prefill riêng biệt, liên tục, do đó có sự gia tăng nhỏ trong thời gian đến token đầu tiên. Với kích thước chunk, chúng ta hiện có một nút điều chỉnh để ưu tiên thời gian đến token đầu tiên hoặc thời gian cho mỗi token đầu ra*. Kích thước chunk điển hình là từ 512 đến 8192 token (mặc định của vLLM là 512 khi chunked prefill lần đầu tiên được triển khai và sau đó được cập nhật lên các giá trị cao hơn).

Tuy nhiên, lợi thế lớn nhất của chiến lược này là chunked prefill tối đa hóa hiệu quả tài nguyên. Prefill chuyên sâu về tính toán, trong khi decode bị giới hạn bởi bộ nhớ. Bằng cách chạy cả hai hoạt động song song, người ta có thể tăng thông lượng tổng thể mà không bị giới hạn bởi tài nguyên GPU. Tất nhiên, hiệu quả tối đa chỉ đạt được ở một kích thước chunk nhất định, điều này lần lượt phụ thuộc vào mô hình tải cụ thể.

Trong một triển khai vLLM tiêu chuẩn và đối với các yêu cầu có kích thước bằng nhau, chúng tôi nhận thấy rằng chunked prefill đã tăng tổng thông lượng token lên +50%. Hiện tại, nó được kích hoạt cho mọi triển khai vLLM của LLM tự lưu trữ tại TNG. Nhìn chung, chunked prefill là một chiến lược mặc định tốt cho hầu hết các trường hợp sử dụng. Tuy nhiên, việc tối ưu hóa kích thước chunk khá khó khăn trong môi trường có các mẫu tải không thể đoán trước (như TNG với nhiều ứng dụng đa dạng của nó); thông thường, bạn sẽ giữ nguyên cài đặt mặc định.

Bất kể cấu hình kích thước chunk chính xác là gì, việc xử lý đồng thời với chunked prefill đi kèm với hai thách thức mà chúng ta sẽ giải quyết trong bài viết tiếp theo.

Recommended for You

Batching liên tục từ những nguyên tắc cơ bản

Batching liên tục từ những nguyên tắc cơ bản

Batching liên tục từ những nguyên tắc cơ bản

DeLERP- Nội suy Tuyến tính Phân rã để Hợp nhất Mô hình

DeLERP- Nội suy Tuyến tính Phân rã để Hợp nhất Mô hình

DeLERP- Nội suy Tuyến tính Phân rã để Hợp nhất Mô hình