Skip to content

Commit 3f238f0

Browse files
authored
[native] Add missing HF LLM runner sources (pytorch#23893)
### Summary The native HF LLM runner Buck targets reference four source files that are missing from the OSS tree, preventing the runner library and utility tests from building. Add `LlmRunner.cpp`, `LlmRunnerUtils.cpp`, `LlmRunnerUtils.h`, and `LlmRunnerUtilsTest.cpp`. These supply `ptn::run_llm` and the helpers used by the checked-in Vulkan and ggml runner entry points. This PR was prepared with OpenAI Codex.
1 parent 9ae3990 commit 3f238f0

4 files changed

Lines changed: 763 additions & 0 deletions

File tree

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
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+
#include <algorithm>
8+
#include <chrono>
9+
#include <cstdint>
10+
#include <cstdio>
11+
#include <exception>
12+
#include <functional>
13+
#include <limits>
14+
#include <memory>
15+
#include <numeric>
16+
#include <stdexcept>
17+
#include <string>
18+
#include <utility>
19+
#include <vector>
20+
21+
#include <executorch/backends/native/examples/hf_llm/runtime/LlmRunner.h>
22+
#include <executorch/backends/native/examples/hf_llm/runtime/LlmRunnerUtils.h>
23+
#include <executorch/backends/native/examples/runner_utils/runner_utils.h>
24+
#include <executorch/backends/native/runtime/Program.h>
25+
#include <executorch/backends/native/runtime/deserialize/Package.h>
26+
#include <executorch/backends/native/runtime/engine/Engine.h>
27+
#include <executorch/backends/native/runtime/graph/ScalarType.h>
28+
#include <executorch/examples/models/llama/tokenizer/llama_tiktoken.h>
29+
#include <gflags/gflags.h>
30+
#include <pytorch/tokenizers/error.h>
31+
32+
DEFINE_string(package, "", "Path to the *.ptn model. Required.");
33+
DEFINE_string(tokenizer, "", "Path to the Llama TikToken model. Required.");
34+
DEFINE_string(prompt, "The capital of France is", "Prompt text.");
35+
DEFINE_string(method, "", "Method to run. Defaults to the first one.");
36+
DEFINE_string(eos_ids, "128001,128009", "Comma-separated EOS token IDs.");
37+
DEFINE_int32(max_new_tokens, 32, "Maximum generated token count.");
38+
DEFINE_int32(max_seq_length, 2048, "Maximum prompt plus generated tokens.");
39+
DEFINE_int32(num_bos, 1, "Number of BOS tokens prepended to the prompt.");
40+
DEFINE_bool(
41+
print_token_ids,
42+
false,
43+
"Print the generated token IDs for exact comparisons.");
44+
DEFINE_bool(
45+
print_top_logits,
46+
false,
47+
"Print top-five raw logits for numerical diagnostics.");
48+
DEFINE_string(
49+
dump_first_logits,
50+
"",
51+
"Write the initial FP32 logits row; refuse to overwrite.");
52+
DEFINE_bool(
53+
warmup_prefill,
54+
false,
55+
"Replay the identical prompt once before timing prefill (benchmark only).");
56+
namespace {
57+
58+
using Clock = std::chrono::steady_clock;
59+
60+
double elapsed_ms(Clock::time_point start, Clock::time_point end) {
61+
return std::chrono::duration<double, std::milli>(end - start).count();
62+
}
63+
64+
// Returns the shortest cache's sequence length, or 0 if the method has none.
65+
size_t kv_cache_length(const ptn::Method& method) {
66+
size_t length = 0;
67+
for (const ptn::Node& node : method.graph.nodes) {
68+
for (const ptn::NamedArgument& input : node.inputs) {
69+
// An absent optional tensor has an invalid id.
70+
if (input.arg.kind() != ptn::ArgKind::Tensor ||
71+
!ptn::valid(input.arg.as_tensor().id)) {
72+
continue;
73+
}
74+
const ptn::Value& value = method.graph.value(input.arg.as_tensor().id);
75+
if (!value.is_tensor()) {
76+
continue;
77+
}
78+
const size_t sequence = ptn::kv_cache_sequence_length(
79+
node.target, input.name, value.tensor_meta().sizes);
80+
if (sequence > 0) {
81+
length = length == 0 ? sequence : std::min(length, sequence);
82+
}
83+
}
84+
}
85+
return length;
86+
}
87+
88+
} // namespace
89+
90+
namespace ptn {
91+
92+
// cppcheck-suppress unusedFunction
93+
int run_llm(const std::function<std::unique_ptr<EngineHost>()>& make_host) {
94+
if (FLAGS_package.empty() || FLAGS_tokenizer.empty()) {
95+
std::fprintf(stderr, "error: --package and --tokenizer are required\n");
96+
return 1;
97+
}
98+
// The tokenizer takes num_bos as an int8_t.
99+
if (FLAGS_max_new_tokens <= 0 || FLAGS_max_seq_length <= 0 ||
100+
FLAGS_num_bos < 0 || FLAGS_num_bos > 127) {
101+
std::fprintf(stderr, "error: invalid generation limits\n");
102+
return 1;
103+
}
104+
105+
try {
106+
const auto eos_ids = ptn::parse_token_ids(FLAGS_eos_ids);
107+
108+
std::unique_ptr<::tokenizers::Tiktoken> tokenizer =
109+
example::get_tiktoken_for_llama();
110+
if (tokenizer->load(FLAGS_tokenizer) != ::tokenizers::Error::Ok) {
111+
throw std::runtime_error(
112+
"failed to load --tokenizer as a Llama 3 TikToken model");
113+
}
114+
auto encoded =
115+
tokenizer->encode(FLAGS_prompt, static_cast<int8_t>(FLAGS_num_bos), 0);
116+
if (!encoded.ok()) {
117+
throw std::runtime_error(
118+
"failed to encode prompt: tokenizer error " +
119+
std::to_string(static_cast<uint32_t>(encoded.error())));
120+
}
121+
std::vector<uint64_t> prompt_tokens = std::move(encoded.get());
122+
if (prompt_tokens.empty()) {
123+
throw std::runtime_error("prompt encoded to no tokens");
124+
}
125+
126+
const Clock::time_point load_start = Clock::now();
127+
const auto package =
128+
std::make_shared<const ptn::Package>(ptn::Package::load(FLAGS_package));
129+
const ptn::ByteSpan program_bytes = package->program_bytes();
130+
const auto program = std::make_shared<const ptn::Program>(
131+
ptn::Program::load(program_bytes.data(), program_bytes.size()));
132+
const std::vector<std::string> method_names = program->method_names();
133+
if (method_names.empty()) {
134+
throw std::runtime_error("package holds no methods");
135+
}
136+
const std::string method_name =
137+
FLAGS_method.empty() ? method_names.front() : FLAGS_method;
138+
const ptn::Method& method = program->get_method(method_name);
139+
140+
std::vector<bool> user_outputs;
141+
user_outputs.reserve(method.output_specs.size());
142+
// cppcheck-suppress-begin useStlAlgorithm
143+
for (const ptn::OutputSpec& spec : method.output_specs) {
144+
user_outputs.push_back(spec.kind == ptn::OutputKind::UserOutput);
145+
}
146+
// cppcheck-suppress-end useStlAlgorithm
147+
const size_t logits_output = ptn::resolve_logits_output(
148+
method.graph.output_ids.size(), user_outputs);
149+
150+
const std::unique_ptr<ptn::EngineHost> host = make_host();
151+
const std::unique_ptr<ptn::EngineContext> context =
152+
host->create_context(program, package);
153+
const Clock::time_point load_end = Clock::now();
154+
const Clock::time_point compile_start = Clock::now();
155+
const std::unique_ptr<ptn::EngineExecutable> executable =
156+
context->compile(method_name);
157+
const Clock::time_point compile_end = Clock::now();
158+
159+
std::printf(
160+
"engine: %s on %s\n",
161+
host->name().c_str(),
162+
host->device_name().c_str());
163+
std::printf(
164+
"method: %s (%zu inputs, %zu outputs)\n",
165+
method_name.c_str(),
166+
executable->num_inputs(),
167+
executable->num_outputs());
168+
std::printf("compile: %.3f ms\n", elapsed_ms(compile_start, compile_end));
169+
std::printf(
170+
"load: %.3f ms (package, program, engine host, and context)\n",
171+
elapsed_ms(load_start, load_end));
172+
std::printf("constants: %zu package bytes\n", package->constant_bytes());
173+
174+
if (executable->num_inputs() != 2) {
175+
throw std::runtime_error("runner requires token and position inputs");
176+
}
177+
if (logits_output >= executable->num_outputs()) {
178+
throw std::runtime_error("logits output is outside engine outputs");
179+
}
180+
// Before any resize, input sizes are the serialized upper bounds.
181+
const std::vector<int64_t> token_shape = executable->input_sizes(0);
182+
if (token_shape.size() != 2 || token_shape[0] != 1 || token_shape[1] <= 0) {
183+
throw std::runtime_error("token input must have shape [1, sequence]");
184+
}
185+
// ExecuTorch Llama exports take the start position; HuggingFace exports
186+
// take one cache position per token.
187+
const std::vector<int64_t> position_shape = executable->input_sizes(1);
188+
const bool per_token_positions =
189+
position_shape.size() == 1 && position_shape[0] == token_shape[1];
190+
if (!per_token_positions && runner_utils::numel_of(position_shape) != 1) {
191+
throw std::runtime_error(
192+
"position input must have one element or one per token");
193+
}
194+
for (size_t i = 0; i < 2; ++i) {
195+
const ptn::ScalarType dtype = executable->input_dtype(i);
196+
if (dtype != ptn::kInt && dtype != ptn::kLong) {
197+
throw std::runtime_error(
198+
"token and position inputs must be Int or Long, got " +
199+
std::string(ptn::scalar_type_name(dtype)));
200+
}
201+
}
202+
const ptn::ScalarType output_dtype =
203+
executable->output_dtype(logits_output);
204+
if (output_dtype != ptn::kFloat && output_dtype != ptn::kHalf) {
205+
throw std::runtime_error(
206+
"logits must be Float or Half, got " +
207+
std::string(ptn::scalar_type_name(output_dtype)));
208+
}
209+
std::vector<float> logits;
210+
211+
auto set_positions = [&](int64_t start, size_t count) {
212+
const size_t n = per_token_positions ? count : 1;
213+
if (per_token_positions) {
214+
executable->resize_input(1, {static_cast<int64_t>(n)});
215+
}
216+
std::vector<int64_t> values(n);
217+
std::iota(values.begin(), values.end(), start);
218+
if (executable->input_dtype(1) == ptn::kInt) {
219+
const std::vector<int32_t> narrowed(values.begin(), values.end());
220+
executable->set_input(1, narrowed.data(), narrowed.size(), ptn::kInt);
221+
} else {
222+
executable->set_input(1, values.data(), values.size(), ptn::kLong);
223+
}
224+
};
225+
auto set_tokens = [&](const std::vector<uint64_t>& tokens) {
226+
executable->resize_input(0, {1, static_cast<int64_t>(tokens.size())});
227+
if (executable->input_dtype(0) == ptn::kInt) {
228+
std::vector<int32_t> narrowed;
229+
narrowed.reserve(tokens.size());
230+
for (const uint64_t token : tokens) {
231+
if (token >
232+
static_cast<uint64_t>(std::numeric_limits<int32_t>::max())) {
233+
throw std::runtime_error("token ID is outside Int range");
234+
}
235+
narrowed.push_back(static_cast<int32_t>(token));
236+
}
237+
executable->set_input(0, narrowed.data(), narrowed.size(), ptn::kInt);
238+
return;
239+
}
240+
std::vector<int64_t> values;
241+
values.reserve(tokens.size());
242+
for (const uint64_t token : tokens) {
243+
if (token >
244+
static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
245+
throw std::runtime_error("token ID is outside Long range");
246+
}
247+
values.push_back(static_cast<int64_t>(token));
248+
}
249+
executable->set_input(0, values.data(), values.size(), ptn::kLong);
250+
};
251+
auto run_tokens = [&](const std::vector<uint64_t>& tokens,
252+
int64_t position) {
253+
set_tokens(tokens);
254+
set_positions(position, tokens.size());
255+
executable->execute();
256+
const std::vector<int64_t> output_shape =
257+
executable->output_sizes(logits_output);
258+
if (output_shape.empty() || output_shape.back() <= 0) {
259+
throw std::runtime_error("logits output has no vocabulary dimension");
260+
}
261+
const size_t logits_numel =
262+
static_cast<size_t>(runner_utils::numel_of(output_shape));
263+
const size_t vocabulary_size = static_cast<size_t>(output_shape.back());
264+
logits.resize(logits_numel);
265+
executable->get_output(
266+
logits_output, logits.data(), logits.size(), ptn::kFloat);
267+
if (position == 0 && !FLAGS_dump_first_logits.empty()) {
268+
FILE* output = std::fopen(FLAGS_dump_first_logits.c_str(), "wbx");
269+
if (output == nullptr) {
270+
throw std::runtime_error(
271+
"cannot create new logits dump: " + FLAGS_dump_first_logits);
272+
}
273+
const float* row = logits.data() + logits_numel - vocabulary_size;
274+
const size_t written =
275+
std::fwrite(row, sizeof(float), vocabulary_size, output);
276+
const int close_status = std::fclose(output);
277+
if (written != vocabulary_size || close_status != 0) {
278+
std::remove(FLAGS_dump_first_logits.c_str());
279+
throw std::runtime_error(
280+
"failed to write complete initial logits dump");
281+
}
282+
}
283+
if (FLAGS_print_top_logits) {
284+
const float* row = logits.data() + logits_numel - vocabulary_size;
285+
const std::vector<size_t> ranks =
286+
ptn::top_logit_indices(row, vocabulary_size, 5);
287+
std::fprintf(
288+
stderr, "top_logits=%lld", static_cast<long long>(position));
289+
for (const size_t rank : ranks) {
290+
std::fprintf(stderr, ",%zu:%.8g", rank, row[rank]);
291+
}
292+
std::fprintf(stderr, "\n");
293+
}
294+
const uint64_t output_token = static_cast<uint64_t>(
295+
ptn::greedy_argmax_last_row(logits, vocabulary_size));
296+
return output_token;
297+
};
298+
299+
const size_t limit = ptn::generation_limit(
300+
prompt_tokens.size(),
301+
static_cast<size_t>(FLAGS_max_new_tokens),
302+
static_cast<size_t>(FLAGS_max_seq_length),
303+
kv_cache_length(method),
304+
static_cast<size_t>(token_shape[1]));
305+
// Prefill sends the whole prompt in one call.
306+
if (prompt_tokens.size() > static_cast<size_t>(token_shape[1])) {
307+
throw std::runtime_error(
308+
"prompt of " + std::to_string(prompt_tokens.size()) +
309+
" tokens exceeds the method's " + std::to_string(token_shape[1]) +
310+
"-token input; chunked prefill is not implemented");
311+
}
312+
313+
// The same prompt and positions overwrite the same cache rows. Keep this
314+
// explicitly opt-in so the normal timing still includes first-use GPU
315+
// warmup and can be compared with standalone benchmarks either way.
316+
if (FLAGS_warmup_prefill) {
317+
if (!FLAGS_dump_first_logits.empty() || FLAGS_print_top_logits) {
318+
throw std::invalid_argument(
319+
"warmup_prefill cannot be combined with logits diagnostics");
320+
}
321+
run_tokens(prompt_tokens, 0);
322+
}
323+
324+
const Clock::time_point prompt_start = Clock::now();
325+
uint64_t next_token = run_tokens(prompt_tokens, 0);
326+
const Clock::time_point prompt_end = Clock::now();
327+
328+
size_t generated = 0;
329+
std::vector<uint64_t> generated_ids;
330+
uint64_t previous_token = prompt_tokens.back();
331+
std::printf("text: ");
332+
std::fflush(stdout);
333+
double decode_ms = 0.0;
334+
size_t decode_steps = 0;
335+
while (generated < limit && !eos_ids.contains(next_token)) {
336+
auto decoded = tokenizer->decode(previous_token, next_token);
337+
if (!decoded.ok()) {
338+
throw std::runtime_error(
339+
"failed to decode token: tokenizer error " +
340+
std::to_string(static_cast<uint32_t>(decoded.error())));
341+
}
342+
std::printf("%s", decoded.get().c_str());
343+
std::fflush(stdout);
344+
previous_token = next_token;
345+
if (FLAGS_print_token_ids) {
346+
generated_ids.push_back(next_token);
347+
}
348+
++generated;
349+
if (generated < limit) {
350+
const Clock::time_point step_start = Clock::now();
351+
next_token = run_tokens(
352+
{previous_token},
353+
static_cast<int64_t>(prompt_tokens.size() + generated - 1));
354+
decode_ms += elapsed_ms(step_start, Clock::now());
355+
++decode_steps;
356+
}
357+
}
358+
std::printf("\n");
359+
if (FLAGS_print_token_ids) {
360+
std::printf("token_ids: ");
361+
for (size_t i = 0; i < generated_ids.size(); ++i) {
362+
std::printf(
363+
"%s%llu",
364+
i == 0 ? "" : ",",
365+
static_cast<unsigned long long>(generated_ids[i]));
366+
}
367+
std::printf("\n");
368+
}
369+
370+
const double prompt_ms = elapsed_ms(prompt_start, prompt_end);
371+
const double prompt_tokens_per_second = prompt_ms > 0.0
372+
? 1000.0 * static_cast<double>(prompt_tokens.size()) / prompt_ms
373+
: 0.0;
374+
const double tokens_per_second = decode_ms > 0.0
375+
? 1000.0 * static_cast<double>(decode_steps) / decode_ms
376+
: 0.0;
377+
std::printf(
378+
"prompt: %zu tokens, %.3f ms (%.3f tokens/s)\n",
379+
prompt_tokens.size(),
380+
prompt_ms,
381+
prompt_tokens_per_second);
382+
std::printf(
383+
"decode: %zu steps, %.3f ms (%.3f steps/s)\n",
384+
decode_steps,
385+
decode_ms,
386+
tokens_per_second);
387+
return 0;
388+
} catch (const std::exception& e) {
389+
std::fprintf(stderr, "error: %s\n", e.what());
390+
return 2;
391+
}
392+
}
393+
394+
} // namespace ptn

0 commit comments

Comments
 (0)