Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions extension/llm/runner/_llm_runner.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ class Stats:
"""End time of tokenizer encoding in milliseconds."""

model_execution_start_ms: int
"""Start time of model execution in milliseconds."""
"""Start time of the most recent model execution window in milliseconds."""

model_execution_end_ms: int
"""End time of model execution in milliseconds."""
"""End time of the most recent model execution window in milliseconds."""

prompt_eval_end_ms: int
"""End time of prompt evaluation in milliseconds."""
Expand All @@ -100,6 +100,9 @@ class Stats:
aggregate_sampling_time_ms: int
"""Total time spent in sampling across all tokens."""

aggregate_model_execution_time_ms: int
"""Total time spent in model execution across all forward calls."""

num_prompt_tokens: int
"""Number of tokens in the input prompt."""

Expand Down
9 changes: 7 additions & 2 deletions extension/llm/runner/llm_runner_helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,16 @@ std::unique_ptr<TextLLMRunner> create_text_llm_runner(
float init_temp = temperature == -1.0f ? 0.0f : temperature;
auto sampler = std::make_unique<Sampler>(vocab_size, init_temp);

auto stats = std::make_unique<Stats>();

// Create text_decoder_runner
ET_LOG(Info, "Using method: %s", method_name.c_str());
auto text_decoder_runner = std::make_unique<TextDecoderRunner>(
module.get(), io_manager.get(), method_name, std::move(sampler));
module.get(),
io_manager.get(),
method_name,
std::move(sampler),
stats.get());

// Create text_prefiller
auto text_prefiller = std::make_unique<TextPrefiller>(
Expand All @@ -284,7 +290,6 @@ std::unique_ptr<TextLLMRunner> create_text_llm_runner(
metadata.at(kMaxSeqLen));

// Create text_token_generator with stats
auto stats = std::make_unique<Stats>();
auto text_token_generator = std::make_unique<TextTokenGenerator>(
tokenizer.get(),
text_decoder_runner.get(),
Expand Down
3 changes: 3 additions & 0 deletions extension/llm/runner/pybindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ PYBIND11_MODULE(_llm_runner, m) {
.def_readonly("inference_end_ms", &Stats::inference_end_ms)
.def_readonly(
"aggregate_sampling_time_ms", &Stats::aggregate_sampling_time_ms)
.def_readonly(
"aggregate_model_execution_time_ms",
&Stats::aggregate_model_execution_time_ms)
.def_readonly("num_prompt_tokens", &Stats::num_prompt_tokens)
.def_readonly("num_generated_tokens", &Stats::num_generated_tokens)
.def("on_sampling_begin", &Stats::on_sampling_begin)
Expand Down
24 changes: 21 additions & 3 deletions extension/llm/runner/stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ struct ET_EXPERIMENTAL Stats {
long inference_start_ms = 0;
// End of the tokenizer encode time.
long token_encode_end_ms = 0;
// Start of the model execution (forward function) time.
/// Start timestamp of the most recent model execution (forward) window.

@JakeStevens JakeStevens Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: all of these edited comments (line 35, 37, 49) have an extra slash, please remove

long model_execution_start_ms = 0;
// End of the model execution (forward function) time.
/// End timestamp of the most recent model execution (forward) window.
long model_execution_end_ms = 0;
// prompt_eval_end_ms: Prompt array allocation and tokenization. Ends right
// before the inference loop starts
Expand All @@ -45,6 +45,8 @@ struct ET_EXPERIMENTAL Stats {
long inference_end_ms = 0;
// Keep a running total of the time spent in sampling.
long aggregate_sampling_time_ms = 0;
/// Running total of time spent in model execution (forward).
long aggregate_model_execution_time_ms = 0;
// Token count from prompt
int64_t num_prompt_tokens = 0;
// Token count from generated (total - prompt)
Expand All @@ -66,6 +68,16 @@ struct ET_EXPERIMENTAL Stats {
aggregate_sampling_timer_start_timestamp = 0;
}

inline void on_model_execution_begin() {
model_execution_start_ms = time_in_ms();
}

inline void on_model_execution_end() {
model_execution_end_ms = time_in_ms();
aggregate_model_execution_time_ms +=
model_execution_end_ms - model_execution_start_ms;
}

void reset(bool all_stats = false) {
// Not resetting model_load_start_ms and model_load_end_ms because reset is
// typically called after warmup and before running the actual run.
Expand All @@ -77,10 +89,13 @@ struct ET_EXPERIMENTAL Stats {
model_load_end_ms = 0;
}
inference_start_ms = 0;
model_execution_start_ms = 0;
model_execution_end_ms = 0;
prompt_eval_end_ms = 0;
first_token_ms = 0;
inference_end_ms = 0;
aggregate_sampling_time_ms = 0;
aggregate_model_execution_time_ms = 0;
num_prompt_tokens = 0;
num_generated_tokens = 0;
gpu_total_bytes = static_cast<uint64_t>(-1);
Expand Down Expand Up @@ -121,10 +136,13 @@ inline std::string stats_to_json_string(const Stats& stats) {
<< "\"model_load_end_ms\":" << stats.model_load_end_ms << ","
<< "\"inference_start_ms\":" << stats.inference_start_ms << ","
<< "\"inference_end_ms\":" << stats.inference_end_ms << ","
<< "\"model_execution_start_ms\":" << stats.model_execution_start_ms << ","
<< "\"model_execution_end_ms\":" << stats.model_execution_end_ms << ","
<< "\"prompt_eval_end_ms\":" << stats.prompt_eval_end_ms << ","
<< "\"first_token_ms\":" << stats.first_token_ms << ","
<< "\"aggregate_sampling_time_ms\":" << stats.aggregate_sampling_time_ms
<< ",";
<< ",\"aggregate_model_execution_time_ms\":"
<< stats.aggregate_model_execution_time_ms << ",";
// Only include GPU fields in the JSON if gpu_total_bytes is valid (not
// equal to sentinel -1)
if (stats.gpu_total_bytes != static_cast<uint64_t>(-1)) {
Expand Down
39 changes: 39 additions & 0 deletions extension/llm/runner/test/test_generation_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,49 @@

using namespace ::testing;
using executorch::extension::llm::GenerationConfig;
using executorch::extension::llm::Stats;
using executorch::extension::llm::stats_to_json_string;

namespace {
class GenerationConfigTest : public Test {};

TEST(StatsTest, SerializesAndResetsModelExecutionTimestamps) {
Stats stats;
stats.model_execution_start_ms = 123;
stats.model_execution_end_ms = 456;
stats.aggregate_model_execution_time_ms = 333;

const std::string json = stats_to_json_string(stats);
EXPECT_NE(json.find("\"model_execution_start_ms\":123"), std::string::npos);
EXPECT_NE(json.find("\"model_execution_end_ms\":456"), std::string::npos);
EXPECT_NE(
json.find("\"aggregate_model_execution_time_ms\":333"),
std::string::npos);
EXPECT_LT(
json.find("\"model_execution_start_ms\""),
json.find("\"model_execution_end_ms\""));

stats.reset();
EXPECT_EQ(stats.model_execution_start_ms, 0);
EXPECT_EQ(stats.model_execution_end_ms, 0);
EXPECT_EQ(stats.aggregate_model_execution_time_ms, 0);
}

TEST(StatsTest, RecordsLatestModelExecutionAndAggregateTime) {
Stats stats;
stats.model_execution_start_ms = 1;
stats.aggregate_model_execution_time_ms = 7;

stats.on_model_execution_begin();
EXPECT_GT(stats.model_execution_start_ms, 1);
stats.on_model_execution_end();

EXPECT_GE(stats.model_execution_end_ms, stats.model_execution_start_ms);
EXPECT_EQ(
stats.aggregate_model_execution_time_ms,
7 + stats.model_execution_end_ms - stats.model_execution_start_ms);
}

TEST_F(GenerationConfigTest, TestResolveMaxNewTokensBothDefault) {
// Test when both seq_len and max_new_tokens are -1 (default)
GenerationConfig config;
Expand Down
18 changes: 16 additions & 2 deletions extension/llm/runner/text_decoder_runner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ TextDecoderRunner::TextDecoderRunner(
Module* module,
IOManager* io_manager,
std::string method_name,
std::unique_ptr<Sampler> sampler)
std::unique_ptr<Sampler> sampler,
Stats* stats)
: module_(module),
io_manager_(io_manager),
method_name_(std::move(method_name)),
sampler_(std::move(sampler)) {}
sampler_(std::move(sampler)),
stats_(stats) {}

// This function is functional, meaning it shouldn't modify any state of the
// input. It should be safe to call multiple times with the same inputs. The
Expand Down Expand Up @@ -66,7 +68,13 @@ ::executorch::runtime::Result<executorch::aten::Tensor> TextDecoderRunner::step(
io_manager_->prepare_decode(tokens, start_pos_tensor, method_name_);
ET_CHECK_OK_OR_RETURN_ERROR(inputs_res.error());
inputs = inputs_res.get();
if (stats_ != nullptr) {
stats_->on_model_execution_begin();
}
auto outputs_res = module_->execute(method_name_, inputs);
if (stats_ != nullptr) {
stats_->on_model_execution_end();
}
ET_CHECK_OK_OR_RETURN_ERROR(outputs_res.error());

auto update_err =
Expand All @@ -86,7 +94,13 @@ ::executorch::runtime::Result<executorch::aten::Tensor> TextDecoderRunner::step(
(void)start_pos; // unused

std::vector<runtime::EValue> inputs{tokens};
if (stats_ != nullptr) {
stats_->on_model_execution_begin();
}
auto outputs_res = module_->execute(method_name_, inputs);
if (stats_ != nullptr) {
stats_->on_model_execution_end();
}
ET_CHECK_OK_OR_RETURN_ERROR(outputs_res.error());
ET_CHECK_MSG(
outputs_res.get().size() == 1,
Expand Down
6 changes: 5 additions & 1 deletion extension/llm/runner/text_decoder_runner.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ namespace executorch {
namespace extension {
namespace llm {

struct Stats;

class ET_EXPERIMENTAL TextDecoderRunner {
public:
explicit TextDecoderRunner(
Module* module,
IOManager* io_manager,
std::string method_name = "forward",
std::unique_ptr<Sampler> sampler = nullptr);
std::unique_ptr<Sampler> sampler = nullptr,
Stats* stats = nullptr);

virtual ~TextDecoderRunner() = default;

Expand Down Expand Up @@ -102,6 +105,7 @@ class ET_EXPERIMENTAL TextDecoderRunner {
IOManager* io_manager_;
std::string method_name_;
std::unique_ptr<Sampler> sampler_;
Stats* stats_;
};

} // namespace llm
Expand Down
2 changes: 2 additions & 0 deletions extension/llm/runner/text_llm_runner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ Error TextLLMRunner::generate(
// return a response token.

stats_->inference_start_ms = time_in_ms();
stats_->model_execution_start_ms = 0;
stats_->model_execution_end_ms = 0;

// Get max_seq_len for single prefill chunk limit
int64_t max_seq_len = metadata_.at(kMaxSeqLen);
Expand Down
Loading