Skip to content

Commit c48a9ae

Browse files
Krzysztof Rymskicopybara-github
authored andcommitted
Helper for doing benchmarks, supports prefill , generation in both single query and multi query for generation
PiperOrigin-RevId: 955262243
1 parent a5c3476 commit c48a9ae

2 files changed

Lines changed: 353 additions & 0 deletions

File tree

BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,23 @@ cc_binary(
10481048
],
10491049
)
10501050

1051+
cc_binary(
1052+
name = "attention_benchmark",
1053+
srcs = ["evals/attention_benchmark.cc"],
1054+
deps = [
1055+
":args",
1056+
":configs",
1057+
":gemma_args",
1058+
":gemma_lib",
1059+
":mat",
1060+
":threading_context",
1061+
":tokenizer",
1062+
"//io",
1063+
"@highway//:abort_header_only",
1064+
"@highway//:timer",
1065+
],
1066+
)
1067+
10511068
cc_binary(
10521069
name = "single_benchmark",
10531070
srcs = ["evals/benchmark.cc"],

evals/attention_benchmark.cc

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
// Copyright 2026 Google LLC
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
// Standalone benchmark tool for Gemma attention implementations.
17+
//
18+
// This binary benchmarks different attention implementations (e.g., standard
19+
// flash attention, tiled attention, scalar attention) across various sequence
20+
// lengths and batch sizes by simulating history in the KV cache and measuring
21+
// decode generation speed.
22+
//
23+
// =============================================================================
24+
// Usage Example
25+
// =============================================================================
26+
//
27+
// bazel run -c opt //third_party/gemma_cpp:attention_benchmark -- \
28+
// --weights /path/to/weights.sbs \
29+
// --tokenizer /path/to/tokenizer.spm \
30+
// --attention_impl tiled_flash \
31+
// --context_lengths 1024,4096,8192 \
32+
// --prefill_length 1 \
33+
// --benchmark_tokens 32 \
34+
// --benchmark_batch_size 1
35+
//
36+
// =============================================================================
37+
// Supported Benchmark Flags
38+
// =============================================================================
39+
// --context_lengths: Comma-separated KV cache history lengths to
40+
// benchmark (default: '1024,8192,32768'; aliases:
41+
// --generation_context_lengths, --prompt_lengths)
42+
// --prefill_length: Number of prompt tokens before decode generation
43+
// (default: 1; alias: --generation_prefill_length)
44+
// --benchmark_tokens: Number of decode tokens to generate (default: 32)
45+
// --benchmark_batch_size: Number of queries to benchmark in a batch
46+
// (default: 1)
47+
48+
#include <stddef.h>
49+
50+
#include <algorithm>
51+
#include <cstdlib>
52+
#include <cstring>
53+
#include <iostream>
54+
#include <string>
55+
#include <vector>
56+
57+
#include "gemma/configs.h"
58+
#include "gemma/gemma.h"
59+
#include "gemma/gemma_args.h"
60+
#include "gemma/tokenizer.h"
61+
#include "io/io.h"
62+
#include "util/args.h"
63+
#include "util/mat.h"
64+
#include "util/threading_context.h"
65+
#include "hwy/base.h"
66+
#include "hwy/timer.h"
67+
68+
namespace gcpp {
69+
70+
class AttentionBenchmarkArgs : public ArgsBase<AttentionBenchmarkArgs> {
71+
public:
72+
AttentionBenchmarkArgs(int argc, char* argv[], ConsumedArgs& consumed) {
73+
InitAndParse(argc, argv, consumed);
74+
}
75+
76+
std::string context_lengths;
77+
std::string generation_context_lengths;
78+
std::string prompt_lengths;
79+
std::string prefill_lengths;
80+
size_t prefill_length;
81+
size_t generation_prefill_length;
82+
size_t benchmark_tokens;
83+
size_t benchmark_batch_size;
84+
85+
template <class Visitor>
86+
void ForEach(const Visitor& visitor) {
87+
visitor(context_lengths, "context_lengths",
88+
std::string("1024,8192,32768"),
89+
"Comma-separated list of simulated KV cache history lengths to "
90+
"benchmark",
91+
2);
92+
visitor(generation_context_lengths, "generation_context_lengths",
93+
std::string(""),
94+
"Alias for context_lengths", 2);
95+
visitor(prompt_lengths, "prompt_lengths", std::string(""),
96+
"Alias for context_lengths", 2);
97+
visitor(prefill_lengths, "prefill_lengths", std::string(""),
98+
"Alias for context_lengths", 2);
99+
visitor(prefill_length, "prefill_length", (size_t)1,
100+
"Number of prompt tokens to prefill before generation (default: 1)",
101+
2);
102+
visitor(generation_prefill_length, "generation_prefill_length", (size_t)0,
103+
"Alias for prefill_length", 2);
104+
visitor(benchmark_tokens, "benchmark_tokens", (size_t)32,
105+
"Number of decode tokens to generate", 2);
106+
visitor(benchmark_batch_size, "benchmark_batch_size", (size_t)1,
107+
"Batch size (number of queries) to benchmark", 2);
108+
}
109+
};
110+
111+
static std::vector<std::string> SplitString(const std::string& str,
112+
char delimiter) {
113+
std::vector<std::string> result;
114+
size_t start = 0;
115+
size_t end = str.find(delimiter);
116+
while (end != std::string::npos) {
117+
result.push_back(str.substr(start, end - start));
118+
start = end + 1;
119+
end = str.find(delimiter, start);
120+
}
121+
result.push_back(str.substr(start));
122+
return result;
123+
}
124+
125+
static bool ParseSizeT(const std::string& str, size_t* out) {
126+
if (str.empty()) return false;
127+
char* endptr = nullptr;
128+
*out = std::strtoul(str.c_str(), &endptr, 10);
129+
return endptr != str.c_str() && *endptr == '\0';
130+
}
131+
132+
} // namespace gcpp
133+
134+
135+
namespace {
136+
137+
std::vector<int> GenerateSyntheticPrompt(const gcpp::Gemma& gemma,
138+
size_t length) {
139+
std::vector<int> tokens;
140+
tokens.reserve(length);
141+
tokens.push_back(gcpp::BOS_ID);
142+
143+
int valid_token = 500; // Arbitrary non-EOS, non-control token ID fallback.
144+
std::vector<int> ids;
145+
if (gemma.Tokenizer().Encode(" the", &ids) && !ids.empty()) {
146+
valid_token = ids[0];
147+
}
148+
for (size_t i = 1; i < length; ++i) {
149+
tokens.push_back(valid_token);
150+
}
151+
return tokens;
152+
}
153+
154+
// Zero out all allocated buffers in the KV cache to ensure clean state.
155+
void ZeroKVCache(gcpp::KVCache& kv_cache) {
156+
if (kv_cache.compact_local_kv_cache_ptr.HasPtr()) {
157+
gcpp::ZeroInit(kv_cache.compact_local_kv_cache_ptr);
158+
}
159+
if (kv_cache.compact_global_kv_cache_ptr.HasPtr()) {
160+
gcpp::ZeroInit(kv_cache.compact_global_kv_cache_ptr);
161+
}
162+
if (kv_cache.compact_kv_cache_ptr.HasPtr()) {
163+
gcpp::ZeroInit(kv_cache.compact_kv_cache_ptr);
164+
}
165+
if (kv_cache.kv_cache.HasPtr()) {
166+
gcpp::ZeroInit(kv_cache.kv_cache);
167+
}
168+
if (kv_cache.k_cache.HasPtr()) {
169+
gcpp::ZeroInit(kv_cache.k_cache);
170+
}
171+
if (kv_cache.v_cache.HasPtr()) {
172+
gcpp::ZeroInit(kv_cache.v_cache);
173+
}
174+
}
175+
176+
} // namespace
177+
178+
int main(int argc, char** argv) {
179+
gcpp::InternalInit();
180+
gcpp::ConsumedArgs consumed(argc, argv);
181+
gcpp::GemmaArgs args(argc, argv, consumed);
182+
gcpp::AttentionBenchmarkArgs bench_args(argc, argv, consumed);
183+
184+
if (gcpp::HasHelp(argc, argv)) {
185+
args.Help();
186+
bench_args.Help();
187+
return 0;
188+
}
189+
190+
consumed.AbortIfUnconsumed();
191+
192+
// Instantiate model
193+
gcpp::ThreadingContext ctx(args.threading);
194+
gcpp::MatMulEnv env(ctx);
195+
gcpp::Gemma gemma(args, ctx);
196+
197+
gcpp::RuntimeConfig runtime_config{};
198+
args.inference.CopyTo(runtime_config);
199+
runtime_config.verbosity = args.inference.verbosity;
200+
size_t decode_tokens = bench_args.benchmark_tokens;
201+
size_t num_queries = bench_args.benchmark_batch_size;
202+
if (num_queries == 0) {
203+
std::cerr << "Benchmark batch size must be > 0" << std::endl;
204+
return 1;
205+
}
206+
207+
std::string lengths_str_arg = bench_args.context_lengths;
208+
if (lengths_str_arg.empty()) {
209+
lengths_str_arg = bench_args.generation_context_lengths;
210+
}
211+
if (lengths_str_arg.empty()) {
212+
lengths_str_arg = bench_args.prompt_lengths;
213+
}
214+
if (lengths_str_arg.empty()) {
215+
lengths_str_arg = bench_args.prefill_lengths;
216+
}
217+
std::vector<std::string> history_lengths_strs =
218+
gcpp::SplitString(lengths_str_arg, ',');
219+
220+
size_t prefill_len = bench_args.prefill_length;
221+
if (prefill_len == 0 && bench_args.generation_prefill_length > 0) {
222+
prefill_len = bench_args.generation_prefill_length;
223+
}
224+
if (prefill_len == 0) prefill_len = 1;
225+
226+
std::cout << "--- Generation Benchmark (Simulated KV Cache History) ---"
227+
<< std::endl;
228+
std::cout << "Batch Size: " << num_queries << std::endl;
229+
std::cout << "Prefill Priming Tokens: " << prefill_len << std::endl;
230+
std::cout << "Decode Tokens: " << decode_tokens << std::endl;
231+
std::cout << "---------------------------------------------------------"
232+
<< std::endl;
233+
234+
for (const auto& len_str : history_lengths_strs) {
235+
if (len_str.empty()) continue;
236+
size_t history_len;
237+
if (!gcpp::ParseSizeT(len_str, &history_len)) {
238+
std::cerr << "Invalid context length: " << len_str << std::endl;
239+
continue;
240+
}
241+
242+
size_t max_seq_len = gemma.Config().max_seq_len;
243+
if (history_len + prefill_len + decode_tokens > max_seq_len) {
244+
size_t clamped_len = (max_seq_len > prefill_len + decode_tokens)
245+
? max_seq_len - prefill_len - decode_tokens
246+
: 0;
247+
std::cerr << "Warning: history_len=" << history_len
248+
<< " + prefill_len=" << prefill_len
249+
<< " + decode_tokens=" << decode_tokens
250+
<< " exceeds max_seq_len (" << max_seq_len
251+
<< "), clamping history_len to " << clamped_len << "."
252+
<< std::endl;
253+
history_len = clamped_len;
254+
}
255+
256+
std::cout << "\nSimulated History Length: " << history_len << " tokens"
257+
<< std::endl;
258+
259+
size_t total_capacity = history_len + prefill_len + decode_tokens + 32;
260+
261+
size_t original_seq_len = args.inference.seq_len;
262+
args.inference.seq_len = total_capacity;
263+
264+
gcpp::RuntimeConfig gen_config = runtime_config;
265+
gen_config.max_generated_tokens = decode_tokens;
266+
gen_config.decode_qbatch_size = num_queries;
267+
gen_config.sample_func = [](size_t, size_t, gcpp::Logits,
268+
size_t) -> gcpp::TokenAndProb {
269+
// Return an arbitrary non-EOS token ID so benchmarks never terminate early.
270+
return gcpp::TokenAndProb{500, 1.0f};
271+
};
272+
273+
std::vector<gcpp::KVCache> kv_caches;
274+
kv_caches.reserve(num_queries);
275+
for (size_t i = 0; i < num_queries; ++i) {
276+
kv_caches.emplace_back(gemma.Config(), args.inference, gen_config,
277+
ctx.allocator);
278+
ZeroKVCache(kv_caches.back());
279+
}
280+
281+
args.inference.seq_len = original_seq_len;
282+
283+
std::vector<int> tokens = GenerateSyntheticPrompt(gemma, prefill_len);
284+
285+
gcpp::TimingInfo timing_info;
286+
287+
size_t generated = 0;
288+
size_t total_prefill_tokens = num_queries * prefill_len;
289+
double start_time = hwy::platform::Now();
290+
double prefill_end_time = start_time;
291+
292+
auto stream_token = [&generated, total_prefill_tokens,
293+
&prefill_end_time](int, float) {
294+
++generated;
295+
if (generated == total_prefill_tokens) {
296+
prefill_end_time = hwy::platform::Now();
297+
}
298+
return true;
299+
};
300+
301+
auto batch_stream_token = [&generated, total_prefill_tokens,
302+
&prefill_end_time](size_t, size_t, int,
303+
float) {
304+
++generated;
305+
if (generated == total_prefill_tokens) {
306+
prefill_end_time = hwy::platform::Now();
307+
}
308+
return true;
309+
};
310+
311+
gen_config.stream_token = stream_token;
312+
gen_config.batch_stream_token = batch_stream_token;
313+
314+
gcpp::AllQueries all_queries(
315+
tokens, /*start_pos=*/history_len, /*prefix_end=*/0,
316+
hwy::Span<gcpp::KVCache>(kv_caches.data(), kv_caches.size()));
317+
318+
gemma.GenerateBatch(gen_config, all_queries, env, timing_info);
319+
320+
double end_time = hwy::platform::Now();
321+
double decode_seconds = end_time - prefill_end_time;
322+
size_t actual_decode_tokens =
323+
generated > total_prefill_tokens ? generated - total_prefill_tokens
324+
: 0;
325+
326+
std::cout << "Decode: " << actual_decode_tokens << " tokens in "
327+
<< decode_seconds << "s ("
328+
<< (decode_seconds > 0 ? actual_decode_tokens / decode_seconds
329+
: 0.0)
330+
<< " tok/s)" << std::endl;
331+
std::cout << "---------------------------------------------------------"
332+
<< std::endl;
333+
}
334+
335+
return 0;
336+
}

0 commit comments

Comments
 (0)