Link bài viết gốc: Thủ thuật tăng tốc LLM cục bộ của Karpathy cuối cùng đã có lời giải chuẩn xác
Akshay · X · @akshay_pachaar · 2026-10-09 01:44
Bài viết này được chia sẻ lại, bản quyền thuộc về tác giả gốc, toàn văn căn cứ theo link bài gốc
Năm 2023, Karpathy đã giải thích lý do tại sao LLM cục bộ lại chạy chậm. Khi model tạo ra một đoạn văn bản ngay trên máy tính của bạn, phần lớn thời gian chip phải chờ nạp trọng số từ bộ nhớ, trong khi các đơn vị tính toán lại rơi vào trạng thái nhàn rỗi.
Vài tuần sau, ông đã mô tả một phương pháp để giải quyết vấn đề này. Chi phí để kiểm tra nhiều token trong một lượt duyệt (pass) gần như tương đương với kiểm tra một token duy nhất, vì vậy một draft model nhỏ có thể đoán trước, và model chính có thể xác thực đồng thời các phỏng đoán đó.
Luận điểm đầu tiên cho đến nay vẫn là giới hạn trần. Chip Apple M5 bản tiêu chuẩn truyền tải 153 GB dữ liệu mỗi giây, trong khi model Qwen3.5 9B 4-bit có dung lượng khoảng 5,2 GB, do đó trong trường hợp mỗi lượt duyệt chỉ tạo ra một token, con chip này chỉ có thể đạt tối đa khoảng 29 token mỗi giây.
Cả llama.cpp và MLX đều nằm ngay dưới giới hạn này, lần lượt đạt 22 và 25 token mỗi giây trên cùng một prompt lập trình.
Luận điểm thứ hai mới chính là nơi mở ra tiềm năng tối ưu, nhưng các giải pháp cục bộ như chế độ MTP của llama.cpp mỗi lần chỉ có thể draft được từ 3 đến 4 token.
Engine suy luận mã nguồn mở Uzu từ Mirai đã đưa phương pháp này tiến xa hơn trên Apple Silicon. Nó có thể draft trước tới 16 token, duy trì tính mạch lạc giữa các phỏng đoán này, và xác thực tất cả chỉ trong một lượt duyệt duy nhất trên kernel được viết riêng cho M5.
Trên cùng một máy M5, cùng một model và cùng một prompt, Uzu đã đạt tốc độ xuất ra từ 92 đến 117 token mỗi giây.
Hãy cùng làm rõ nguồn gốc của giới hạn trần này, cách Uzu bứt phá qua nó, và sau đó so sánh benchmark với llama.cpp cùng MLX bằng một benchmark client có thể áp dụng cho chính các prompt của bạn.
Tại sao quá trình decode với batch size bằng 1 lại bị giới hạn bởi bộ nhớ (Why batch-one decoding is memory-bound)
Quá trình suy luận của LLM có hai giai đoạn khác biệt.
- Giai đoạn prefill xử lý song song các token của prompt.
- Giai đoạn decode tạo ra từng token phản hồi một cách tuần tự.
Decode là giai đoạn đáng chú ý, mỗi token mới đều cần thêm một lượt duyệt qua model. Trong lượt duyệt đó, runtime sẽ đọc các khối trọng số và tính điểm số cho token tiếp theo.
Khi batch size bằng 1, một lượt lan truyền xuôi (forward pass) chỉ đẩy tiến trình của một sequence duy nhất.
So với khối lượng dữ liệu trọng số phải di chuyển, tổng khối lượng tính toán là rất nhỏ. Để tạo ra một token, chip phải truyền toàn bộ trọng số của model từ bộ nhớ vào GPU trong một lần, và thực hiện khoảng một phép nhân-cộng cho mỗi trọng số.
GPU có thể hoàn thành các phép tính này trước khi bộ nhớ kịp nạp khối trọng số tiếp theo. Do đó, tốc độ tính toán thường bị nghẽn bởi băng thông bộ nhớ.
Điều này tạo ra một giới hạn trần cứng. Bản M5 tiêu chuẩn truyền tải 153 GB dữ liệu từ bộ nhớ mỗi giây. Checkpoint Qwen3.5 9B 4-bit có dung lượng khoảng 5,2 GB. Nếu mỗi lượt duyệt chỉ tạo ra một token, thì model chạy trên con chip này chỉ có thể xuất ra tối đa 153 ÷ 5,2 ≈ 29 token mỗi giây, bất kể GPU tính toán nhanh đến mức nào.
Các engine thực tế thường có hiệu năng thấp hơn mức trần này một chút, vì mỗi lượt duyệt còn phải đọc thêm dữ liệu khác, chẳng hạn như bộ nhớ cache của các token trước đó.
Quantization là kỹ thuật tiêu chuẩn để cải thiện vấn đề này, bằng cách sử dụng các trọng số nhỏ hơn để giảm số lượng byte cần đọc trong mỗi lượt duyệt.
Tuy nhiên, quantization vẫn chỉ đang tối ưu hóa cùng một vòng lặp tạo đơn lẻ từng token. Model chính vẫn buộc phải chạy lại cho mỗi token được sinh ra.
Trên thực tế, quantization đã rất phổ biến, vì vậy giờ hãy cùng tìm hiểu điều gì khiến Uzu trở nên khác biệt với tư cách là một engine suy luận.
Uzu tối ưu số byte và số token trên mỗi lượt duyệt như thế nào (How Uzu optimizes bytes and tokens per pass)
Uzu tối ưu hóa hai đại lượng trong vòng lặp decode: giảm lượng byte truyền tải trong mỗi lượt duyệt và tăng số lượng token được chấp nhận trong mỗi lượt duyệt.
- Quantization giải quyết đại lượng thứ nhất. Kỹ thuật này lưu trữ trọng số model với ít bit hơn. Các checkpoint nhỏ hơn đòi hỏi ít lưu lượng bộ nhớ hơn, đồng thời chừa lại nhiều bộ nhớ hợp nhất cho context và trạng thái ứng dụng.
- Speculative decoding giải quyết đại lượng thứ hai. Nó xác thực đồng thời nhiều token ứng viên. Nếu nhiều ứng viên được phê duyệt, sequence sẽ tiến thêm nhiều token cùng lúc.
Nhiều engine cục bộ đã hỗ trợ trọng số 4-bit. Điểm khác biệt của Uzu nằm ở luồng xử lý phần cứng được thiết kế xoay quanh định dạng này.
Mirai phát hành nhiều checkpoint đã được tối ưu cho cùng một model nền tảng. Hãng gắn nhãn chúng theo các phân cấp kích thước: M đại diện cho Medium, còn L đại diện cho Large.
Các nhãn này mô tả checkpoint, chứ không phải các kiến trúc model khác nhau.
- Checkpoint Medium sử dụng quantization số nguyên bất đối xứng 4-bit.
- Checkpoint Large sử dụng quantization đối xứng 8-bit.
Medium ưu tiên mức chiếm dụng bộ nhớ nhỏ hơn, trong khi Large giữ lại nhiều bit hơn cho mỗi trọng số. Tài liệu Giải thích chi tiết về Quantization của Mirai mô tả cách họ kết hợp post-training quantization với quantization-aware distillation.
Việc sử dụng định dạng số nguyên là một chủ đích có tính toán, vì nó đem lại lợi ích cho cả phần cứng lẫn khả năng nén.
Cụ thể hơn, khác với các thế hệ GPU Apple cũ hơn, M5 đạt thông lượng tính toán số học cao nhất ở định dạng int8.
Do đó, Uzu cũng lượng tử hóa các giá trị kích hoạt (activation - các giá trị trung gian truyền giữa các layer) sang 8-bit trong thời gian thực. Khi cả hai vế của phép nhân ma trận đều được lưu dưới dạng số nguyên, bước xác thực phỏng đoán sẽ chạy trực tiếp trên luồng ma trận int8 được tăng tốc phần cứng của M5.
Độ chính xác thấp hơn lại kéo theo một vấn đề khác.
Cụ thể, grouped quantization sẽ sinh ra sai số làm tròn khi một nhóm chứa các giá trị ngoại lai (outlier).
Mỗi giá trị trong nhóm dùng chung một hệ số tỷ lệ (scaling factor). Hệ số này phải đại diện cho biên độ lớn nhất. Các giá trị nhỏ hơn do đó chỉ được phân bổ ít cấp độ số nguyên hơn, dẫn đến sai số làm tròn lớn hơn.
Uzu áp dụng phép biến đổi Hadamard ngẫu nhiên theo khối đường chéo (block-diagonal randomized Hadamard transform) trước khi quantization. Ma trận Hadamard chỉ chứa các phần tử +1 và -1, cộng thêm một hệ số tỷ lệ tổng thể.
Nhân một khối trọng số với ma trận này sẽ trộn đều từng giá trị vào mọi vị trí, do đó một outlier lớn sẽ được dàn đều thành nhiều giá trị có kích thước trung bình.
"Ngẫu nhiên" ở đây chỉ việc đảo dấu ngẫu nhiên được áp dụng trước khi trộn, còn "theo khối đường chéo" nghĩa là mỗi khối gồm 32 giá trị sẽ được trộn độc lập. Phép biến đổi này hoàn toàn khả nghịch, vì vậy ngoài sai số làm tròn ra, nó không làm thay đổi kết quả tính toán của model.
Mirai đã chọn kích thước 32 phần tử để khớp với độ rộng SIMD của kernel GPU Apple. SIMD nghĩa là một lệnh được thực thi đồng thời trên 32 thread GPU, nhờ đó một nhóm thread có thể biến đổi trọn vẹn một khối.
Khối biến đổi (transform block) và nhóm lượng tử hóa (quantization group) quản lý các hoạt động khác nhau.
- Transform block xác định những giá trị nào sẽ được ma trận Hadamard trộn lẫn.
- Quantization group xác định những giá trị nào sẽ dùng chung các tham số quantization.
Kích thước của hai thành phần này không nhất thiết phải trùng khớp nhau.
Điểm khác biệt của Uzu nằm ở thiết kế đồng bộ giữa định dạng số học, các phép biến đổi và kernel thời gian chạy (runtime kernel). Quantization giúp giảm lưu lượng bộ nhớ và khối lượng tính toán trong mỗi lượt chạy model.
Tuy nhiên, quá trình decode thông thường vẫn chỉ trả về một token duy nhất cho mỗi lượt chạy model đích. Speculative decoding chính là đòn bẩy tiếp theo được sử dụng để giải quyết nút thắt cổ chai này.
Tạo nhiều token từ một lượt chạy model (Producing several tokens from one model pass)
Speculative decoding bổ sung thêm một draft model nhỏ bên cạnh model chính.
Draft model (bản nhỏ) đề xuất các token tiếp theo với chi phí tính toán thấp. Model chính sẽ kiểm tra đồng thời các vị trí này. Nó chấp nhận các tiền tố hợp lệ và tiếp tục sinh từ vị trí token đầu tiên bị từ chối.
Giả sử draft model đề xuất 5 token và model chính đồng ý với 4 token đầu tiên. Khi đó, một lượt xác thực duy nhất này đã giúp sequence tiến thêm 5 token — 4 token nháp được chấp nhận cộng với token do chính model chính tạo ra ở vị trí thứ năm.
Quá trình decode thông thường sẽ cần tới 5 lượt chạy model chính riêng biệt để tạo ra cùng một đoạn văn bản đó.
Model chính vẫn nắm quyền kiểm soát kết quả đầu ra cuối cùng. Draft model chỉ đóng vai trò đề xuất các token có thể kiểm tra song song. Quy trình chấp nhận chuẩn xác sẽ bảo toàn phân phối đầu ra của model mục tiêu.
Bước xác thực đòi hỏi nhiều phép tính toán hơn so với việc decode từng token đơn lẻ.
Nhưng hướng đi này vẫn được ưa chuộng hơn vì suy luận cục bộ với batch size bằng 1 thường còn dư thừa năng lực tính toán nhàn rỗi trong lúc các trọng số đang được nạp qua bộ nhớ.
Speculative decoding tận dụng năng lực tính toán nhàn rỗi này để giảm bớt số lượt chạy của model chính.
Sự đánh đổi này phụ thuộc vào tỷ lệ chấp nhận. Chuỗi draft càng dài thì batch xác thực càng lớn, nhưng cũng tạo ra nhiều khả năng các lỗi ở phần đầu làm vô hiệu hóa các token phía sau.
Nhiều hệ thống tích hợp sẵn trong model thường draft từ 3 đến 4 token. Một ví dụ phổ biến là MTP (Multi-token Prediction), trong đó các prediction head bổ sung được huấn luyện ngay trong model để đoán vài token tiếp theo.
Uzu sử dụng mức hạn ngạch từ 16 đến 32 token trên luồng xử lý M5 của mình. Checkpoint Qwen3.5 9B được sử dụng ở phần sau của bài viết này được thiết lập một tree budget gồm 16 token dành riêng cho dòng chip M5.
Các batch lớn hơn này cung cấp đủ khối lượng công việc cho các bộ tăng tốc neural của M5. Đây là các đơn vị ma trận được tích hợp trong mỗi nhân GPU, chúng chỉ thực sự phát huy hiệu quả khi phép nhân ma trận có nhiều hàng. Việc kiểm tra cùng lúc 16 token ứng viên mang lại cho chúng 16 hàng dữ liệu để xử lý thay vì chỉ 1 hàng.
Các bản draft song song dài hơn lại đặt ra vấn đề về tính mạch lạc.
Vấn đề với việc tạo draft song song (Issue with parallel draft generation)
Các bộ tạo draft song song tốc độ cao có thể dự đoán nhiều vị trí tương lai cùng một lúc.
Đúng như dự đoán, mỗi vị trí đều nhìn thấy context hiện tại, nhưng không thể thấy các token đã được chọn ở các vị trí draft trước đó.
Hãy xem xét một prompt kết thúc bằng câu "She went to the store to buy." Các vị trí độc lập có thể ưu tiên lần lượt "a", "few", "of" và "milk". Từng token khi đứng riêng lẻ đều có vẻ hợp lý. Nhưng khi ghép lại, chúng lại tạo thành "a few of milk".
Sự không khớp ở những token đầu tiên sẽ khiến toàn bộ chuỗi phía sau bị bác bỏ. Đó là lý do tại sao khi độ dài bản draft song song tăng lên, tỷ lệ chấp nhận lại có xu hướng sụt giảm. Càng nhiều vị trí dự đoán thì càng tạo ra nhiều cơ hội để các token tưởng chừng hợp lý khi đứng riêng lẻ kết hợp thành một chuỗi không hợp lệ.
Bài báo Trees from Marginals đã giới thiệu Weaver nhằm khắc phục điểm yếu này. Weaver là một adapter tự hồi quy với 56,7M tham số trong thiết lập nghiên cứu của bài báo. Nó hoạt động trên danh sách các ứng viên thay vì thực hiện phép chiếu trên toàn bộ từ vựng.
Bộ draft DFlash song song trước tiên sẽ dự đoán đồng loạt các ứng viên tiềm năng cho từng vị trí trong tương lai. Sau đó, Weaver sẽ tuần tự lựa chọn từ các ứng viên này. Mỗi lựa chọn đều có thể phụ thuộc vào các lựa chọn trước đó.
Ví dụ trước đó giờ đây có thể trở thành "a gallon of milk". Các lựa chọn tiếp theo sẽ tuân theo lộ trình đã được định hình từ trước.
Weaver còn có thể duy trì nhiều nhánh suy luận mạch lạc cùng lúc. Một nhánh có thể chứa "a gallon of milk". Nhánh khác lại có thể chứa "a bottle of juice". Model chính sẽ xác thực toàn bộ cây dự đoán này và chấp nhận nhánh hợp lệ dài nhất.
Hình dạng của cây có thể thích ứng với prompt. Mã nguồn mang tính dự đoán cao có thể hỗ trợ các nhánh dài và hẹp. Viết lách tự do có thể cần các nhánh ngắn hơn và nhiều phương án thay thế hơn.
Báo cáo nghiên cứu cho thấy mức cải thiện 24,7% so với baseline DFlash đã qua tối ưu hóa. Báo cáo cũng ghi nhận tốc độ tăng tốc 4,37 lần so với autoregressive decoding. Các thử nghiệm đó sử dụng CUDA và SGLang. Đây là kết quả nghiên cứu, không phải kết quả đo lường phổ quát của Uzu.
Uzu áp dụng cùng ý tưởng draft này vào runtime dành cho Apple của mình. Vấn đề còn lại là xác thực cây một cách hiệu quả trên target model.
Verifying a draft tree without restoring model state (Xác thực cây draft mà không cần khôi phục trạng thái model)
Một số tầng của model duy trì một trạng thái nội bộ liên tục. Mỗi token mới sẽ đọc trạng thái này và cập nhật nó. Đối với một chuỗi đơn lẻ, runtime sẽ áp dụng các cập nhật này một cách tuần tự.
Cây draft tạo ra một số chuỗi tiếp nối khả dĩ. Mỗi nhánh sẽ cập nhật trạng thái theo một cách khác nhau. Nếu runtime cập nhật trạng thái thời gian thực khi kiểm tra một nhánh, nó phải khôi phục trạng thái trước đó trước khi kiểm tra nhánh khác. Việc lặp lại quy trình này trên một cây lớn sẽ làm tăng các thao tác bộ nhớ và công việc trùng lặp.
Qwen3.5 và Qwen3.6 chứa các tầng có trạng thái (stateful layer) gọi là tầng Gated DeltaNet. Tầng attention duy trì một bộ nhớ cache liên tục tăng dần cho từng token trong quá khứ. Trong khi đó, tầng Gated DeltaNet nén tất cả những gì nó nhìn thấy thành một trạng thái có kích thước cố định, và mỗi token được chấp nhận sẽ làm thay đổi trạng thái này.
Trong Qwen3.5 9B, có 24 trong số 32 tầng thuộc loại này.
Uzu tách biệt việc kiểm tra các nhánh với việc commit trạng thái của chúng. Trong quá trình xác thực, nó tính toán các giá trị cần thiết cho từng nhánh ứng viên mà không làm thay đổi trạng thái thời gian thực. Model chính sau đó sẽ chọn đường dẫn được chấp nhận. Uzu chỉ áp dụng cập nhật trạng thái cho các token được chấp nhận.
Lộ trình Ghi chú triển khai Uzu gọi phương pháp này là xác thực cây không cần rollback. Kernel Metal của nó tính toán output DeltaNet của toàn bộ cây trong một lượt duyệt đơn không vòng lặp, thay vì tiến hành từng nhánh, từng token một.
Sau đó, trạng thái chỉ được commit dọc theo đường dẫn đã chọn, do đó không bao giờ xuất hiện các cập nhật sai cần phải rollback.
Điều này rất quan trọng, vì speculative decoding chỉ có ý nghĩa khi chi phí xác thực thấp hơn chi phí giải mã thông thường từng token một. Việc khôi phục trạng thái lặp đi lặp lại sẽ làm hao hụt một phần hiệu quả thu được.
Sự kết hợp giữa Weaver và cơ chế xác thực không cần rollback giúp Uzu có thể đề xuất các chuỗi token dài và kiểm tra chúng mà không cần liên tục khôi phục trạng thái model.
What this looks like on a base M5 (Hiệu năng thực tế trên chip M5 bản tiêu chuẩn)
Để so sánh sự khác biệt giữa giải mã thông thường, speculative decoding truyền thống và phương pháp của Uzu, chúng tôi đã chạy cùng một model Qwen3.5 9B theo ba cách trên MacBook Pro trang bị chip M5 bản tiêu chuẩn và 16 GB unified memory:
- llama.cpp, không dùng speculative decoding
- llama.cpp, sử dụng MTP head tích hợp của model để draft trước 3 token, tức cấu hình truyền thống
- Uzu, kết hợp với DFlash và Weaver
Cả ba đều sử dụng trọng số 4-bit và cùng một prompt: "Viết một hàm Python để hợp nhất hai danh sách đã sắp xếp."
Published benchmark comparisons (So sánh benchmark đã công bố)
Hiện đã có một số benchmark công khai cho Uzu, dùng để đo lường tốc độ đầu ra, tốc độ đầu vào, dung lượng bộ nhớ sử dụng và chất lượng quantization.
Kết quả speculative decoding dưới đây sử dụng Qwen3.6 27B Mirai-M . Checkpoint này chiếm 15,6 GB. Mirai đã thực hiện đo lường trên máy M5 Max trang bị 128 GB bộ nhớ.
Những số liệu này cho thấy tốc độ của Uzu đạt:
- Gấp 2,1 lần MTPLX
- Gấp 3,8 lần llama.cpp
- Gấp 4,4 lần MLX.
Ngoài ra, khi không dùng speculative decoding, Uzu đạt tốc độ 35,54 token/giây. Kết quả speculative decoding của nó đạt tới 114 token/giây, đồng nghĩa với việc nhánh speculative mang lại mức tăng tốc gấp 3,2 lần.
Ngoài ra còn có kết quả tốc độ tạo đầu ra được phân loại theo loại prompt.
Mã nguồn và toán học chứa rất nhiều nội dung tiếp nối có thể dự đoán được. Trò chuyện mở mang lại cho drafter ít token đáng tin cậy hơn. Vì vậy, tốc độ speculative là một đặc tính của khối lượng công việc (workload), chứ không phải là tham số cố định của model.
Run a model locally and inspect the runtime (Chạy model cục bộ và kiểm tra runtime)
Cách nhanh nhất để trải nghiệm Uzu là sử dụng công cụ dòng lệnh (CLI) của nó. Công cụ này được cài đặt qua Homebrew và sẽ tải xuống model đã chọn trong lần sử dụng đầu tiên.
brew install miraimirai --model trymirai/Qwen3.5-9B-M \ -m "Write a python function to merge two sorted lists"
Chúng tôi đã thử nghiệm bằng Mirai CLI 0.6.1 trên MacBook Pro trang bị chip M5 bản tiêu chuẩn, 16 GB unified memory, chạy macOS 26.6.2. Lần chạy đầu tiên sẽ tải xuống model.
Dưới đây là quá trình chạy thực tế:
Sau khi xuất ra câu trả lời, CLI đã in các thông tin thống kê sau:
time to first token: 0.12 sprefill speed: 161.11 t/sgeneration speed: 98.10 t/stokens per forward pass: 8.39 t/fmemory used: 5.77 GBtotal energy: 114.90 Jinput energy per token: 0.11 J/tokoutput energy per token: 0.16 J/tokduration: 7.36 s
Ý nghĩa của các con số này như sau:
- Thời gian đến token đầu tiên là khoảng thời gian chờ trước khi token đầu tiên của câu trả lời xuất hiện, phần lớn thời gian này dùng để đọc prompt.
- Tốc độ prefill là tốc độ engine đọc prompt, tính bằng số token prompt xử lý được mỗi giây. prompt của chúng tôi chỉ có khoảng 20 token, quá ngắn nên con số này không nói lên nhiều điều. prompt càng dài thì giá trị này sẽ càng cao.
- Tốc độ tạo là tốc độ ghi câu trả lời, tính bằng số token xuất ra mỗi giây. Đây chính là giai đoạn decoding đã đề cập trước đó, bị giới hạn bởi băng thông bộ nhớ.
- Số token trên mỗi lượt forward pass là số token đầu ra trung bình mà model 9B tạo ra trong mỗi lượt forward pass. Giải mã thông thường đạt chính xác 1,00.
- Dung lượng bộ nhớ sử dụng là lượng unified memory mà engine chiếm dụng, chủ yếu là trọng số model cộng với drafter và cache.
Con số 8,39 t/f phản ánh tác dụng của cơ chế speculative. Trung bình mỗi lượt forward pass của model 9B tạo ra khoảng 8 token. Đây là lý do tại sao trên một con chip bị giới hạn khoảng 29 token đơn lẻ, tốc độ tạo lại có thể đạt tới 98 tok/s.
Tốc độ giữa các lần chạy khác nhau có sự chênh lệch nhỏ. Trong nhiều lần chúng tôi chạy lệnh này, tốc độ tạo dao động từ 92 đến 117 tok/s, mỗi lượt forward pass tạo ra từ 7,5 đến 9,6 token.
Speculative được bật tùy theo chip. Cấu hình drafter của checkpoint này liệt kê M5, M5 Pro và M5 Max, do đó trên các máy Mac từ M1 đến M4, cùng lệnh này sẽ chạy mà không có drafter và báo cáo 1,00 t/f.
CLI sẽ xuất ra dữ liệu về độ trễ, throughput, bộ nhớ, mức tiêu thụ năng lượng và speculative. Giờ đây chúng ta có thể gọi cùng một runtime này từ ứng dụng.
Call Uzu through a local application endpoint (Gọi Uzu qua endpoint ứng dụng cục bộ)
Uzu có thể cung cấp một HTTP endpoint tương thích với OpenAI. Model vẫn chạy trên máy Mac của người dùng, và các yêu cầu HTTP chỉ đi qua giao diện loopback cục bộ.
Khởi động server trong một terminal:
mirai server --model trymirai/Qwen3.5-9B-M --port 8000 --no-prefix-cache
Server tải một checkpoint cục bộ và lắng nghe trên 127.0.0.1, các tiến trình trên cùng máy Mac có thể gọi đến nó. Điều này rất hữu ích cho các ứng dụng hiện có vốn đã tương tác qua giao thức trò chuyện của OpenAI.
Theo mặc định, Uzu giữ lại prefix cache. Nó lưu trữ phần mở đầu đã được xử lý của các prompt trước đó, vì vậy các prompt lặp lại có thể bỏ qua phần lớn giai đoạn prefill. Tuỳ chọn --no-prefix-cache có thể tắt tính năng này để mỗi request thử nghiệm đều thực hiện toàn bộ công việc, giúp các lần chạy lặp lại duy trì khả năng so sánh chuẩn xác.
Các ứng dụng hiện có có thể chuyển sang Uzu bằng cách trỏ OpenAI client tới http://127.0.0.1:8000/v1. Phần tiếp theo sẽ sử dụng một client nhỏ để đo thời gian của Uzu, MLX và llama.cpp theo cách hoàn toàn giống nhau.
Sử dụng cùng một client cho Uzu, MLX và llama.cpp
Dưới đây là cùng một prompt được chạy qua từng engine trên chip M5 bản tiêu chuẩn. Mỗi engine chạy độc lập và không có tác vụ nào khác trên máy.
Cả llama.cpp và MLX đều thấp hơn giới hạn 29 token/giây, đúng như dự kiến khi mỗi lượt chỉ tạo ra một token. Trong lần ghi nhận này, tốc độ ghi của Uzu trên cùng con chip nhanh gấp khoảng 3,7 lần so với MLX.
Để thực hiện so sánh này bằng prompt của riêng bạn, hãy khởi động từng engine dưới dạng server cục bộ và dùng một client duy nhất để đo thời gian cho cả ba.
Giao diện server cục bộ mang đến cho chúng ta một phương pháp so sánh thiết thực.
Uzu, MLX LM và llama.cpp đều có thể cung cấp endpoint trò chuyện theo phong cách OpenAI. Chúng ta có thể chạy Qwen3.5 9B trên cả ba engine và gửi cùng một prompt đến từng server.
Khởi động Uzu bằng checkpoint 4-bit Medium của Mirai:
mirai server --model trymirai/Qwen3.5-9B-M --port 8000
Khởi động MLX LM bằng bản chuyển đổi MLX 4-bit của cùng model nền tảng:
brew install uvuv tool install mlx-lmuv tool update-shell
uv tool update-shell sẽ thêm thư mục chứa mlx_lm.server vào PATH trong shell của bạn.
Sau khi chạy, hãy mở một terminal mới rồi khởi động server:
mlx_lm.server --model mlx-community/Qwen3.5-9B-4bit --port 8001
Khởi động llama.cpp bằng checkpoint GGUF Q4_K_S từng dùng trong so sánh đã công bố của Mirai:
brew install llama.cppllama-server --versionllama-server \ -hf unsloth/Qwen3.5-9B-MTP-GGUF:Q4_K_S \ --alias qwen3.5-9b-q4-k-s \ -ngl 99 -fa on -c 8192 -np 1 \ --port 8002
-c 8192 giúp ngăn llama.cpp cấp phát toàn bộ context 262K của model, vì dung lượng này không thể cùng tồn tại với trọng số trên máy Mac 16 GB. -np 1 để chạy một slot đơn lẻ. Để bật tính năng MTP draft của llama.cpp như trong video đầu tiên, hãy thêm --spec-type draft-mtp --spec-draft-n-max 3 . MTP yêu cầu -np 1 .
Mỗi server sẽ tải về checkpoint tương ứng trong lần đầu khởi động. Vui lòng chỉ chạy từng server một tại mỗi thời điểm, để trong suốt quá trình đo lường chỉ có duy nhất một bản sao Qwen3.5 9B chiếm dụng unified memory.
Ba checkpoint này đều xuất phát từ cùng một model Qwen3.5 9B, nhưng chúng không dùng chung định dạng file hay công thức lượng tử hóa. Uzu sử dụng Mirai-M, MLX sử dụng bản chuyển đổi 4-bit của riêng mình, còn llama.cpp sử dụng Q4_K_S. Do đó, đây là phép so sánh toàn bộ engine path mà developer có thể chạy, chứ không chỉ là bài test riêng cho kernel.
Chạy benchmark client tới các cổng 8000, 8001 và 8002, mỗi lần chỉ chạy với một server.
Công cụ kiểm thử cần giữ cố định nội dung prompt, giới hạn output, cài đặt sampling và số lượt chạy. Hãy sử dụng chất lượng lượng tử hóa tương đương, chứ không chỉ đơn thuần là độ rộng bit tương tự. Bên cạnh thời gian client end-to-end, hãy ghi lại các chỉ số gốc từ engine.
Cuối cùng, đây là một client độc lập. Hãy lưu file với tên compare_client.py. Script này chỉ sử dụng thư viện chuẩn của Python và gửi cùng một prompt mang tính xác định (deterministic) tới bất kỳ endpoint local nào tương thích với OpenAI.
Nó tắt tính năng suy nghĩ để mỗi engine trả lời trực tiếp, đồng thời truy vấn lượng token sử dụng của từng server. Nó báo cáo thời gian nhận token đầu tiên, thời gian end-to-end và tốc độ decoding.
Uzu cũng báo cáo số pass chạy target model, vì vậy đối với Uzu, script cũng sẽ in ra số token trên mỗi pass.
import json, os, sys, time, urllib.requestPROMPT = "Write a Python function that merges two sorted lists."body = json.dumps({ "model": os.environ["MODEL"], "messages": [{"role": "user", "content": PROMPT}], "temperature": 0, # same answer every run "max_tokens": 1024, "stream": True, # tokens arrive as they are made "stream_options": {"include_usage": True}, # server reports token counts "chat_template_kwargs": {"enable_thinking": False}, # answer directly, no thinking}).encode()request = urllib.request.Request(os.environ["ENDPOINT"], body, {"Content-Type": "application/json"})start = time.perf_counter()first = last = Noneusage = {}with urllib.request.urlopen(request, timeout=600) as response: for line in response: line = line.decode().strip() if not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break event = json.loads(data) usage = event.get("usage") or usage for choice in event.get("choices") or []: delta = choice.get("delta") or {} text = delta.get("content") or "" if text or delta.get("reasoning_content") or delta.get("reasoning"): last = time.perf_counter() first = first or last print(text, end="", flush=True)if first is None: sys.exit("No tokens received. Check the server log.")tokens = usage.get("completion_tokens", 0)print(f"\n\ntime to first token: {first - start:.2f} s")print(f"end-to-end time: {time.perf_counter() - start:.2f} s")if tokens > 1 and last > first: print(f"decode speed: {(tokens - 1) / (last - first):.1f} tok/s")if usage.get("spec_verify_ct"): # only Uzu reports how many model passes it ran print(f"tokens per pass: {tokens / usage['spec_verify_ct']:.2f}")
Chạy từng engine một
Chỉ chạy một server tại một thời điểm, để chỉ có duy nhất một bản sao model nằm trong unified memory và hai engine không phải tranh chấp băng thông bộ nhớ.
Đối với mỗi engine, hãy khởi động server trong một terminal, sau đó chạy client hai lần trong terminal thứ hai. Lần chạy đầu tiên bao gồm quá trình warmup, vì vậy hãy bỏ qua kết quả đó và giữ lại lần thứ hai. Trước khi tiếp tục, hãy nhấn Ctrl-C để dừng server.
Đối với Uzu, hãy khởi động server trong terminal đầu tiên:
mirai server \ --model trymirai/Qwen3.5-9B-M \ --port 8000 \ --no-prefix-cache
Chạy client từ terminal thứ hai:
ENDPOINT=http://127.0.0.1:8000/v1/chat/completions \MODEL=trymirai/Qwen3.5-9B-M \python3 compare_client.py
Chạy client hai lần. Bỏ qua kết quả đầu tiên vì nó bao gồm giai đoạn warmup. Giữ lại kết quả thứ hai cùng các thống kê decoding do Uzu server in ra. Sau đó nhấn Ctrl-C để dừng Uzu.
Khởi động MLX trong terminal đầu tiên:
mlx_lm.server \ --model mlx-community/Qwen3.5-9B-4bit \ --port 8001
Chạy client tương tự từ terminal thứ hai:
ENDPOINT=http://127.0.0.1:8001/v1/chat/completions \MODEL=mlx-community/Qwen3.5-9B-4bit \python3 compare_client.py
Tương tự, hãy bỏ qua lần chạy đầu tiên và giữ lại lần thứ hai. Dùng Ctrl-C để dừng MLX.
Khởi động llama.cpp trong terminal đầu tiên:
llama-server \ -hf unsloth/Qwen3.5-9B-MTP-GGUF:Q4_K_S \ --alias qwen3.5-9b-q4-k-s \ -ngl 99 -fa on -c 8192 -np 1 \ --port 8002
Chạy client từ terminal thứ hai:
ENDPOINT=http://127.0.0.1:8002/v1/chat/completions \MODEL=qwen3.5-9b-q4-k-s \python3 compare_client.py
Bỏ qua lần chạy đầu tiên và giữ lại lần thứ hai. Phép so sánh cần giữ cố định các thiết lập sau:
- Máy Mac và chế độ nguồn
- prompt và chat template
- Giới hạn token đầu ra
- Cài đặt temperature và sampling
- Số lần warmup
- Số lượng request đo lường
Tốc độ decoding ở client bao gồm cả truyền phát qua HTTP, do đó dự kiến sẽ thấp hơn một chút so với tốc độ mà mỗi engine in ra trong log của nó. Hãy so sánh các con số phía client với nhau, chứ không phải so với con số trên CLI.
Mirai CLI không có cờ bật/tắt để vô hiệu hóa speculation, do đó quy trình này so sánh toàn bộ engine path thay vì cô lập riêng DFlash-Weaver.
Trong video đầu tiên, việc llama.cpp chạy khi bật và không bật MTP là góc nhìn cùng một engine sát nhất để quan sát hiệu quả của speculative decoding.
Kết luận
Tải checkpoint đã lượng tử hóa chỉ là bước đầu tiên của suy luận local.
Thách thức thực sự nằm ở việc duy trì tính thực tế và hiệu quả của việc sinh nội dung khi mỗi request đều chạy với batch size bằng 1 ngay trên thiết bị của người dùng.
Uzu xử lý vấn đề này ở nhiều cấp độ khác nhau.
- Lượng tử hóa giúp giảm lưu lượng bộ nhớ.
- Metal kernel của nó được tối ưu hóa riêng cho phần cứng Apple.
- DFlash-Weaver đề xuất chuỗi token dài hơn, trong khi cơ chế xác thực không cần rollback sẽ kiểm tra các chuỗi này mà không phải liên tục khôi phục trạng thái lặp.
Trên chip M5 cơ bản, sự kết hợp này đã nâng tốc độ của model 9B 4-bit từ 22 token/giây khi dùng llama.cpp thông thường lên 92 token/giây khi dùng Uzu xử lý cùng một prompt.
Tỷ lệ chấp nhận thay đổi tùy theo workload. Phần cứng, chất lượng lượng tử hóa, độ dài context và trạng thái nhiệt cũng ảnh hưởng đến kết quả. Một thử nghiệm đúng đắn nên sử dụng chính prompt của ứng dụng và đo lường đồng thời cả chất lượng lẫn độ trễ.
Nếu bạn muốn tìm hiểu sâu hơn, Repository Uzu chứa engine, local server, hướng dẫn build cùng binding cho Rust, Swift, Python và TypeScript.
Hãy bắt đầu từ local server, chạy benchmark client trong bài viết này và thay thế prompt mẫu bằng các request thực tế trong sản phẩm của bạn.
git clone https://github.com/trymirai/uzu.git
Tìm Uzu trên GitHub →
(Đừng quên bấm star nhé)
Chúc bạn có những trải nghiệm tuyệt vời!
Học tương tác với AI (NotebookLM)
Ngoài đọc , bạn có thể nghe , xem và xem hoạ thông tin ở đầu bài. Bài này còn có bộ công cụ tương tác do AI tạo: Bài kiểm tra trắc nghiệm, Thẻ ghi nhớ, Sơ đồ tư duy, Bản trình bày và Báo cáo chuyên sâu.
Mở không gian học tương tác