这篇文章介绍了一个基于 Rust 和 WebAssembly (Wasm) 的解决方案,用于在异构边缘计算设备上快速和便携地进行 Llama2 模型的推理。与 Python 相比,这种 Rust+Wasm 应用程序的体积仅为 Python 的 1/100,速度提升 100 倍,并且可以在全硬件加速环境中安全运行,不需要更改二进制代码。文章基于 Georgi Gerganov 创建的 llama.cpp 项目,将原始的 C++ 程序适配到 Wasm 上。安装过程包括安装 WasmEdge 和 GGML 插件,下载预构建的 Wasm 应用和模型,然后使用 WasmEdge 运行 Wasm 推理应用,并传递 GGUF 格式的模型文件。此外,文章还提供了多个命令行选项,用于配置与模型的交互方式。
原文链接:https://www.secondstate.io/articles/fast-llm-inference/
curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash -s -- --plugin wasi_nn-ggml
步骤2. 下载预构建的 Wasm 应用和模型
curl -LO https://github.com/second-state/llama-utils/raw/main/chat/llama-chat.wasm
你还应该下载一个 GGUF 格式的 llama2 模型。下面的例子下载了一个调整为5位权重的 llama2 7B聊天模型。
curl -LO https://huggingface.co/wasmedge/llama2/resolve/main/llama-2-7b-chat-q5_k_m.gguf
wasmedge --dir .:. --nn-preload default:GGML:AUTO:llama-2-7b-chat-q5_k_m.gguf llama-chat.wasm
Options:
-m, --model-alias <ALIAS>
Model alias [default: default]
-c, --ctx-size <CTX_SIZE>
Size of the prompt context [default: 4096]
-n, --n-predict <N_PRDICT>
Number of tokens to predict [default: 1024]
-g, --n-gpu-layers <N_GPU_LAYERS>
Number of layers to run on the GPU [default: 100]
-b, --batch-size <BATCH_SIZE>
Batch size for prompt processing [default: 4096]
-r, --reverse-prompt <REVERSE_PROMPT>
Halt generation at PROMPT, return control.
-s, --system-prompt <SYSTEM_PROMPT>
System prompt message string [default: "[Default system message
for the prompt template]"]
-p, --prompt-template <TEMPLATE>
Prompt template. [default: llama-2-chat] [possible values: llama-2-chat, codellama-instruct, mistral-instruct-v0.1, mistrallite, openchat, belle-llama-2-chat, vicuna-chat, chatml]
--log-prompts
Print prompt strings to stdout
--log-stat
Print statistics to stdout
--log-all
Print all log information to stdout
--stream-stdout
Print the output to stdout in the streaming way
-h, --help
Print help
例如,以下命令指定了 2048 个 token 的上下文长度和每次响应的最大 512 个 token 。它还告诉 WasmEdge 以流式方式将模型响应逐个 token 返回到stdout。该程序在低端 M2 MacBook 上每秒生成大约 25 个 token。
wasmedge --dir .:. --nn-preload default:GGML:AUTO:llama-2-7b-chat-q5_k_m.gguf \
llama-chat.wasm -c 2048 -n 512 --log-stat --stream-stdout
[USER]:
Who is the "father of the atomic bomb"?(谁是“原子弹之父”?)
---------------- [LOG: STATISTICS] -----------------
llama_new_context_with_model: n_ctx = 2048
llama_new_context_with_model: freq_base = 10000.0
llama_new_context_with_model: freq_scale = 1
llama_new_context_with_model: kv self size = 1024.00 MB
llama_new_context_with_model: compute buffer total size = 630.14 MB
llama_new_context_with_model: max tensor size = 102.54 MB
[2023-11-10 17:52:12.768] [info] [WASI-NN] GGML backend: llama_system_info: AVX = 0 | AVX2 = 0 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 0 | NEON = 1 | ARM_FMA = 1 | F16C = 0 | FP16_VA = 1 | WASM_SIMD = 0 | BLAS = 0 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 |
The "father of the atomic bomb" is a term commonly associated with physicist J. Robert Oppenheimer. Oppenheimer was the director of the Manhattan Project, the secret research and development project that produced the atomic bomb during World War II. He is widely recognized as the leading figure in the development of the atomic bomb and is often referred to as the "father of the atomic bomb."
llama_print_timings: load time = 15643.70 ms
llama_print_timings: sample time = 2.60 ms / 83 runs ( 0.03 ms per token, 31886.29 tokens per second)
llama_print_timings: prompt eval time = 7836.72 ms / 54 tokens ( 145.12 ms per token, 6.89 tokens per second)
llama_print_timings: eval time = 3198.24 ms / 82 runs ( 39.00 ms per token, 25.64 tokens per second)
llama_print_timings: total time = 18852.93 ms
----------------------------------------------------
下面是它在 Nvidia A10G 机器上以每秒 50 个 token 的速度运行示例。
wasmedge --dir .:. --nn-preload default:GGML:AUTO:llama-2-7b-chat-q5_k_m.gguf \
llama-chat.wasm -c 2048 -n 512 --log-stat
[USER]:
Who is the "father of the atomic bomb"? (谁是“原子弹之父”?)
---------------- [LOG: STATISTICS] -----------------
llm_load_tensors: using CUDA for GPU acceleration
llm_load_tensors: mem required = 86.04 MB
llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloading non-repeating layers to GPU
llm_load_tensors: offloaded 35/35 layers to GPU
llm_load_tensors: VRAM used: 4474.93 MB
..................................................................................................
llama_new_context_with_model: n_ctx = 2048
llama_new_context_with_model: freq_base = 10000.0
llama_new_context_with_model: freq_scale = 1
llama_kv_cache_init: offloading v cache to GPU
llama_kv_cache_init: offloading k cache to GPU
llama_kv_cache_init: VRAM kv self = 1024.00 MB
llama_new_context_with_model: kv self size = 1024.00 MB
llama_new_context_with_model: compute buffer total size = 630.14 MB
llama_new_context_with_model: VRAM scratch buffer: 624.02 MB
llama_new_context_with_model: total VRAM used: 6122.95 MB (model: 4474.93 MB, context: 1648.02 MB)
[2023-11-11 00:02:22.402] [info] [WASI-NN] GGML backend: llama_system_info: AVX = 1 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 1 | SSSE3 = 1 | VSX = 0 |
llama_print_timings: load time = 2601.44 ms
llama_print_timings: sample time = 2.63 ms / 84 runs ( 0.03 ms per token, 31987.81 tokens per second)
llama_print_timings: prompt eval time = 203.90 ms / 54 tokens ( 3.78 ms per token, 264.84 tokens per second)
llama_print_timings: eval time = 1641.84 ms / 83 runs ( 19.78 ms per token, 50.55 tokens per second)
llama_print_timings: total time = 4254.95 ms
----------------------------------------------------
[ASSISTANT]:
The "father of the atomic bomb" is a term commonly associated with physicist J. Robert Oppenheimer. Oppenheimer was the director of the Manhattan Project, the secret research and development project that produced the first atomic bomb during World War II. He is widely recognized as the leading figure in the development of the atomic bomb and is often referred to as the "father of the atomic bomb."
(“原子弹之父”这个词通常与物理学家 J. Robert Oppenheimer 联系在一起。奥本海默是曼哈顿计划的主管,曼哈顿计划是二战期间研发出第一颗原子弹的秘密研发项目。他被广泛认为是原子弹研发的领导人物,常被称为“原子弹之父”。)
注:原始文档均为英文,括号内的中文为翻译内容。
边缘上的 Llama,图片由 Midjourney 生成
Python 包具有复杂的依赖性,难以设置和使用。 Python 依赖庞大。Python 或 PyTorch 的 Docker 镜像通常有几 GB 甚至几十 GB。这对于边缘服务器或设备上的AI推理尤其成问题。 Python 是一种非常慢的语言,与 C、C++ 和 Rust 等编译语言相比,慢达 35,000 倍。 由于 Python 慢,大部分实际工作必须委托给 Python 包装器下的原生共享库。这使得 Python 推理应用非常适合演示,但根据业务需求进行修改时非常困难。 重度依赖原生库和复杂的依赖管理使得 Python AI 程序难以在各种设备上移植,同时利用设备的独特硬件特性。
LLM 工具链中常用的 Python 包相互冲突
超轻量级。推理应用仅为 2 MB,包含所有依赖,不到典型 PyTorch 容器大小的 1%。 非常快。在推理应用的各个环节,包括预处理、张量计算和后处理,都能达到原生 C/Rust 的速度。 便携。相同的 Wasm 字节码应用可以在所有主要计算平台上运行,并支持异构硬件加速。 易于设置、开发和部署。不再有复杂的依赖。只要在你的笔记本上使用标准工具构建单个 Wasm 文件,就可以在任何地方部署! 安全且适用于云。Wasm 运行时旨在隔离不受信任的用户代码。Wasm 运行时可以由容器工具管理,并轻松部署在云原生平台上。
fn main() {
let args: Vec<String> = env::args().collect();
let model_name: &str = &args[1];
let graph =
wasi_nn::GraphBuilder::new(wasi_nn::GraphEncoding::Ggml, wasi_nn::ExecutionTarget::AUTO)
.build_from_cache(model_name)
.unwrap();
let mut context = graph.init_execution_context().unwrap();
let system_prompt = String::from("<<SYS>>You are a helpful, respectful and honest assistant. Always answer as short as possible, while being safe. <</SYS>>");
let mut saved_prompt = String::new();
loop {
println!("Question:");
let input = read_input();
if saved_prompt == "" {
saved_prompt = format!("[INST] {} {} [/INST]", system_prompt, input.trim());
} else {
saved_prompt = format!("{} [INST] {} [/INST]", saved_prompt, input.trim());
}
// 将用户问题字符串预处理成张量格式后,设置为模型输入张量,以供模型进行下游推理计算。
let tensor_data = saved_prompt.as_bytes().to_vec();
context
.set_input(0, wasi_nn::TensorType::U8, &[1], &tensor_data)
.unwrap();
// 执行推理计算
context.compute().unwrap();
// 获取输出
let mut output_buffer = vec![0u8; 1000];
let output_size = context.get_output(0, &mut output_buffer).unwrap();
let output = String::from_utf8_lossy(&output_buffer[..output_size]).to_string();
println!("Answer:\n{}", output.trim());
saved_prompt = format!("{} {} ", saved_prompt, output.trim());
}
}
要自行构建应用,只需安装 Rust 编译器及其 wasm32-wasi 编译目标。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-wasi
然后,检出源项目,并运行cargo命令从 Rust 源项目构建 Wasm 文件。
# 克隆源项目
git clone https://github.com/second-state/llama-utils
cd llama-utils/chat/
# 构建
cargo build --target wasm32-wasi --release
# 结果 wasm 文件
cp target/wasm32-wasi/release/llama-chat.wasm .
为更多的硬件和操作系统平台添加 GGML 插件。我们也对 Linux 和 Windows 上的 TPU、ARM NPU 以及其他专门的 AI 芯片感兴趣。 支持更多 llama.cpp 配置。我们目前支持从 Wasm 向 GGML 插件传递一些配置选项。但我们希望支持 GGML 提供的所有选项! 在其他兼容 Wasm 的语言中支持 WASI NN API。我们特别感兴趣的语言包括 Go、Zig、Kotlin、JavaScript、C 和 C++。
mediapipe-rs 项目为 Google 的 mediapipe Tensorflow 模型系列提供了 Rust+Wasm API。 WasmEdge YOLO 项目提供了用于处理 YOLOv8 PyTorch 模型的 Rust+Wasm API。 WasmEdge ADAS 演示 展示了如何使用 Intel OpenVINO 模型进行自动驾驶汽车的道路分割。 WasmEdge 文档 AI 项目将为一系列流行的 OCR 和文档处理模型提供 Rust+Wasm API。