Hạ tầng phục vụ mô hình

Đổi engine, không thêm GPU

Bạn gõ xong câu hỏi, bấm gửi, rồi nhìn một màn hình trống. Một giây ở đó dài hơn bạn tưởng. Đầu tháng 9, tôi chuyển misa-ai-1.1 sang một engine mới và quãng trống ấy rút từ 0,879 giây xuống 0,098 giây, trong khi mỗi pod phục vụ gấp đôi số request. Vẫn bộ trọng số đó, vẫn từng ấy GPU, chất lượng output không đổi. Câu hỏi đáng hỏi là phần nhanh lên đó đến từ đâu.

Minh hoạ trừu tượng: nhiều dòng token mảnh chảy ngang, bị các khối dày chặn lại từng đợt
Mỗi request đi qua hai giai đoạn dùng chung một GPU: đọc hết prompt, rồi sinh câu trả lời ra từng token. Bản mới nhanh hơn ở cả hai, nhiều nhất ở giai đoạn đầu.

Một lần đổi engine, không đổi gì khác

misa-ai-1.1 phục vụ khoảng một triệu request mỗi ngày trên cụm GPU nội bộ. Đây là model MoE kiến trúc hybrid linear-attention, trọng số FP8.

Khi nâng cấp, tôi giữ nguyên mọi thứ trừ engine: trọng số, context length, memory fraction, chunked prefill size, phần GPU được cấp, và cả hai bản đều không bật speculative decoding. Tôi muốn biết chắc phần nhanh lên đến từ đâu, nên chỉ đổi đúng một biến.

Kết quả trên traffic thật

Prefill là lúc engine đọc hết input, và đây là quãng chờ trước khi có token đầu tiên. Decode là lúc model sinh câu trả lời, mỗi lần một token. Hai giai đoạn giành nhau cùng một GPU, nên engine quyết định cả thời gian chờ lẫn tốc độ sinh token.

Bản mới nhanh hơn ở cả hai. Chênh lệch lớn nhất nằm ở prefill, và đó cũng là phần người dùng cảm thấy rõ nhất.

Biểu đồ cột ngang: cấu hình cũ 3.555 token mỗi giây, cấu hình mới 25.927 token mỗi giây
Tốc độ prefill. Số token phải prefill thật chia cho thời gian chờ tới token đầu tiên, đo trong sáu ngày chạy thật. Thời gian chờ token đầu tiên: 0,879 giây xuống 0,098 giây. Phép so này còn thiệt cho bản mới, vì nó dùng lại được ít cache hơn (35,3% so với 40,2%) mà vẫn xong sớm hơn.

Decode nhanh hơn ở mọi mức tải đo được, và khoảng cách nới rộng đúng lúc tải lên cao.

Biểu đồ cột nhóm: ở cả năm dải tải, cấu hình mới có ITL median thấp hơn cấu hình cũ
Tốc độ decode, xếp theo số request chạy cùng lúc trên một pod. Đo bằng khoảng cách giữa hai token liên tiếp, thấp hơn là token ra đều hơn. Nhanh hơn từ 1,5 đến 1,8 lần, và tải càng cao thì khoảng cách giữa hai phiên bản càng giãn. Ở dải tải cao nhất, 10,3 mili giây xuống 5,8.

Tính theo mức trung vị đo được, một response 300 token đi từ 2,8 xuống 1,2 giây. Quãng chờ trước token đầu tiên: 0,88 xuống 0,10 giây. Thời gian nằm hàng đợi: 0,099 xuống 0,005 giây.

Request hoàn tất sớm thì giải phóng GPU sớm, nên cùng một pod phục vụ được 194 nghìn request mỗi ngày thay vì 88 nghìn.

Nếu sản phẩm của bạn đang gọi misa-ai-1.1, đây là phần bạn thấy ngay mà không phải sửa gì: câu trả lời bắt đầu hiện gần như tức thì thay vì sau gần một giây, và cùng lượng GPU đó chịu được gấp đôi lượng người dùng.

Đã sửa gì trên misa-ai-1.1

Bốn thay đổi, tất cả nằm ở tầng engine. Tôi không đụng vào weights, tokenizer hay chat template, và các service gọi sang nó cũng không sửa dòng code nào.

Một, nâng container image. Bản mới cách bản cũ khoảng hai tháng phát triển upstream.

Hai, cho lớp linear-attention một kernel đọc prompt viết riêng. Model là kiến trúc hybrid, phần lớn layer dùng linear attention, nhưng cấu hình cũ không khai báo backend cho nhóm này nên chúng chạy bằng kernel chung, vốn viết cho kiểu attention khác. Kernel mới viết đúng cho loại layer đó, và với prompt rất dài thì được phép bỏ qua phần không cần đọc lại.

Ba, chia lại bộ nhớ GPU. Model hybrid này cần hai vùng nhớ co giãn theo hai thứ khác hẳn nhau.

Các layer full-attention phải nhớ nguyên văn: mỗi token đọc vào thì cất thêm cặp K và V của token đó, nên vùng này dày lên theo từng token, khoảng 20 KB mỗi token. Đây là KV cache.

Các layer linear-attention thì không giữ lịch sử. Mỗi layer mang theo một ma trận cố định, và mỗi token mới đi qua chỉ cập nhật vào ma trận đó chứ không ghi thêm dòng nào. Đọc 10 nghìn hay 100 nghìn token thì ma trận vẫn đúng chừng ấy ô. Cộng hết các layer như vậy lại thì khoảng 63 MB cho mỗi request đang chạy, và con số đó không đổi dù prompt dài hay ngắn.

Engine chia bộ nhớ GPU cho hai vùng theo một tỉ lệ mặc định. Với tải của MISA, thứ đẩy tới giới hạn là số request chạy song song chứ không phải độ dài prompt: mỗi request chiếm đủ 63 MB kể cả khi nó chỉ hỏi một câu ngắn. Vùng đó cạn trước trong khi KV cache còn thừa chỗ, nên request mới phải xếp hàng dù GPU vẫn còn bộ nhớ trống. Dời ranh giới theo số đo thật thì cả hai vùng cùng dùng hết, và độ dài context giữ nguyên.

Sơ đồ: cùng một dải bộ nhớ GPU, cách chia mặc định để trống một mảng lớn ở pool lớn dần theo prompt trong khi pool cố định mỗi request đã cạn và request phải xếp hàng; chia lại thì cả hai pool đều dùng hết và không còn xếp hàng
Cùng một dải bộ nhớ GPU, hai cách chia. Tỉ lệ mặc định để trống một mảng lớn trong KV cache, trong khi vùng trạng thái linear-attention đã cạn. Số request chạy song song bị chặn bởi vùng cạn đó, nên request mới phải chờ dù GPU vẫn còn bộ nhớ trống.

Bốn, bật nhóm sparse prefill kernel cho lớp full-attention, loại kernel dành cho context rất dài và có ngưỡng kích hoạt theo độ dài KV cache.

Prefill nhanh lên chủ yếu nhờ thay đổi thứ hai. Nhìn file cấu hình thì khó đoán, vì thay đổi thứ tư chiếm số dòng gấp bảy lần.

Tốc độ đến từ đâu

Nghi phạm đầu tiên là nhóm flag sparse prefill: kernel viết cho context rất dài, bật lên thì prefill nhanh. Nhưng nó chỉ chạy khi KV cache của một request vượt 95.000 token. Còn prompt thật của workload này dài bao nhiêu?

Thang log: toàn bộ dải p50 tới p99,9 của prompt nằm dưới 33.270 token, còn ngưỡng bật sparse prefill là 95.000 token
Độ dài prompt thực tế, so với ngưỡng để sparse prefill chịu chạy. Trục ngang mỗi vạch gấp mười lần vạch trước. 5.852.014 request trong sáu ngày. Thanh xanh trải từ mức giữa (2.400 token) tới mức 99,9% (33.270 token). Chỉ 60 request chạm được ngưỡng 95.000, tức 0,001%.

Sáu ngày, gần sáu triệu request, đúng 60 cái chạm ngưỡng. Sparse prefill gần như chưa bao giờ chạy, và toàn bộ mức tăng prefill thuộc về nhóm layer linear-attention, nhóm nằm trên đường đi của mọi request.

Điều đáng nhớ là ngưỡng kích hoạt do tải quyết định chứ không do model. Bộ flag đó mang sang chỗ khác vẫn nằm im nếu prompt ở đó không đủ dài.

Cùng đợt đó, misa-ai-2.0

misa-ai-2.0 là model đa phương thức, tôi chuyển sang sglang trong cùng đợt. Lần này phần việc nặng hơn: trước khi đổi được backend attention, phải sửa chính engine.

sglang có bốn backend attention, và trên H200 thì model này chỉ dùng được cái chậm nhất. Lý do nằm ở cấu trúc model: phần lớn layer là sliding-window chạy ở head_dim thông thường, còn một số ít layer full-attention chạy ở head_dim lớn gấp đôi, vì chúng gộp K với V làm một tensor. Hai backend nhanh nhất chặn cứng head_dim ở mức thông thường nên từ chối thẳng. Backend còn lại nhận được kích thước lớn nhưng chưa bao giờ nạp nổi trong image đang dùng.

Sơ đồ: cột layer của model, phần lớn là sliding-window head_dim thường và một số ít full-attention head_dim gấp đôi; hai backend nhanh khoá ở head_dim thường, backend chậm nhận mọi head_dim
Một nhóm nhỏ layer quyết định cả model chạy backend nào. Chúng chiếm thiểu số, nhưng là phần duy nhất phải đọc lại toàn bộ ngữ cảnh, và là phần duy nhất chạy ở head_dim lớn.

Hai bản vá gỡ chỗ đó. Bản thứ nhất lấy từ một PR upstream chưa được merge, sửa kernel chậm: nó đặt cứng số khối K/V được chở song song với việc tính là một, tức chở một khối, chờ về, tính xong rồi mới chở khối sau, và suốt lúc chờ thì phần tính của GPU ngồi không. Khối ở nhóm layer head_dim lớn nặng gấp đôi nên khoảng ngồi không cũng dài gấp đôi. Bản vá cho kernel chọn số khối theo head_dim thay vì đặt cứng.

Sơ đồ thời gian: kernel cũ chở K/V rồi GPU chờ rồi mới tính, lặp lại; kernel đã vá chở khối sau chồng lên lúc đang tính khối trước nên tổng thời gian ngắn hơn
Cùng lượng việc, khác cách xếp. Kernel cũ để GPU ngồi không suốt lúc chờ khối K/V về. Kernel đã vá chở khối sau ngay trong lúc đang tính khối trước, nên khoảng trống biến mất và cả chuỗi ngắn lại.

Bản thứ hai tôi tự viết, để backend nhanh nạp được. Thư viện flash-attn có nhiều phiên bản giao diện hàm, và thay vì hỏi thẳng bản đang cài, nó đoán theo biến môi trường CUDA_VERSION. Trên image này nó đoán sai, gọi hàm sai kiểu, và mọi model head_dim lớn vỡ ngay ở bước dựng CUDA graph, không cờ nào né được. Bản vá bỏ phần đoán, đọc thẳng chữ ký hàm thật.

Sau hai bản vá, model chạy được trên backend nhanh, và CompactAttention dựng tiếp trên nền đó. Thay vì quét toàn bộ prefix ở mỗi bước, CompactAttention chia prefix thành từng khối rồi chỉ giữ lại các khối quan trọng nhất, và chỉ bật khi prefix vượt 8.192 token. Kết quả trực tiếp: đọc 152.000 token trong 9,6 giây.

Thấy nhanh hơn chưa đủ để chuyển. Cuối tháng 8, một lượt đo riêng trên 1×H200 với prompt 27,6 nghìn token không dùng lại cache cho thấy sglang nhanh hơn vLLM 1,15 lần, thêm ba bản vá kernel thì lên 1,27 lần. Chừng đó không đáng để đánh đổi một lần chuyển production, nên tôi giữ nguyên hiện trạng và đi tìm phần còn thiếu.

Phép thử quyết định là chạy lại đúng chuỗi request của sự cố ngày 4 tháng 9, ép nhanh gấp 2,5 lần so với lúc nó xảy ra thật. Tôi muốn biết bản mới có chịu được đúng cái đã làm sập bản cũ hay không.

Biểu đồ cột ngang: vLLM 46,0 giây, sglang 3,2 giây latency trung bình trên cùng một chuỗi request
Chạy lại chuỗi request của sự cố ngày 4 tháng 9, ép nhanh gấp 2,5 lần. Cùng phần cứng, cùng chuỗi request, chỉ khác engine và backend attention. Latency trung bình 46,0 giây xuống 3,2 giây. Không chỉ nhanh hơn, bản mới còn xử lý thêm 24% request mỗi phút.

Lên production, hàng đợi giảm đúng như lúc đo thử: thời gian chờ tới lượt từ 0,297 giây xuống 0,005 giây.

Ba workload cùng phục vụ bộ trọng số đó đều khai served-model-name giống hệt nhau, nhưng mỗi cái một api-key. Gateway vì thế chia cho chúng ba luồng traffic khác hẳn nhau, input trung bình 10.159, 8.714 và 14.272 token. Bản mới đang xử lý lượng prefill gấp mười lần bản cũ, 1.350 so với 126 token mỗi pod mỗi giây, mà hàng đợi vẫn gần như trống.

Tóm lại

Hai model production đổi engine, không thêm một GPU nào. Với misa-ai-1.1, quãng chờ token đầu tiên ngắn đi chín lần và mỗi pod phục vụ gấp đôi số request. Với misa-ai-2.0, chạy lại đúng chuỗi request từng làm sập bản cũ, latency trung bình từ 46,0 xuống 3,2 giây.

Phần việc nằm dưới hai con số đó không phải là sửa cấu hình. Để misa-ai-2.0 chạy được trên backend attention nhanh, phải vá chính engine, gồm một bản vá lấy từ PR upstream chưa merge và một bản tự viết sau khi đo chữ ký hàm thật thay vì tin vào cách thư viện đoán. Để biết chia bộ nhớ GPU thế nào cho đúng, phải đo tải thật rồi mới dời ranh giới. Và để biết flag nào thực sự có công, phải đếm trong gần sáu triệu request xem có bao nhiêu cái chạm được ngưỡng kích hoạt của chính flag đó.

Cụm GPU của bạn nhiều khả năng cũng đang chạy dưới sức của nó. Hãy đẩy sản phẩm tới giới hạn mới và cho tôi biết chỗ nào còn nghẽn.