- GGUF là định dạng tệp mô hình ngôn ngữ được llama.cpp sử dụng, gói siêu dữ liệu cần cho việc chạy vào một tệp duy nhất để đơn giản hóa việc phân phối và nạp mô hình
- Mẫu chat là các script Jinja2 xử lý định dạng hội thoại, gọi công cụ và mã hóa thông điệp đa phương tiện, nhưng hành vi vẫn khác nhau tùy từng triển khai
- GGUF có thể chứa token đặc biệt như token kết thúc và các thiết lập sampler được khuyến nghị, gần đây còn có thể chỉ rõ thứ tự chuỗi sampler
- Hiện định dạng gọi công cụ vẫn khác nhau theo từng mô hình nên các engine suy luận vẫn phải hardcode theo từng trường hợp, và việc sinh parser dựa trên ngữ pháp vẫn là ứng viên cần cải thiện trong chuẩn
- Vẫn còn thiếu
think_token, khả năng đóng gói mô hình projection và các cờ tính năng, nên việc tách phần suy nghĩ, cấu hình đa phương thức và phát hiện tính năng hỗ trợ vẫn còn khó khăn
GGUF chứa gì
- GGUF là định dạng tệp mà llama.cpp dùng cho các mô hình ngôn ngữ
- Ưu điểm cốt lõi của GGUF là đưa nhiều thành phần cần thiết để chạy mô hình vào một tệp duy nhất
- GGUF đưa các thông tin bổ sung này vào một tệp để giúp việc xử lý mô hình dễ dàng hơn
Mẫu chat
- Các mô hình ngôn ngữ hội thoại được huấn luyện trên một chuỗi token có định dạng cụ thể, và định dạng này trông giống cấu trúc hội thoại
- Ví dụ về định dạng Gemma4 như sau
<|turn>user
Hi there!<turn|>
<|turn>model
Hi there, how can I help you today?<turn|>
- Ví dụ về mẫu định dạng LFM2 như sau
<s>
<|im_start|>user Hi there!<|im_end|>
<|im_start|>assistant Hi there, how can I help you today?<|im_end|>
- Trong thực tế, mẫu này trở nên phức tạp hơn nhiều khi còn bao gồm khối suy luận, mô tả công cụ, gọi công cụ và phản hồi, cũng như mã hóa thông điệp đa phương tiện như hình ảnh, âm thanh và video
- Việc này do mẫu chat đảm nhiệm, là các script viết bằng ngôn ngữ mẫu Jinja2
- Một mô hình có thể có nhiều mẫu chat
- Có thể có mẫu hỗ trợ gọi công cụ và mẫu không hỗ trợ gọi công cụ tách biệt
- Phần lớn mô hình cung cấp một mẫu chat lớn duy nhất và chỉ xử lý phần gọi công cụ khi công cụ được chỉ định
- Với một số mô hình, cần tìm riêng một mẫu chat chuyên cho công cụ
- Jinja2 gần như là một ngôn ngữ lập trình với vòng lặp, câu điều kiện, gán, danh sách, từ điển, v.v.
- Ứng dụng LLM hội thoại phải kèm theo một trình thông dịch để chạy chương trình như script Jinja khoảng 250 dòng mà Gemma cung cấp mỗi khi có tin nhắn mới được thêm vào
- Cách xử lý Jinja cũng khác nhau giữa các triển khai
- Hugging Face transformers dùng thư viện jinja2 có sẵn của Python
llama-server và llama-cli của llama.cpp dùng triển khai Jinja riêng
- llama_chat_apply_template được lộ ra qua API
libllama là cách cũ, hardcode trực tiếp một số định dạng chat bằng C++
- NobodyWho dùng minijinja, bản Jinja do chính tác giả gốc tái triển khai bằng Rust
- Đây không phải minja, thư viện Jinja tối giản mà llama.cpp từng dùng
- Có khác biệt hiệu năng đáng kể giữa các triển khai Jinja
- Tuy vậy, xử lý mẫu chat không phải nút thắt hiệu năng trong ứng dụng LLM cục bộ nên đây không phải chủ đề gây tranh cãi lớn
Token đặc biệt
- Mô hình ngôn ngữ có thể tiếp tục xuất token kế tiếp cho chuỗi token đầu vào, nên cần có cách dừng quá trình sinh
- Cách phổ biến là dùng token kết thúc, và khi mô hình xuất token này thì engine suy luận sẽ dừng sinh
- Token kết thúc là một ví dụ của token đặc biệt
- Token đặc biệt thường mang ý nghĩa vượt quá các ký tự đã được token hóa thông thường
- Thường không nên hiển thị cho người dùng, nhưng nhiều token vẫn có biểu diễn văn bản nên về mặt kỹ thuật vẫn có thể hiển thị
- Một số token đặc biệt của Gemma4 như sau
1 / <eos>: kết thúc chuỗi, mô hình xuất ra để dừng sinh
2 / <bos>: bắt đầu chuỗi, được thêm vào trước đầu vào
46 / <|tool_call>: đánh dấu bắt đầu một lời gọi công cụ
47 / <tool_call|>: đánh dấu kết thúc một lời gọi công cụ
105 / <|turn>: đánh dấu bắt đầu một lượt hội thoại
106 / <turn|>: đánh dấu kết thúc một lượt hội thoại
Thiết lập sampler và thứ tự
- Mô hình ngôn ngữ xuất ra phân phối xác suất của token tiếp theo, và quá trình chọn token từ phân phối này được gọi là sampling
- Cách đơn giản nhất là chọn ngẫu nhiên theo phân phối đã gán trọng số
- Trên thực tế, thường cho kết quả tốt hơn nếu áp dụng các phép biến đổi lên phân phối xác suất trước khi chọn token cụ thể
- Khi các phòng nghiên cứu phát hành mô hình mới, họ thường đi kèm các thiết lập sampler được khuyến nghị
- Người dùng cũng thường sao chép dán các giá trị từ tệp Markdown hoặc nơi khác để có phản hồi tốt hơn
- Để giảm việc sao chép thủ công của người dùng, NobodyWho đã đưa các mô hình tuyển chọn lên trang Hugging Face và đóng gói thiết lập sampler khuyến nghị theo định dạng riêng
- Cách này hoạt động, nhưng để mô hình thực sự hữu ích thì vẫn cần bước chuyển đổi phía NobodyWho
- Nhờ tính năng mới được thêm gần đây vào định dạng GGUF, giờ đây có thể chỉ rõ trực tiếp chuỗi sampler bên trong tệp mô hình
- Vì vậy định dạng riêng của NobodyWho không còn cần thiết nữa, và đó chính là kết quả mong muốn
- Trong ứng dụng web llm-sampling, có thể nhanh chóng xem vai trò của các bước sampler khác nhau
- Nếu kéo thả từng bước riêng lẻ, có thể thấy thứ tự các bước sampling tạo ra khác biệt lớn đối với phân phối cuối cùng
- Nhiều định dạng cấu hình sampler, bao gồm tệp JSON trong image Ollama hay
generation_config.json của Hugging Face, không có cách chỉ rõ thứ tự các bước sampling
- Chuẩn GGUF có thể chỉ định thứ tự sampling bằng trường
general.sampling.sequence
- Dù vậy, nhiều mô hình GGUF vẫn bỏ qua trường này và dựa vào thứ tự ngầm định là hành vi mặc định của llama.cpp
Những gì vẫn còn thiếu
- Một engine suy luận tốt cố gắng cung cấp giao diện thống nhất cho nhiều mô hình ngôn ngữ khác nhau
- Nếu parse và tận dụng thông tin bổ sung trong siêu dữ liệu GGUF, có thể giảm đáng kể các nhánh mã riêng theo từng mô hình
-
Định dạng gọi công cụ
- Gần như mọi engine suy luận đều có các nhánh hardcode để parse các định dạng gọi công cụ khác nhau
- Ví dụ về định dạng gọi công cụ của Qwen3 như sau
<tool_call>{"name": "get_weather", "arguments": {"location": "Copenhagen"}}</tool_call>
- Ví dụ về định dạng gọi công cụ của Qwen3.5 như sau
<tool_call>
<function=get_weather>
<parameter=city>
Copenhagen
</parameter>
</function>
</tool_call>
- Ví dụ về định dạng gọi công cụ của Gemma4 như sau
<|tool_call>call:get_weather{city:<|"|>Copenhagen<|"|>}<tool_call|>
- Khi có mô hình mới xuất hiện, nhiều engine suy luận lại phải tự triển khai parser riêng
- Nếu tệp mô hình có thể chứa ngữ pháp (grammar) và từ đó suy ra parser, đó sẽ là một bổ sung rất giá trị cho chuẩn GGUF
- NobodyWho còn thêm một bước tạo ngữ pháp ràng buộc phù hợp với công cụ cụ thể được truyền vào
- Nhờ đó có thể bảo đảm tính an toàn kiểu cho lời gọi công cụ
- Điều này đặc biệt hữu ích với các mô hình nhỏ cỡ 1B trở xuống, vốn có thể mắc lỗi như truyền float vào chỗ cần integer
- Ngay cả khi đã có ngữ pháp để tạo parser gọi công cụ tổng quát, NobodyWho vẫn cần tiếp tục triển khai hàm sinh ngữ pháp cho từng công cụ cụ thể được truyền vào
- Một định dạng meta-grammar có thể tạo ra ngữ pháp cụ thể cho từng công cụ rồi suy ra parser từ đó vẫn là một bài toán thú vị
-
Token Think
- Đây là phần còn thiếu dễ bổ sung nhất
- Kho Hugging Face upstream đã bắt đầu thêm trường
think_token
think_token rất hữu ích để tách phần suy nghĩ khỏi đầu ra được sinh ra
- Phần suy nghĩ thường nên bị loại bỏ hoặc được render khác với phần nội dung chính
- Bản chuyển đổi GGUF downstream thường không bao gồm trường này
- Kết quả là các engine suy luận dựa trên GGUF không thể tách luồng suy nghĩ ra khỏi đầu ra chính nếu không viết mã riêng cho từng họ mô hình
- Thêm
think_token vào pipeline chuyển đổi GGUF tiêu chuẩn sẽ giải quyết được vấn đề này
-
Mô hình projection
- Tương tác với LLM đa phương thức cho phép LLM nhìn trực tiếp hình ảnh và âm thanh thay vì văn bản đòi hỏi thêm một mô hình để xử lý đầu vào phi văn bản
- Mô hình bổ sung này được gọi là mô hình projection
- Hiện nay, cách làm phổ biến là truyền vào hai tệp GGUF
- Một GGUF cho mô hình ngôn ngữ chính
- Một mô hình nhỏ hơn khác để xử lý hình ảnh và âm thanh
- Cách này phá vỡ sự tiện lợi một-tệp của GGUF
- Sẽ là cải tiến lớn nếu một tệp GGUF duy nhất có thể đóng gói cùng lúc trọng số và cấu hình của mô hình projection bên trong tệp chính
- Mô hình projection thường có kích thước khoảng 1GB
- Đây là mức overhead đủ lớn để muốn tránh nếu không sử dụng
- Cung cấp hai biến thể GGUF, một bản có trọng số projection và một bản không có, là cách hợp lý
- Như vậy có thể quay lại trạng thái chỉ cần quản lý một URL để tải xuống và một tệp để cache trên đĩa
-
Danh sách tính năng hỗ trợ
- Mỗi mô hình hỗ trợ các tính năng khác nhau, và chỉ nhìn vào tệp GGUF thì rất khó phát hiện chính xác các tính năng được hỗ trợ
- Một số mô hình hỗ trợ đầu vào hình ảnh, một số thì không
- Cách xử lý tốt nhất hiện nay là giả định có hỗ trợ hình ảnh nếu có truyền mô hình projection vào
- Một số mô hình hỗ trợ gọi công cụ native, một số thì không
- Cách xử lý tốt nhất hiện nay là kiểm tra bằng so khớp chuỗi xem mẫu chat có đoạn nào cố render danh sách JSON schema của công cụ hay không
- Rõ ràng đây chỉ là giải pháp tạm bợ
- Một số mô hình xuất khối suy nghĩ, một số thì không
- Vì tag suy nghĩ thường không có trong siêu dữ liệu GGUF, hiện không rõ cách tốt nào để biết có nên kỳ vọng mô hình sẽ xuất khối suy nghĩ hay không
- Nếu cộng đồng GGUF thêm cờ tính năng vào tệp mô hình, các thư viện suy luận không phụ thuộc mô hình sẽ có thể đưa ra thông báo lỗi và cảnh báo nhất quán hơn
- Ví dụ, có thể đưa ra hướng dẫn phù hợp hơn khi người dùng cố gọi công cụ với mô hình không hỗ trợ gọi công cụ native
Kết luận
- GGUF đưa các thông tin bổ sung cần thiết để chạy mô hình đúng cách vào một tệp duy nhất, giúp không phải thêm quá nhiều nhánh mã riêng theo từng mô hình
- GGUF là định dạng cởi mở, có thể mở rộng và có cộng đồng mạnh
- Nếu cùng nhau cải thiện chuẩn này, có thể vừa duy trì trải nghiệm lập trình viên tốt vừa giúp ứng dụng thay mô hình dễ dàng hơn
- Siêu dữ liệu GGUF đã rất hữu ích, nhưng vẫn còn chỗ để cải thiện như ngữ pháp gọi công cụ,
think_token, đóng gói mô hình projection và cờ tính năng
1 bình luận
Ý kiến trên Hacker News
Thật tiếc khi mô hình chiếu bị tách thành một tệp riêng, và tôi cũng muốn nó nằm trong một tệp duy nhất
Tôi không rõ chính xác vì sao lại thành ra như vậy, nhưng điều đó khá lệch với triết lý một tệp duy nhất được đặt ra khi thiết kế GGUF
Hy vọng sẽ có ai đó dẫn dắt việc hợp nhất hai thứ này, còn lần này thì có vẻ tôi đang đứng hơi ngoài luồng :-)
Tôi thích quyết định đó. Vì vậy, tôi nghĩ cũng không quá gượng ép khi cho rằng vẫn có khả năng cởi mở với việc đưa tệp Mmproj vào trong GGUF
Vấn đề duy nhất tôi nghĩ ra là sẽ đưa vào định dạng nào. Có các lựa chọn như BF16, F16, v.v.
GGML và GGUF đã cực kỳ quan trọng với hệ sinh thái máy học/AI mã nguồn mở
Những dự án như llama.cpp, whisper.cpp, stable-diffusion.cpp nhìn chung chạy tốt ngay trên nhiều nền tảng và backend phần cứng khác nhau
Chỉ cần biên dịch, nạp mô hình vào và chạy. Thế là có luôn web UI và API
> <|turn>user Hi there!<|turn>model Hi there, how can I help you todayTrời ơi, họ còn tạo ra được một định dạng khó đọc hơn cả XML
Định dạng này được thiết kế để không bị nhầm với nội dung thực, mà nội dung đó có thể là bất kỳ văn bản nào từ Internet
Muốn vậy thì phải dùng một định dạng không xuất hiện ở nơi khác
Theo tôi, thứ còn thiếu lớn nhất hiện nay là cách định nghĩa kiến trúc mô hình mà không hardcode vào bản build hiện tại
Không nhất thiết phải đạt mức tương đương hiệu năng 1:1 với các mô hình được hỗ trợ đầy đủ
Việc có hỗ trợ bài bản, được nhà cung cấp xác thực ngay từ ngày phát hành hay không là yếu tố quyết định việc người ta thấy mô hình đó tuyệt vời hay tệ hại. Các đợt phát hành Gemma và Qwen gần đây là ví dụ
Tôi không chắc lời giải là gì, nhưng có thể là viết một DSL mô tả đồ thị mô hình rồi đưa nó vào GGUF
Một phương án khác là đọc module PyTorch của bản phát hành mô hình chính thức rồi bằng cách nào đó chuyển đổi sang các phép toán GGML
Tôi muốn đưa nó vào bản đầu tiên, nhưng lúc đó ưu tiên là đưa ra một đặc tả tối thiểu khả dụng và làm cho nó được triển khai
Giờ tôi vẫn muốn thấy điều đó, nhưng cần một người dẫn dắt hiểu rất rõ tình trạng hiện tại của GGML IR
Sau đó có thể phơi ra một giao diện chung nhận các tham số phổ biến, còn các tham số tùy biến bổ sung thì để dưới dạng extension như Wayland
Như vậy sẽ hỗ trợ không chỉ họ transformer như LLaMa mà cả họ mạng nơ-ron hồi quy như RWKV, các mô hình đa phương thức, v.v.
Tôi không rõ triển khai thực tế sẽ ra sao, nhưng nghe là một ý tưởng hay. Dù vậy, tôi lo rằng nếu đồ thị tính toán bị đóng cứng trong tệp mô hình, thì những cải tiến kiến trúc hay tối ưu hóa không cần đổi trọng số sẽ không thể áp dụng cho các tệp cũ nếu không chuyển đổi
> Điểm thực sự gọn gàng của GGUF là nó là một tệp duy nhất. So với một kho safetensors điển hình trên Hugging Face, các tệp JSON cần thiết bị rải rác khắp nơi [...]Thú vị là với tôi, mô hình AI “lúc nào” cũng là một tệp duy nhất. Vì ở mảng tạo ảnh cục bộ thì đó là tiêu chuẩn
safetensors cũng có thể chứa đủ thứ bên trong, nên không nhất thiết phải cần GGUF cho việc này
Tuy nhiên, các text encoder của mô hình hiện đại bản thân chúng cũng là các mô hình ngôn ngữ nặng vài GB, nên không ai nhét bản sao trùng lặp của chúng vào mọi checkpoint cả
Phần lớn mô hình ảnh trước đây hoặc đến nay vẫn là một tệp duy nhất, nhưng safetensors của LLM, ít nhất vào thời điểm đó, thì không như vậy, và tôi muốn ép điều này ở cấp độ cấu trúc
Ngoài ra, tôi không muốn runtime như llama.cpp phải cần một bộ đọc JSON, trong khi cách làm ST có lẽ sẽ đòi hỏi điều đó
Vấn đề lớn hơn, nếu tôi nhớ không nhầm, là vào thời điểm đó ST không hỗ trợ các định dạng lượng tử hóa mới của GGML, và việc có định dạng tệp riêng cho phép đạt được mức linh hoạt mà ST khó có thể mang lại
Để thực sự chạy kiến trúc với trọng số, ngoài tệp trọng số đơn lẻ còn cần nhiều encoder và decoder khác nữa
Công cụ bạn dùng có thể che giấu điều đó, nhưng bên dưới bề mặt chúng vẫn tồn tại
Về
llama_chat_apply_template, cái APIlibllamahơi kỳ quặc khi phơi ra một số định dạng chat được hardcode trực tiếp trong C++, với tư cách là người đang mày mò một ứng dụng suy luận desktop bằng FLTK[0], tôi ước gì nó dùng chính trình phân tích template Jinja2 mà llama.cpp sử dụngHoặc ít nhất là có một hàm C khác làm việc đó. Để parse cho đúng thì có vẻ cần truyền vào nhiều loại dữ liệu, chẳng hạn để template biết liệu có đang dùng tool calling hay không
Hiện giờ tôi đang dùng hàm tạm bợ này, nhưng cuối cùng có lẽ tôi sẽ phải tự dùng một trình thông dịch Jinja2 hoặc chép mã từ llama.cpp sang
Dù vậy, cách tiếp cận all-in-one của GGUF rất tiện. Tôi đồng ý rằng việc mô hình chiếu là một tệp riêng tạo cảm giác kỳ lạ
Khi lần đầu tải một mô hình có hỗ trợ vision, tôi chỉ tải tệp GGUF có vẻ phù hợp, rồi llama.cpp báo không thể xử lý mô hình, và khá lâu sau tôi mới nhận ra cần thêm một tệp nữa
Lúc đó suy nghĩ của tôi đúng nghĩa là “GGUF chẳng phải là định dạng gói mọi thứ vào một chỗ sao?” :-P
[0] https://i.imgur.com/GiTBE1j.png
Tôi vẫn luôn dùng định dạng safetensors + tệp metadata tương tự như kho trên Hugging Face
Hoàn toàn không phải bất tiện lớn gì, nhưng việc GGUF có định dạng gọn hơn và hỗ trợ tốt là điều khá ổn
Chính việc nhìn vào những thứ GGUF còn thiếu lại khiến tôi hiểu GGUF hơn
Định dạng tool calling thực sự rất tự nhiên, và có vẻ sẽ là một cột mốc trên con đường từ LLM sang agent
Gần đây tôi tải 7B Mistral của TheBloke để thử nghiệm, và tôi có một chiếc 4070
Bạn nên thử Gemma 4 e4b. Nó có kích thước tương tự Mistral 7B và sẽ chạy tốt trên 4070
Cái tên “E4B” hơi dễ gây hiểu nhầm một chút
Trên 4070 12GB, bạn có thể chạy Qwen 3.5 9B q4km hoặc Qwen 3.6 35B. Bản sau thông minh hơn nhiều nhưng cũng chậm hơn nhiều vì phải offload bộ nhớ
Thử cả hai trong LM Studio đi, năng lực của chúng thực sự đáng ngạc nhiên
Tôi thích TheBloke, vẫn mong anh ấy tiếp tục làm model