Skip to content

Commit 307f135

Browse files
JakeStevensmeta-codesync[bot]
authored andcommitted
Add CMake targets for HF LLM runners (#23899)
Summary: Pull Request resolved: #23899 Add `EXECUTORCH_BUILD_NATIVE_LLM_RUNNER` to build and install `run_hf_llm_vulkan` and `run_hf_llm_ggml` for the enabled engines. Share gflags setup with the vision runner, resolve tokenizers with regex lookahead support, and add `native_llm_runner_utils_test`. Document building and running the GGML CPU LLM runner. Authored with assistance from Codex. Differential Revision: D125479665
1 parent 7cebc6a commit 307f135

9 files changed

Lines changed: 286 additions & 15 deletions

File tree

‎.ci/scripts/test-native.sh‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,22 @@ case "${component}" in
2121
test_regex='^native_runtime_test$'
2222
;;
2323
ggml)
24-
cmake_args+=(-DEXECUTORCH_BUILD_NATIVE_GGML=ON -DGGML_NATIVE=OFF)
25-
targets+=(native_ggml_engine_test)
26-
test_regex='^native_ggml_engine_test$'
24+
cmake_args+=(
25+
-DEXECUTORCH_BUILD_NATIVE_GGML=ON
26+
-DEXECUTORCH_BUILD_NATIVE_LLM_RUNNER=ON
27+
-DGGML_NATIVE=OFF
28+
)
29+
targets+=(run_hf_llm_ggml native_ggml_engine_test native_llm_runner_utils_test)
30+
test_regex='^native_(ggml_engine|llm_runner_utils)_test$'
2731
;;
2832
vulkan)
2933
cmake_args+=(
3034
-DEXECUTORCH_BUILD_NATIVE_VULKAN=ON
3135
-DEXECUTORCH_BUILD_NATIVE_VISION_RUNNER=ON
36+
-DEXECUTORCH_BUILD_NATIVE_LLM_RUNNER=ON
3237
-DEXECUTORCH_VULKAN_SHADER_COMPILE_NTHREADS="${build_jobs}"
3338
)
34-
targets+=(native_vulkan_engine run_classification native_vulkan_passes_test native_runner_utils_test)
39+
targets+=(native_vulkan_engine run_classification run_hf_llm_vulkan native_vulkan_passes_test native_runner_utils_test)
3540
test_regex='^native_(vulkan_passes|runner_utils)_test$'
3641
;;
3742
*)
@@ -51,5 +56,12 @@ cmake -S . -B "${build_dir}" -G Ninja \
5156
cmake --build "${build_dir}" \
5257
--parallel "${build_jobs}" \
5358
--target "${targets[@]}"
54-
ctest --test-dir "${build_dir}/backends/native/test" \
59+
ctest --test-dir "${build_dir}/backends/native" \
5560
-R "${test_regex}" --output-on-failure --no-tests=error --timeout 120
61+
62+
if [[ "${component}" == "ggml" ]]; then
63+
PYTHONPATH="${PWD}/src:${PYTHONPATH:-}" \
64+
FLATC_EXECUTABLE="${FLATC_EXECUTABLE:-${PWD}/${build_dir}/third-party/flatc_ep/bin/flatc}" \
65+
python .ci/scripts/test_native_llm_runner.py \
66+
--runner "${build_dir}/backends/native/examples/hf_llm/runtime/run_hf_llm_ggml"
67+
fi
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Compare GGML LLM runner prefill and decoding with a tiny eager HF Llama."""
8+
9+
import argparse
10+
import base64
11+
import re
12+
import subprocess
13+
import tempfile
14+
from pathlib import Path
15+
16+
import torch
17+
from executorch.backends.native.examples.hf_llm.export_hf_llm import LastTokenLM
18+
from executorch.exir.native import to_native
19+
from transformers import LlamaConfig, LlamaForCausalLM
20+
21+
22+
@torch.no_grad()
23+
def main() -> None:
24+
parser = argparse.ArgumentParser(description=__doc__)
25+
parser.add_argument("--runner", required=True, type=Path)
26+
args = parser.parse_args()
27+
28+
torch.manual_seed(0)
29+
torch.set_num_threads(4)
30+
max_seq_length = 48
31+
max_new_tokens = 8
32+
config = LlamaConfig(
33+
vocab_size=512,
34+
hidden_size=32,
35+
intermediate_size=64,
36+
num_hidden_layers=2,
37+
num_attention_heads=4,
38+
num_key_value_heads=2,
39+
max_position_embeddings=max_seq_length,
40+
bos_token_id=256,
41+
eos_token_id=257,
42+
attn_implementation="sdpa",
43+
)
44+
model = LlamaForCausalLM(config).eval()
45+
model.generation_config.use_cache = True
46+
model.generation_config.cache_implementation = "static"
47+
wrapper = LastTokenLM(model, max_seq_length).eval()
48+
seq = torch.export.Dim("seq", min=1, max=max_seq_length - 1)
49+
exported = torch.export.export(
50+
wrapper,
51+
(torch.zeros((1, 6), dtype=torch.long), torch.arange(6)),
52+
dynamic_shapes={"input_ids": {1: seq}, "cache_position": {0: seq}},
53+
strict=True,
54+
)
55+
56+
with tempfile.TemporaryDirectory() as directory:
57+
root = Path(directory)
58+
package = root / "llama.ptn"
59+
to_native(exported).save(str(package))
60+
tokenizer = root / "tokenizer.model"
61+
# The byte vocabulary puts Llama's BOS at 256, followed by 255 specials.
62+
tokenizer.write_text(
63+
"".join(
64+
f"{base64.b64encode(bytes([i])).decode()} {i}\n" for i in range(256)
65+
)
66+
)
67+
prompt = "abc"
68+
tokens = [config.bos_token_id, *prompt.encode()]
69+
expected = []
70+
# Recompute the full prefix without a cache for an independent reference.
71+
for _ in range(max_new_tokens):
72+
logits = model(torch.tensor([tokens]), use_cache=False).logits
73+
token = int(logits[0, -1].argmax())
74+
expected.append(token)
75+
tokens.append(token)
76+
77+
result = subprocess.run(
78+
[
79+
str(args.runner.resolve()),
80+
"--device=cpu",
81+
"--cpu_threads=4",
82+
f"--package={package}",
83+
f"--tokenizer={tokenizer}",
84+
f"--prompt={prompt}",
85+
f"--eos_ids={config.vocab_size}",
86+
f"--max_new_tokens={max_new_tokens}",
87+
f"--max_seq_length={max_seq_length}",
88+
"--print_token_ids",
89+
],
90+
capture_output=True,
91+
text=True,
92+
errors="replace",
93+
timeout=120,
94+
)
95+
print(result.stdout, end="")
96+
print(result.stderr, end="")
97+
result.check_returncode()
98+
match = re.search(r"^token_ids: ([0-9,]+)$", result.stdout, re.MULTILINE)
99+
if match is None:
100+
raise AssertionError("Runner did not report generated token IDs")
101+
actual = [int(token) for token in match[1].split(",")]
102+
if actual != expected:
103+
raise AssertionError(f"Generated tokens {actual} != eager {expected}")
104+
print(f"GGML HF LLM runner matched all {max_new_tokens} eager token IDs")
105+
106+
107+
if __name__ == "__main__":
108+
main()

‎.github/workflows/test-native.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
- .github/workflows/_docker-image.yml
88
- .ci/docker/**
99
- .ci/scripts/test-native.sh
10+
- .ci/scripts/test_native_llm_runner.py
1011
- .gitmodules
1112
- CMakeLists.txt
1213
- backends/native/**
@@ -25,6 +26,10 @@ on:
2526
- backends/xnnpack/third-party/FXdiv
2627
- backends/xnnpack/third-party/cpuinfo
2728
- backends/xnnpack/third-party/pthreadpool
29+
- examples/models/llama/tokenizer/**
30+
- extension/llm/tokenizers
31+
- extension/llm/export/**
32+
- exir/**
2833
- runtime/**
2934
- third-party/**
3035
- tools/cmake/**
@@ -93,4 +98,8 @@ jobs:
9398
export CMAKE_PREFIX_PATH="/tmp/native-deps:${CMAKE_PREFIX_PATH:-}"
9499
export PATH="/tmp/native-deps/bin:${PATH}"
95100
101+
if [[ "${NATIVE_COMPONENT}" == "ggml" ]]; then
102+
./install_requirements.sh --use-pt-pinned-commit
103+
fi
104+
96105
CMAKE_BUILD_PARALLEL_LEVEL=8 bash .ci/scripts/test-native.sh "${NATIVE_COMPONENT}"

‎CMakeLists.txt‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,9 @@ if(EXECUTORCH_BUILD_EXTENSION_NAMED_DATA_MAP)
990990
endif()
991991

992992
if(EXECUTORCH_BUILD_EXTENSION_LLM)
993-
if(EXECUTORCH_BUILD_EXTENSION_LLM_RUNNER)
993+
if(EXECUTORCH_BUILD_EXTENSION_LLM_RUNNER
994+
OR EXECUTORCH_BUILD_NATIVE_LLM_RUNNER
995+
)
994996
set(SUPPORT_REGEX_LOOKAHEAD ON)
995997
# llama/runner/CMakeLists.txt builds a shared library libllama_runner.so
996998
# that transitively depends on tokenizers. Need to build tokenizers with
@@ -1001,7 +1003,9 @@ if(EXECUTORCH_BUILD_EXTENSION_LLM)
10011003
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
10021004
endif()
10031005
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/llm/tokenizers)
1004-
if(EXECUTORCH_BUILD_EXTENSION_LLM_RUNNER)
1006+
if(EXECUTORCH_BUILD_EXTENSION_LLM_RUNNER
1007+
OR EXECUTORCH_BUILD_NATIVE_LLM_RUNNER
1008+
)
10051009
set(CMAKE_POSITION_INDEPENDENT_CODE
10061010
${ORIGINAL_CMAKE_POSITION_INDEPENDENT_CODE_FLAG}
10071011
)

‎backends/native/CMakeLists.txt‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ install(
4545
PATTERN "ggml" EXCLUDE
4646
)
4747

48-
if(EXECUTORCH_BUILD_NATIVE_VISION_RUNNER)
49-
if(NOT EXECUTORCH_BUILD_NATIVE_VULKAN)
48+
if(EXECUTORCH_BUILD_NATIVE_VISION_RUNNER OR EXECUTORCH_BUILD_NATIVE_LLM_RUNNER)
49+
if(NOT EXECUTORCH_BUILD_NATIVE_VULKAN AND NOT EXECUTORCH_BUILD_NATIVE_GGML)
5050
message(
5151
FATAL_ERROR
52-
"EXECUTORCH_BUILD_NATIVE_VISION_RUNNER requires EXECUTORCH_BUILD_NATIVE_VULKAN=ON"
52+
"Native runners require EXECUTORCH_BUILD_NATIVE_VULKAN=ON or EXECUTORCH_BUILD_NATIVE_GGML=ON"
5353
)
5454
endif()
5555
add_subdirectory(examples)

‎backends/native/README.md‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,22 @@ target_link_libraries(my_app PRIVATE native_api native_vulkan_engine)
5050

5151
With `EXECUTORCH_BUILD_TESTS=ON`, `native_runtime_test` covers the shared runtime
5252
and API. `native_vulkan_passes_test` covers the Vulkan graph rewrites without
53-
requiring a GPU. When the vision runner below is enabled,
54-
`native_runner_utils_test` covers its shared runner utilities. Run these with
53+
requiring a GPU. When either runner below is enabled,
54+
`native_runner_utils_test` covers their shared runner utilities. Run these with
5555
`ctest --test-dir cmake-out -R native_`.
5656

5757
The `Test Native Engines` CI workflow has separate Linux jobs for the shared
5858
runtime, GGML, and Vulkan, with each test suite run once. To reproduce with
5959
dependencies installed, run `bash .ci/scripts/test-native.sh runtime`,
6060
`bash .ci/scripts/test-native.sh ggml`, or
61-
`bash .ci/scripts/test-native.sh vulkan` from the OSS checkout. The Vulkan job
62-
also builds `run_classification`; its tests do not require a GPU.
61+
`bash .ci/scripts/test-native.sh vulkan` from the OSS checkout. The GGML job
62+
also runs `native_llm_runner_utils_test` and executes `run_hf_llm_ggml` on a tiny
63+
seeded Hugging Face Llama with a generated byte tokenizer. Its eight generated
64+
token IDs must match eager PyTorch, covering prefill and KV-cache decoding without
65+
downloading a model. The GGML job requires the Python export dependencies from
66+
`./install_requirements.sh --use-pt-pinned-commit`. The Vulkan
67+
job builds `run_classification` and `run_hf_llm_vulkan`; its tests do not require
68+
a GPU.
6369

6470
## GGML engine
6571

@@ -116,3 +122,33 @@ Export the `.ptn`, input, reference logits, and labels with
116122
[`export.py`](../../examples/native/vision/export.py). The runner prints the
117123
top-5 logits and, when `--expected` is given, exits nonzero if the logits
118124
differ from the reference beyond `--rtol`/`--atol` (both default to `1e-3`).
125+
126+
## Native LLM runner
127+
128+
`EXECUTORCH_BUILD_NATIVE_LLM_RUNNER=ON` builds `run_hf_llm_vulkan` with Native
129+
Vulkan and `run_hf_llm_ggml` with Native GGML. It requires gflags and
130+
`tokenizers::tokenizers` (the vendored tokenizer is built automatically in an
131+
OSS checkout; otherwise provide an installed tokenizers package with regex
132+
lookahead support). It accepts Llama 3 TikToken tokenizer files.
133+
134+
To build the text runner for GGML CPU, initialize the llama.cpp submodule as above:
135+
136+
```sh
137+
cmake -S . -B cmake-out -DCMAKE_BUILD_TYPE=Release \
138+
-DEXECUTORCH_BUILD_NATIVE_GGML=ON \
139+
-DEXECUTORCH_BUILD_NATIVE_LLM_RUNNER=ON
140+
cmake --build cmake-out --target run_hf_llm_ggml
141+
cmake --install cmake-out --prefix install
142+
install/bin/run_hf_llm_ggml --device cpu --cpu_threads 4 \
143+
--package llama.ptn --tokenizer tokenizer.model \
144+
--prompt "The capital of France is" --max_new_tokens 32
145+
```
146+
147+
The GGML runner accepts `--device cpu|gpu`, `--device_index`, `--cpu_threads`,
148+
and `--max_cached_graphs` (default 1, 0 rebuilds each call). For GPU execution,
149+
enable the desired `GGML_*` backend when configuring ExecuTorch, as above.
150+
Buck provides separate
151+
`run_hf_llm_ggml_{cpu,cuda,hip,metal}` targets; the Metal target uses xplat.
152+
153+
With `EXECUTORCH_BUILD_TESTS=ON`, `native_llm_runner_utils_test` exercises the
154+
LLM generation utilities.

‎backends/native/examples/CMakeLists.txt‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,22 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
if(NOT TARGET gflags AND NOT TARGET gflags::gflags)
8+
find_package(gflags REQUIRED)
9+
endif()
10+
if(TARGET gflags::gflags)
11+
set(_native_runner_gflags gflags::gflags)
12+
else()
13+
set(_native_runner_gflags gflags)
14+
endif()
15+
716
add_subdirectory(runner_utils)
8-
add_subdirectory(vision)
17+
if(EXECUTORCH_BUILD_NATIVE_VISION_RUNNER)
18+
if(NOT EXECUTORCH_BUILD_NATIVE_VULKAN)
19+
message(FATAL_ERROR "The native vision runner requires Native Vulkan")
20+
endif()
21+
add_subdirectory(vision)
22+
endif()
23+
if(EXECUTORCH_BUILD_NATIVE_LLM_RUNNER)
24+
add_subdirectory(hf_llm/runtime)
25+
endif()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
if(NOT TARGET tokenizers::tokenizers)
8+
if(EXISTS "${EXECUTORCH_ROOT}/extension/llm/tokenizers/CMakeLists.txt")
9+
set(SUPPORT_REGEX_LOOKAHEAD ON)
10+
add_subdirectory(
11+
${EXECUTORCH_ROOT}/extension/llm/tokenizers
12+
${CMAKE_CURRENT_BINARY_DIR}/tokenizers
13+
)
14+
else()
15+
find_package(tokenizers CONFIG REQUIRED)
16+
endif()
17+
endif()
18+
19+
add_library(native_llm_runner_utils STATIC LlmRunnerUtils.cpp)
20+
target_compile_features(native_llm_runner_utils PUBLIC cxx_std_20)
21+
target_include_directories(
22+
native_llm_runner_utils PUBLIC $<BUILD_INTERFACE:${EXECUTORCH_ROOT}/..>
23+
)
24+
add_library(
25+
native_llm_runner STATIC
26+
LlmRunner.cpp
27+
${EXECUTORCH_ROOT}/examples/models/llama/tokenizer/llama_tiktoken.cpp
28+
)
29+
target_link_libraries(
30+
native_llm_runner
31+
PUBLIC native_engine
32+
PRIVATE ${_native_runner_gflags} native_llm_runner_utils native_package
33+
native_runner_utils native_runtime tokenizers::tokenizers
34+
)
35+
foreach(target native_llm_runner native_llm_runner_utils)
36+
target_compile_options(
37+
${target} PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
38+
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-fexceptions>
39+
)
40+
endforeach()
41+
42+
set(_llm_runners)
43+
if(EXECUTORCH_BUILD_NATIVE_VULKAN)
44+
add_executable(run_hf_llm_vulkan run_hf_llm_vulkan.cpp)
45+
target_link_libraries(run_hf_llm_vulkan PRIVATE native_vulkan_engine)
46+
list(APPEND _llm_runners run_hf_llm_vulkan)
47+
endif()
48+
if(EXECUTORCH_BUILD_NATIVE_GGML)
49+
add_executable(run_hf_llm_ggml run_hf_llm_ggml.cpp)
50+
target_link_libraries(run_hf_llm_ggml PRIVATE native_ggml_engine)
51+
list(APPEND _llm_runners run_hf_llm_ggml)
52+
endif()
53+
foreach(target IN LISTS _llm_runners)
54+
target_link_libraries(
55+
${target} PRIVATE native_llm_runner ${_native_runner_gflags}
56+
)
57+
target_compile_options(
58+
${target} PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
59+
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-fexceptions>
60+
)
61+
install(TARGETS ${target} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
62+
endforeach()
63+
64+
if(EXECUTORCH_BUILD_TESTS)
65+
add_executable(native_llm_runner_utils_test LlmRunnerUtilsTest.cpp)
66+
target_link_libraries(
67+
native_llm_runner_utils_test PRIVATE native_llm_runner_utils
68+
GTest::gtest_main
69+
)
70+
add_test(NAME native_llm_runner_utils_test
71+
COMMAND native_llm_runner_utils_test
72+
)
73+
endif()

‎tools/cmake/preset/default.cmake‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ define_overridable_option(
192192
EXECUTORCH_BUILD_NATIVE_VISION_RUNNER
193193
"Build the native vision classification runner (run_classification)" BOOL OFF
194194
)
195+
define_overridable_option(
196+
EXECUTORCH_BUILD_NATIVE_LLM_RUNNER "Build the native LLM runners" BOOL OFF
197+
)
195198
define_overridable_option(
196199
EXECUTORCH_BUILD_WEBGPU "Build the WebGPU backend" BOOL OFF
197200
)
@@ -528,6 +531,15 @@ check_required_options_on(
528531
EXECUTORCH_BUILD_NATIVE_VULKAN
529532
)
530533

534+
if(EXECUTORCH_BUILD_NATIVE_LLM_RUNNER AND NOT (EXECUTORCH_BUILD_NATIVE_VULKAN
535+
OR EXECUTORCH_BUILD_NATIVE_GGML)
536+
)
537+
message(
538+
FATAL_ERROR
539+
"Native LLM runners require EXECUTORCH_BUILD_NATIVE_VULKAN=ON or EXECUTORCH_BUILD_NATIVE_GGML=ON"
540+
)
541+
endif()
542+
531543
if(NOT EXISTS ${EXECUTORCH_PAL_DEFAULT_FILE_PATH})
532544
message(
533545
FATAL_ERROR

0 commit comments

Comments
 (0)