llama: bổ sung lấy mẫu dựa trên ngữ pháp
(github.com/ggerganov)- PR #1773 của llama.cpp bổ sung API giới hạn các ứng viên lấy mẫu bằng ngữ pháp phi ngữ cảnh, nhằm bảo đảm kết quả sinh ra không lệch khỏi định dạng đã chỉ định
- API nhận cấu trúc dữ liệu ngữ pháp dựa trên code point 32-bit, lọc token ứng viên bằng
llama_sample_grammar, rồi phản ánh token đã chọn vào trạng thái ngữ pháp bằngllama_grammar_accept_token mainđược thêm các đối số--grammarvà--grammar-fileđể nhập ngữ pháp dạng BNF mở rộng; các ví dụ gồm Chess, biểu thức số học, JSON và sinh phạm vi ký tự tiếng Nhật- Các bài kiểm thử được thực hiện trên M2 Max và mô hình 30B Q4_0; khi áp dụng ngữ pháp, đầu ra bị giới hạn đúng theo ký pháp cờ vua, biểu thức số học, JSON và phạm vi ký tự tiếng Nhật, trong khi không có ngữ pháp thì mô hình sinh văn bản hoặc mã nguồn thông thường không khớp với prompt
- Phần thảo luận tập trung vào các ràng buộc khi dùng thực tế như hỗ trợ file ngữ pháp, xử lý dòng trống và chú thích, có nên đưa parser vào API, che logit bằng GPU, xử lý theo lô các token cố định và overhead hiệu năng
Thay đổi chính của PR
- Đây là PR bổ sung lấy mẫu dựa trên ngữ pháp cho llama.cpp
- Công việc này tham khảo phần triển khai trước đó là #1397 và công việc CFG của grantslatton
- API mới nhận ngữ pháp phi ngữ cảnh đã được tuần tự hóa để dẫn hướng và giới hạn quá trình lấy mẫu
- Trong
main, các ví dụ ngữ pháp kiểu BNF tương tự được thêm vào để chỉ định ngữ pháp sinh
Cấu trúc Grammar API
- API
llamanhận cấu trúc dữ liệu ngữ pháp phi ngữ cảnh cho các code point 32-bit - Các kiểu phần tử ngữ pháp biểu diễn kết thúc quy tắc, bắt đầu lựa chọn thay thế, tham chiếu quy tắc, ký tự, cận trên của phạm vi ký tự và thêm ký tự thay thế
- Hàm khởi tạo nhận các thông tin sau
- mảng quy tắc
- số lượng quy tắc
- chỉ số quy tắc bắt đầu
llama_grammar_elementcótypevàvalue, trong đóvalueđược dùng làm code point Unicode hoặc ID quy tắc
Cách lấy mẫu
- Mã lấy mẫu theo ngữ pháp mô hình hóa một ô tô mát đẩy xuống không tất định
- Nó duy trì N ngăn xếp để biểu diễn các trạng thái phân tích cú pháp có thể có
- Quá trình lấy mẫu token hoạt động theo hai bước
llama_sample_grammarchỉ giữ lại các ứng viên khớp với ít nhất một ngăn xếp phân tích trong số các token ứng viênllama_grammar_accept_tokenthêm token đã chọn vào trạng thái ngữ pháp
Tính năng nhập ngữ pháp trong main
mainđược thêm các đối số--grammarvà--grammar-file- Hai đối số này nhận ngữ pháp BNF mở rộng đơn giản để giới hạn kết quả sinh
- Bộ phân tích ngữ pháp được cài đặt trong
examples/grammar-parser.{h,cpp} - Các tính năng ngữ pháp được hỗ trợ gồm phạm vi ký tự, gom nhóm và toán tử lặp
- Quy tắc
rootxác định điểm bắt đầu của ngữ pháp - Các bản cập nhật sau đó bổ sung hỗ trợ file ngữ pháp, thêm ví dụ, chú thích kiểu shell, dòng trống giữa các quy tắc và cho phép xuống dòng trong nhóm ngoặc
Ví dụ kiểm thử
- Môi trường kiểm thử được nêu là mô hình M2 Max, 30B
- Ví dụ Chess
- Dùng
--grammar-file grammars/chess.gbnfsẽ sinh ra định dạng ký pháp cờ vua như1. e4 e5,2. Nf3 Nc6 - Nếu chạy cùng prompt mà không có ngữ pháp thì mô hình sinh ra câu văn thông thường liên quan đến Sir Thomas Gresham
- Dùng
- Ví dụ biểu thức số học
- Dùng ngữ pháp nội tuyến để ép theo dạng
expr "=" ws num "\n" - Kết quả bị giới hạn theo dạng biểu thức số học như
10 *a*1 +b*2 =640 - Nếu không có ngữ pháp thì đầu ra là một đoạn mã Go
- Dùng ngữ pháp nội tuyến để ép theo dạng
- Ví dụ JSON
- Dùng
grammars/json.gbnfsẽ sinh ra cấu trúc JSON dạng{ "fullName": ..., "address": ... } - Nếu không có ngữ pháp thì mô hình sinh văn xuôi kiểu tự giới thiệu
- Dùng
- Ví dụ tiếng Nhật
grammars/japanese.gbnfcho phép hiragana, katakana, dấu câu và phạm vi CJK- Khi áp dụng ngữ pháp, đầu ra là danh sách dựa trên ký tự tiếng Nhật
- Nếu không dùng ngữ pháp thì đầu ra là danh sách các bước bằng tiếng Anh
Thảo luận về review và thiết kế
- Có đề xuất nhận ngữ pháp từ file, và sau đó hỗ trợ grammar file cùng ví dụ đã được thêm vào
- Các vấn đề được phát hiện trong quá trình dùng ban đầu gồm xung đột với
--prompt-cachevà crash do dòng trống trong ngữ pháp - Hỗ trợ dòng trống và chú thích, cùng việc cho phép xuống dòng trong nhóm ngoặc, đã được đưa vào các commit tiếp theo
- Đã có thảo luận về việc có nên đưa parser vào API của llama.cpp hay không
- Nếu parser ở ngoài API, người dùng downstream sẽ phải sao chép parser để hỗ trợ tính năng này
- Tác giả muốn giảm thay đổi trong chính
llama.cpp, nhưng cũng đồng ý rằng đưa parser vào API sẽ tiện hơn
- Có đề xuất để
llama_grammargiữ một bản sao ngữ pháp nhị phân, nhờ đó người dùng không cần duy trì vòng đời của bản sao đã truyền vào
Thảo luận về hiệu năng và tối ưu hóa
- Có ý kiến cho rằng ngay cả khi
N > 1token tiếp theo được xác định duy nhất bởi ngữ pháp, cách hiện tại vẫn lấy mẫu từng token một - Việc đánh giá theo lô nhiều token cố định có thể tăng mạnh tốc độ suy luận trong những trường hợp như vậy
- Tác giả trả lời rằng các token vẫn cần được đánh giá và nút thắt có vẻ nằm ở phần đánh giá; họ cho rằng đánh giá theo lô ở mức chuỗi có thể là một điểm tối ưu hóa
- Cũng có ý tưởng biên dịch ngữ pháp trên GPU thành tensor chuyển trạng thái hoặc che logit trên GPU cho toàn bộ tập token
torch-grammarđược nhắc đến như một cách tiếp cận tương tự, thực hiện che logit bắt buộc theo ngữ pháp trên GPU cho toàn bộ tập token
Quan sát về hiệu năng
- Tác giả cho biết mới chỉ thử suy luận trên CPU và trong phạm vi họ thử thì ảnh hưởng hiệu năng không lớn
- Các con số quan sát trên M2 Max
- Lấy mẫu không ràng buộc khoảng 0.5ms/token
- Lấy mẫu có áp dụng ngữ pháp khoảng 6ms/token
- Đánh giá token của 13B Q4_K khoảng 70ms/token
- Một người dùng khác báo cáo trường hợp 13B giảm từ khoảng 20T/s xuống 13T/s
- Tác giả cho biết họ thường thấy overhead ngữ pháp vào khoảng 5ms/token, nhưng với một số ngữ pháp cụ thể tác động lớn hơn và có thể tồn tại các trường hợp xấu
Thảo luận về các mở rộng liên quan
- Có nhắc đến hướng dùng đầu vào JSON Schema rồi chuyển sang BNF
- Tác giả cho biết họ có một nhánh cục bộ hoạt động theo ví dụ trong README của jsonformer, và dùng script Python để sinh JSON BNF phù hợp với JSON Schema
- Sau đó,
examples : generate JSON according to schema #1887được nhắc đến như phần liên kết với PR này - Cách tiếp cận này được trả lời là độc lập với biến thể mô hình và cũng có thể dùng cùng các mô hình fine-tune
- Một PR bổ sung lấy mẫu dựa trên ngữ pháp cho whisper.cpp cũng được nhắc đến riêng
1 bình luận
Các ý kiến trên Hacker News
Theo cách tôi hiểu thì cơ chế hoạt động là như thế này. Mô hình ngôn ngữ lấy prompt làm điểm khởi đầu và xuất ra từng token một; cuộc trò chuyện với LLM cũng có thể được xem là người dùng đưa vào một chuỗi token, mô hình sinh ra một phần, rồi người dùng lại nối tiếp vào đó
Kỹ thuật ngữ pháp này cho phép kiểm soát token ở mức tinh vi hơn nhiều. Ví dụ, nếu đưa đến đoạn
Give me the address of the White House as JSON: {"street": ", LLM sẽ trả về1600 Pennsylvania Ave NW"; ngay khi thấy dấu nháy kép đóng, người dùng chèn", "City": "để nhậnWashington, DC"Tuy nhiên, vì đây là dựa trên ngữ pháp, nên nó có thể làm được nhiều thứ hơn rất nhiều chứ không chỉ JSON. Trước đây tôi từng thấy một đề xuất khá hay trên Twitter: OpenAI có thể cho phép truyền một ngữ pháp phi ngữ cảnh xác định làm tham số API, hoặc xa hơn nữa là nhận vài KB dưới dạng một binary WASM nhỏ chính là sampler, rồi chạy nó với vài MB bộ nhớ; như vậy năng lực của LLM có thể được mở rộng đáng kể
https://twitter.com/grantslatton/status/1637692033115762688
Thiết lập
temperatuređiều chỉnh khả năng các token không đứng hạng cao nhất được chọn, nhằm giảm đầu ra lặp lại. Việc buộc LLM tuân theo ngữ pháp về cơ bản là lọc danh sách trước khi chọn token, nhưng sự ngẫu nhiên do temperature kiểm soát vẫn có thể cònMột tính năng nâng cao hơn là cho phép quay lui khi AI bị kẹt và không thể tạo ra đầu ra hợp lệ
Theo tôi, điểm cốt lõi của PR sampling dựa trên ngữ pháp là llama.cpp dùng ngữ pháp để thu hẹp token đầu ra tiếp theo vào một tập con giới hạn các token có thể có
Nó được viết bởi một trong các nhà phát triển thư viện Outlines; Outlines cũng là một thư viện workflow LLM khá ổn
Ngay cả khi gửi các quy tắc DCFG hoặc phiên bản đã biên dịch như một phần của request, tôi cũng không thấy điều đó thay đổi căn bản cách dự đoán token như thế nào. Nếu mô hình dự đoán thứ không phù hợp với ngữ pháp yêu cầu, ý là lại đưa prompt vào cho đến khi đúng à?
Cần chỉ ra rằng thứ này chỉ đặt ràng buộc ngữ pháp lên văn bản do mô hình tạo ra, chứ không thật sự căn chỉnh nội dung. Nó hữu ích khi cần đảm bảo server xuất JSON được định dạng tốt, nhưng có lẽ không giải quyết được nhiều vấn đề alignment hiện nay trong sinh ngôn ngữ
Ví dụ, hiện Llama hay GPT thường gắn nhãn sai cho các khối mã Markdown. Sampling dựa trên ngữ pháp có thể buộc phải gắn nhãn, nhưng nhãn có đúng hay không thì phụ thuộc ngữ cảnh nên không thể cưỡng ép được. Cũng khó kỳ vọng đầu ra tốt nếu tạo một ngôn ngữ chuyên biệt theo miền mới rồi không căn chỉnh theo ngôn ngữ đó
Chẳng hạn có thể tưởng tượng một hệ thống hot-swap LoRA dành cho Markdown và tệp ngữ pháp Markdown tùy theo nhu cầu
Tôi thật sự thích cái này. Trước đây tôi từng làm Constrained Text Generation Studio (https://github.com/Hellisotherpeople/Constrained-Text-Genera...), và cũng đã có một bài liên quan được đăng ở COLING 2022 (https://paperswithcode.com/paper/most-language-models-can-be...)
Dù vậy, tôi luôn nghĩ rằng hướng đi là các cách tiếp cận như thế này, hoặc những ý tưởng liên quan được liệt kê trong bài này: https://arxiv.org/abs/2306.03081
Giờ tôi phải nghĩ xem làm sao tạo được ngữ pháp có thể cưỡng ép những thứ như số âm tiết hay quy tắc cú pháp. Hiện LLM làm các tác vụ kiểu đó rất kém vì cách token hóa
Tôi cũng đã triển khai cái này cho PyTorch: https://github.com/Shopify/torch-grammar. Tôi cũng có một bản hack của text-generation-inference dùng nó; nếu cần tôi có thể chia sẻ
Với các enum chuỗi dạng nhiều lựa chọn, về cơ bản giống dropdown, tôi tự hỏi liệu khi đưa ra lựa chọn cuối cùng, nếu xét xác suất kết hợp toàn phần của trạng thái có logits thay vì dùng thuật toán tham lam thì có hoạt động tốt hơn không
Khi token bắt đầu được chia sẻ giữa nhiều mục trong danh sách, điều đó có thể giúp ưu tiên lựa chọn đúng thay vì lựa chọn có token bắt đầu phổ biến nhất. Tất nhiên sau khi một phần logits bị đưa về 0 thì phải điều chỉnh xác suất thì mới thực sự hợp lý
“Thư viện” ngữ pháp này đã được trích dẫn như một ví dụ về định dạng: https://github.com/antlr/grammars-v4
Trong đó có đủ thứ, từ assembly và C++ đến GLSL, ngôn ngữ scripting, số học, trò chơi, shortcut freedesktop, LLVM IR, và cả những định dạng đặc biệt như Verilog
Có ai giải thích dễ hiểu được không? Tôi biết LLM ở mức nào đó, nhưng chưa thật sự hiểu Georgi đang làm gì ở đây và vì sao một số người lại phấn khích như vậy
Các chương trình như ChatGPT “diễn giải” vector xác suất đó rồi chọn, tức lấy mẫu, một trong các token hàng đầu để tạo văn bản. Nhưng cách này đôi khi quá linh hoạt: ngay cả khi muốn đầu ra JSON, nó vẫn có thể chọn token không hợp với cú pháp JSON và tạo ra JSON sai
Cách “ép” LLM sinh ra thứ như JSON là thay đổi quá trình lấy mẫu. Thay vì chọn bất kỳ token hàng đầu nào, trước hết chỉ giữ lại các token phù hợp với cú pháp JSON, rồi lấy mẫu các token hàng đầu trong tập con đó
LLM tạo văn bản theo đơn vị token. Trước tiên, một mạng nơ-ron khổng lồ gán xác suất cho tất cả token có thể có; sau đó quy trình lấy mẫu dùng các xác suất đó để chọn một token, rồi lặp lại quá trình này
Quy trình lấy mẫu không phải là mạng nơ-ron nên có thể được thay đổi theo nhiều cách. Cũng có thể dùng lấy mẫu tham lam, luôn chọn token có xác suất cao nhất, nhưng thường thì chọn ngẫu nhiên có trọng số theo xác suất sẽ tốt hơn. Nó tạo ra sự đa dạng và cũng giảm khả năng rơi vào vòng lặp. Tuy nhiên, bất kỳ token nào có xác suất khác 0 đều có thể được chọn, nên JSON sai vẫn có thể xuất hiện. PR này đặt xác suất của mọi token không hợp lệ theo ngữ pháp thành 0 để chúng không thể được chọn
Cũng có thể có những sửa đổi thú vị khác trong quá trình lấy mẫu. Khi lấy mẫu từng token một, ta có thể đi vào ngõ cụt và không còn lựa chọn nào, nên có thể cho phép quay lui. Xa hơn nữa, ở mỗi bước có thể xét nhiều lựa chọn để tạo một cây các đầu ra khả dĩ, rồi cuối cùng chọn đường đi có xác suất tổng thể cao nhất. Nếu xét mọi lựa chọn thì đó là cây đầy đủ phân nhánh theo số token có thể có, nên sẽ tăng theo cấp số mũ; vì vậy có thể cắt tỉa bằng cách chỉ giữ, chẳng hạn, 5 đường đi tốt nhất ở mỗi bước. Đó là beam search. Vì chi phí chạy mạng nơ-ron tạo xác suất rất lớn, việc tăng chi phí lên 5 lần là khá nặng, nên thường không dùng trong LLM, nhưng vẫn khả thi và kết quả cũng tốt hơn ở mức nào đó. Cũng có thể nghĩ đến việc dùng Monte Carlo Tree Search như các engine cờ vua
Vì vậy ta đưa ra một ngữ pháp rằng phản hồi phải bắt đầu bằng một chữ cái viết hoa, sau đó là các chữ thường, dấu hai chấm, dấu cách, các chữ số rồi kết thúc. Khi đó, lúc tìm token đầu tiên, chỉ những token tương thích với mẫu đó mới được xét; các bước sau cũng chỉ tiếp tục xét những token tương thích với phần tiếp theo của mẫu
Loại ngữ pháp này biến việc đó thành một mẫu linh hoạt và hữu ích
Việc này thú vị nên tôi đang định đưa vào công việc của mình. Tuy nhiên, từ góc nhìn Bitter Lesson, tôi có cảm giác đây có thể không phải là cách tiếp cận tốt nhất ngoài rất ngắn hạn: http://www.incompleteideas.net/IncIdeas/BitterLesson.html
Cũng có dự án dùng phương pháp tương tự: https://github.com/automorphic-ai/trex
Playground: https://automorphic.ai/playground
Tôi muốn được gợi ý các bài báo hoặc bài tổng quan giải thích cách sampling/decoding được thực hiện trong kỷ nguyên mạng nơ-ron đầu-cuối. Tôi biết thời HMM thì decoding trong dịch máy và nhận dạng giọng nói được làm như thế nào, ví dụ như https://en.wikipedia.org/wiki/Viterbi_algorithm hay https://en.wikipedia.org/wiki/Beam_search
Dạo này tôi có cảm giác mọi người chỉ làm theo kiểu “tham lam”, nhưng không chắc lắm. Nếu có tài liệu gợi ý về chủ đề này thì tốt quá
https://platform.openai.com/docs/api-reference/completions/c...
Tất nhiên, vì giờ ta biết GPT-4 có kiến trúc mixture of experts, nên bên trong nó đang song song hóa việc tính toán. Ngoài ra còn bao gồm cả cách chỉnh sửa logit bằng các hạng mục presence/frequency penalty