|
| 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