• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) Facebook, Inc. and its affiliates.
2 // All rights reserved.
3 //
4 // Copyright 2019 Google LLC
5 //
6 // This source code is licensed under the BSD-style license found in the
7 // LICENSE file in the root directory of this source tree.
8 
9 #include <algorithm>
10 #include <cfloat>
11 #include <chrono>
12 #include <cmath>
13 #include <functional>
14 #include <limits>
15 #include <mutex>
16 #include <random>
17 #include <vector>
18 
19 #include <benchmark/benchmark.h>
20 #ifdef BENCHMARK_GEMMLOWP
21 #include "gemmlowp/public/gemmlowp.h"
22 #endif  // BENCHMARK_GEMMLOWP
23 #ifdef BENCHMARK_RUY
24 #include "ruy/ruy.h"
25 #endif  // BENCHMARK_RUY
26 #include "bench/gemm.h"
27 #include "bench/utils.h"
28 #include <xnnpack/AlignedAllocator.h>
29 #include <xnnpack/common.h>
30 #include <xnnpack/gemm.h>
31 #include <xnnpack/pack.h>
32 #include <xnnpack/params-init.h>
33 #include <xnnpack/params.h>
34 
35 
GEMMBenchmark(benchmark::State & state,xnn_qu8_gemm_ukernel_function gemm,size_t mr,size_t nr,size_t kr,size_t sr,benchmark::utils::IsaCheckFunction isa_check=nullptr)36 static void GEMMBenchmark(benchmark::State& state,
37   xnn_qu8_gemm_ukernel_function gemm,
38   size_t mr, size_t nr, size_t kr, size_t sr,
39   benchmark::utils::IsaCheckFunction isa_check = nullptr)
40 {
41   const size_t mc = state.range(0);
42   const size_t nc = state.range(1);
43   const size_t kc = state.range(2);
44 
45   const size_t nc_stride = benchmark::utils::RoundUp(nc, nr);
46   const size_t kc_stride = benchmark::utils::RoundUp(kc, kr);
47 
48   std::random_device random_device;
49   auto rng = std::mt19937(random_device());
50   auto i32rng = std::bind(std::uniform_int_distribution<int32_t>(-10000, 10000), std::ref(rng));
51   auto u8rng = std::bind(std::uniform_int_distribution<uint32_t>(0, std::numeric_limits<uint8_t>::max()), std::ref(rng));
52 
53   std::vector<uint8_t> a(mc * kc);
54   std::generate(a.begin(), a.end(), std::ref(u8rng));
55   std::vector<uint8_t> k(nc * kc);
56   std::generate(k.begin(), k.end(), std::ref(u8rng));
57   std::vector<int32_t> b(nc);
58   std::generate(b.begin(), b.end(), std::ref(i32rng));
59 
60   const size_t w_elements = kc_stride * nc_stride + nc_stride * sizeof(int32_t) / sizeof(uint8_t);
61   const size_t c_elements = mc * nc;
62   const size_t num_buffers = 1 +
63     benchmark::utils::DivideRoundUp<size_t>(benchmark::utils::GetMaxCacheSize(),
64       sizeof(uint8_t) * (w_elements + c_elements));
65 
66   std::vector<uint8_t, AlignedAllocator<uint8_t, 32>> w(w_elements * num_buffers);
67   std::fill(w.begin(), w.end(), 0);
68   const xnn_qu8_packing_params packing_params = { 127, 127 };
69   xnn_pack_qu8_gemm_goi_w(1 /* groups */, nc, kc, nr, kr, sr, k.data(), b.data(), w.data(), &packing_params);
70   std::vector<uint8_t> c(c_elements * num_buffers);
71   std::fill(c.begin(), c.end(), 0xA5);
72 
73   union xnn_qu8_gemm_params quantization_params =
74     xnn_init_qu8_gemm_params(127, 0.75f, 127, 1, 254);
75 
76   size_t buffer_index = 0;
77   for (auto _ : state) {
78     // Use circular buffers (exceeding cache size) and prefetch to control cache state:
79     // - A is always in L1 cache (if fits, otherwise L2, L3, etc)
80     // - W is not in cache (for any cache level)
81     // - C is not in cache (for any cache level)
82     state.PauseTiming();
83     benchmark::utils::PrefetchToL1(a.data(), a.size() * sizeof(uint8_t));
84     buffer_index = (buffer_index + 1) % num_buffers;
85     state.ResumeTiming();
86 
87     for (uint32_t m = 0; m < mc; m += mr) {
88       const uint32_t mb = min(mc - m, mr);
89       for (uint32_t n = 0; n < nc; n += nr) {
90         const uint32_t nb = min(nc - n, nr);
91         gemm(
92           mb, nb, kc * sizeof(uint8_t),
93           a.data() + m * kc, kc * sizeof(uint8_t),
94           w.data() + (w_elements * buffer_index + n * (kc_stride + sizeof(int32_t))) / sizeof(uint8_t),
95           c.data() + (mc * buffer_index + m) * nc + n, nc * sizeof(uint8_t), nr * sizeof(uint8_t),
96           &quantization_params);
97       }
98     }
99   }
100 
101   const uint64_t cpu_frequency = benchmark::utils::GetCurrentCpuFrequency();
102   if (cpu_frequency != 0) {
103     state.counters["cpufreq"] = cpu_frequency;
104   }
105 
106   state.counters["OPS"] = benchmark::Counter(
107     uint64_t(state.iterations()) * 2 * mc * nc * kc, benchmark::Counter::kIsRate);
108 }
109 
110 #ifdef BENCHMARK_GEMMLOWP
111 struct GemmlowpOutputPipeline {
112   typedef gemmlowp::VectorMap<const int32_t, gemmlowp::VectorShape::Col> ColVectorMap;
113   typedef std::tuple<
114       gemmlowp::OutputStageBiasAddition<ColVectorMap>,
115       gemmlowp::OutputStageQuantizeDownInt32ToUint8ScaleByFixedPoint,
116       gemmlowp::OutputStageClamp,
117       gemmlowp::OutputStageSaturatingCastToUint8>
118       Pipeline;
119 
MakeGemmlowpOutputPipeline120   static Pipeline Make(
121       const int32_t* bias_data,
122       int output_rows,
123       int32_t output_offset,
124       int32_t output_multiplier,
125       int output_shift,
126       int32_t output_activation_min,
127       int32_t output_activation_max)
128   {
129     ColVectorMap bias_vector(bias_data, output_rows);
130     gemmlowp::OutputStageBiasAddition<ColVectorMap> bias_addition_stage;
131     bias_addition_stage.bias_vector = bias_vector;
132     gemmlowp::OutputStageQuantizeDownInt32ToUint8ScaleByFixedPoint quantize_down_stage;
133     quantize_down_stage.result_offset_after_shift = output_offset;
134     quantize_down_stage.result_fixedpoint_multiplier = output_multiplier;
135     quantize_down_stage.result_shift = output_shift;
136     gemmlowp::OutputStageClamp clamp_stage;
137     clamp_stage.min = output_activation_min;
138     clamp_stage.max = output_activation_max;
139     gemmlowp::OutputStageSaturatingCastToUint8 saturating_cast_stage;
140     return std::make_tuple(bias_addition_stage, quantize_down_stage, clamp_stage, saturating_cast_stage);
141   }
142 };
143 
GemmlowpBenchmark(benchmark::State & state,uint32_t threads)144 static void GemmlowpBenchmark(benchmark::State& state, uint32_t threads)
145 {
146   const size_t mc = state.range(0);
147   const size_t nc = state.range(1);
148   const size_t kc = state.range(2);
149 
150   std::random_device random_device;
151   auto rng = std::mt19937(random_device());
152   auto i32rng = std::bind(std::uniform_int_distribution<int32_t>(-10000, 10000), std::ref(rng));
153   auto u8rng = std::bind(std::uniform_int_distribution<uint32_t>(0, std::numeric_limits<uint8_t>::max()), std::ref(rng));
154 
155   std::vector<uint8_t> a(mc * kc);
156   std::generate(a.begin(), a.end(), std::ref(u8rng));
157 
158   const size_t kElements = nc * kc;
159   const size_t bElements = nc;
160   const size_t c_elements = mc * nc;
161   const size_t num_buffers = 1 +
162     benchmark::utils::DivideRoundUp<size_t>(benchmark::utils::GetMaxCacheSize(),
163       kElements * sizeof(uint8_t) + bElements * sizeof(int32_t) + c_elements * sizeof(uint8_t));
164 
165   std::vector<uint8_t> k(kElements * num_buffers);
166   std::generate(k.begin(), k.end(), std::ref(u8rng));
167   std::vector<int32_t> b(bElements * num_buffers);
168   std::generate(b.begin(), b.end(), std::ref(i32rng));
169   std::vector<uint8_t> c(c_elements * num_buffers);
170   std::fill(c.begin(), c.end(), 0xA5);
171 
172   gemmlowp::MultiThreadGemmContext threadingContext;
173   threadingContext.set_max_num_threads(threads);
174 
175   size_t buffer_index = 0;
176   for (auto _ : state) {
177     state.PauseTiming();
178     benchmark::utils::PrefetchToL1(a.data(), a.size() * sizeof(uint8_t));
179     buffer_index = (buffer_index + 1) % num_buffers;
180     state.ResumeTiming();
181 
182     gemmlowp::MatrixMap<const uint8_t, gemmlowp::MapOrder::RowMajor> AM(a.data(), mc, kc, kc);
183     gemmlowp::MatrixMap<const uint8_t, gemmlowp::MapOrder::ColMajor> BM(k.data() + buffer_index * kElements, kc, nc, kc);
184     gemmlowp::MatrixMap<uint8_t, gemmlowp::MapOrder::RowMajor> CM(c.data() + buffer_index * c_elements, mc, nc, nc);
185     const auto& outputPipeline = GemmlowpOutputPipeline::Make(b.data() + buffer_index * bElements, nc, 127, 127, 127, 0, 255);
186     gemmlowp::GemmWithOutputPipeline<uint8_t, uint8_t, gemmlowp::L8R8WithLhsNonzeroBitDepthParams>(
187         &threadingContext, AM, BM, &CM, 127, 127, outputPipeline);
188   }
189 
190   const uint64_t cpu_frequency = benchmark::utils::GetCurrentCpuFrequency();
191   if (cpu_frequency != 0) {
192     state.counters["cpufreq"] = cpu_frequency;
193   }
194 
195   state.counters["OPS"] = benchmark::Counter(
196     uint64_t(state.iterations()) * 2 * mc * nc * kc, benchmark::Counter::kIsRate);
197 }
198 
gemmlowp_st(benchmark::State & state,const char * net)199 static void gemmlowp_st(benchmark::State& state, const char* net)
200 {
201   GemmlowpBenchmark(state, 1);
202 }
203 #endif  // BENCHMARK_GEMMLOWP
204 
205 
206 #ifdef BENCHMARK_RUY
RuyBenchmark(benchmark::State & state,size_t threads)207 static void RuyBenchmark(benchmark::State& state, size_t threads)
208 {
209   const size_t mc = state.range(0);
210   const size_t nc = state.range(1);
211   const size_t kc = state.range(2);
212 
213   std::random_device random_device;
214   auto rng = std::mt19937(random_device());
215   auto i32rng = std::bind(std::uniform_int_distribution<int32_t>(-10000, 10000), std::ref(rng));
216   auto u8rng = std::bind(std::uniform_int_distribution<uint32_t>(0, std::numeric_limits<uint8_t>::max()), std::ref(rng));
217 
218   const size_t num_buffers = 1 +
219     benchmark::utils::DivideRoundUp<size_t>(benchmark::utils::GetMaxCacheSize(),
220       nc * (sizeof(uint8_t) * (mc + kc) + sizeof(int32_t)));
221 
222   std::vector<uint8_t> a(mc * kc);
223   std::generate(a.begin(), a.end(), std::ref(u8rng));
224   std::vector<uint8_t> k(num_buffers * nc * kc);
225   std::generate(k.begin(), k.end(), std::ref(u8rng));
226   std::vector<int32_t> b(num_buffers * nc);
227   std::generate(b.begin(), b.end(), std::ref(i32rng));
228   std::vector<uint8_t> c(num_buffers * nc * mc);
229   std::fill(c.begin(), c.end(), std::nanf(""));
230 
231   // Note: context must be static to avoid the cost of re-creating it for each benchmark.
232   static ruy::Context context;
233   context.set_max_num_threads(threads);
234 
235   ruy::Matrix<uint8_t> ruy_a;
236   ruy::MakeSimpleLayout(nc, kc, ruy::Order::kRowMajor, ruy_a.mutable_layout());
237   ruy_a.set_zero_point(127);
238   ruy::Matrix<uint8_t> ruy_b;
239   ruy::MakeSimpleLayout(kc, mc, ruy::Order::kColMajor, ruy_b.mutable_layout());
240   ruy_b.set_data(a.data());
241   ruy_b.set_zero_point(127);
242   ruy::Matrix<uint8_t> ruy_c;
243   ruy::MakeSimpleLayout(nc, mc, ruy::Order::kColMajor, ruy_c.mutable_layout());
244   ruy_c.set_zero_point(127);
245 
246   ruy::MulParams<int32_t, uint8_t> mul_params;
247   mul_params.set_multiplier_fixedpoint(0x40000000);
248 
249   // ruy::Context uses deferred initialization, which affects percieved GEMM performance. Initialization happens during
250   // the first GEMM calls, and per Benoit Jacob it takes up to ~250 milliseconds for performance to stabilize.
251   // Thus, on the first benchmark, we compute GEMM for 500 milliseconds (to be safe) without recording performance, and
252   // keep the ruy::Context object initialized (by being static) between subsequent benchmarks.
253   static std::once_flag warmup;
254   std::call_once(warmup, [&](){
255     auto start = std::chrono::steady_clock::now();
256     do {
257       ruy_a.set_data(k.data());
258       ruy_c.set_data(c.data());
259       mul_params.set_bias(b.data());
260 
261       ruy::Mul(ruy_a, ruy_b, mul_params, &context, &ruy_c);
262     } while (std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count() < 0.5);
263   });
264 
265   size_t buffer_index = 0;
266   for (auto _ : state) {
267     // Use circular buffers (exceeding cache size) and prefetch to control cache state:
268     // - A is always in L1 cache (if fits, otherwise L2, L3, etc)
269     // - K is not in cache (for any cache level)
270     // - B is not in cache (for any cache level)
271     // - C is not in cache (for any cache level)
272     state.PauseTiming();
273     benchmark::utils::PrefetchToL1(a.data(), a.size() * sizeof(uint8_t));
274     buffer_index = (buffer_index + 1) % num_buffers;
275     state.ResumeTiming();
276 
277     ruy_a.set_data(k.data() + buffer_index * nc * kc);
278     ruy_c.set_data(c.data() + buffer_index * mc * nc);
279     mul_params.set_bias(b.data() + buffer_index * nc);
280 
281     ruy::Mul(ruy_a, ruy_b, mul_params, &context, &ruy_c);
282   }
283 
284   const uint64_t cpu_frequency = benchmark::utils::GetCurrentCpuFrequency();
285   if (cpu_frequency != 0) {
286     state.counters["cpufreq"] = cpu_frequency;
287   }
288 
289   state.counters["OPS"] = benchmark::Counter(
290     uint64_t(state.iterations()) * 2 * mc * nc * kc, benchmark::Counter::kIsRate);
291 }
292 
ruy_st(benchmark::State & state,const char * net)293 static void ruy_st(benchmark::State& state, const char* net)
294 {
295   RuyBenchmark(state, 1);
296 }
297 #endif  // BENCHMARK_RUY
298 
299 
300 #if XNN_ARCH_ARM || XNN_ARCH_ARM64
qu8_gemm_4x8__neon(benchmark::State & state,const char * net)301   static void qu8_gemm_4x8__neon(benchmark::State& state, const char* net) {
302     GEMMBenchmark(state, xnn_qu8_gemm_minmax_ukernel_4x8__neon, 4, 8, 1, 1, benchmark::utils::CheckNEON);
303   }
304 
qu8_gemm_8x8__neon(benchmark::State & state,const char * net)305   static void qu8_gemm_8x8__neon(benchmark::State& state, const char* net) {
306     GEMMBenchmark(state, xnn_qu8_gemm_minmax_ukernel_8x8__neon, 8, 8, 1, 1, benchmark::utils::CheckNEON);
307   }
308 
309   BENCHMARK_GEMM(qu8_gemm_4x8__neon)
BENCHMARK_GEMM(qu8_gemm_8x8__neon)310   BENCHMARK_GEMM(qu8_gemm_8x8__neon)
311 #endif
312 
313 #if XNN_ARCH_X86 || XNN_ARCH_X86_64
314   static void qu8_gemm_4x4c2__sse2(benchmark::State& state, const char* net) {
315     GEMMBenchmark(state, xnn_qu8_gemm_minmax_ukernel_4x4c2__sse2, 4, 4, 2, 1);
316   }
317 
qu8_gemm_2x4c8__sse2(benchmark::State & state,const char * net)318   static void qu8_gemm_2x4c8__sse2(benchmark::State& state, const char* net) {
319     GEMMBenchmark(state, xnn_qu8_gemm_minmax_ukernel_2x4c8__sse2, 2, 4, 8, 1);
320   }
321 
322   BENCHMARK_GEMM(qu8_gemm_4x4c2__sse2)
323   BENCHMARK_GEMM(qu8_gemm_2x4c8__sse2)
324 #endif
325 
326 #ifdef BENCHMARK_RUY
327 BENCHMARK_GEMM(ruy_st)
328 #endif  // BENCHMARK_RUY
329 #ifdef BENCHMARK_GEMMLOWP
330 BENCHMARK_GEMM(gemmlowp_st)
331 #endif  // BENCHMARK_GEMMLOWP
332 
333 #ifndef XNNPACK_BENCHMARK_NO_MAIN
334 BENCHMARK_MAIN();
335 #endif
336