blob: 97e16a61c1388868bc1c4599eadcf7d19ae27a3b [file] [log] [blame]
Joseph Dobson6f8b17d2020-02-11 19:32:11 +00001/*
2 * Copyright (c) 2020 ARM Limited.
3 *
4 * SPDX-License-Identifier: MIT
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to
8 * deal in the Software without restriction, including without limitation the
9 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10 * sell copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24#pragma once
25
26#include "arm_gemm.hpp"
27#include "utils.hpp"
28
29#include "mergeresults.hpp"
30#include "transform.hpp"
31
32#ifdef CYCLE_PROFILING
33#include "profiler.hpp"
34#endif
35
36#include <algorithm>
37#include <cassert>
38
39// Some macros used to decide how much working space to allocate.
40// Round allocations up to the next cache line.
41#define ALLOC_ROUND 64
42#define ROUND_UP(x) ((((x) + ALLOC_ROUND-1) / ALLOC_ROUND) * ALLOC_ROUND)
43
44// Implementation of the GemmCommon abstract class.
45//
46// This implementation interleaves the source matrices in blocks - good for
47// larger matrices.
48namespace arm_gemm {
49
50template<typename strategy, typename To, typename Tr>
51class GemmInterleavedPretransposed2d : public GemmCommon<To, Tr> {
52 typedef typename strategy::operand_type Toi;
53 typedef typename strategy::result_type Tri;
54
55 /* const properties set by constructor */
56 const CPUInfo * const _ci;
57
58 const unsigned int _Msize;
59 const unsigned int _Nsize;
60 const unsigned int _Ksize;
61
62 const unsigned int _nbatches;
63 const unsigned int _nmulti;
64
Joseph Dobson6f8b17d2020-02-11 19:32:11 +000065 const Activation _act;
66
67 const int _maxthreads;
68 int _nthreads;
69
70 /* Blocking info */
71 unsigned int _k_block=0;
72 unsigned int _x_block=0;
73
74 unsigned int _Mround_div=0;
75 unsigned int _Mround=0;
76 unsigned int _Nround_div=0;
77 unsigned int _Nround=0;
78
79 /* Working space, pretransposed buffer */
80 const Toi *_B_transposed=nullptr;
81 void *_working_space=nullptr;
82
83 /* We will need to walk through the blocks of B in a few contexts, so
84 * factor that out. */
85 class blockwalker {
86 private:
87 /* Size loops, etc. based on our parent's configuration */
88 const GemmInterleavedPretransposed2d<strategy, To, Tr> &_parent;
89
90 /* K, X and multi parameters for current iteration. */
91 unsigned int _k0=0, _x0=0, _xmin=0, _xmax=0, _multi=0;
92
93 unsigned int _index=0;
94 bool _done=false;
95 bool _newkblock=true;
96 bool _newmulti=true;
97
98 public:
99 blockwalker(const GemmInterleavedPretransposed2d<strategy, To, Tr> &parent)
100 : _parent(parent)
101 , _xmax { parent._Nsize }
102 { }
103
104 blockwalker(const GemmInterleavedPretransposed2d<strategy, To, Tr> &parent, unsigned int x0, unsigned int xmax)
105 : _parent(parent)
106 , _x0 { x0 }
107 , _xmin { x0 }
108 , _xmax { xmax }
109 {
110 assert(_x0 <= _xmax);
111 }
112
113 unsigned int xmax() {
114 return std::min(_x0 + _parent._x_block, _xmax);
115 }
116
117 unsigned int kmax() {
118 return std::min(_k0 + _parent._k_block, _parent._Ksize);
119 }
120
121 /* Advance to the next block, return false at the end. */
122 bool advance(void) {
123 if (_done) {
124 return false;
125 }
126
127 _newkblock=false;
128 _x0 += _parent._x_block;
129 if (_x0 >= _xmax) {
130 _x0=_xmin;
131 _k0 += _parent._k_block;
132 if (_k0 >= _parent._Ksize) {
133 _k0=0;
134 _multi++;
135 if (_multi >= _parent._nmulti) {
136 _done=true;
137 return false;
138 }
139 _newmulti=true;
140 }
141 _newkblock=true;
142 }
143 _index++;
144
145 return true;
146 }
147
148 unsigned int k0(void) { return _k0; }
149 unsigned int x0(void) { return _x0; }
150 unsigned int multi(void) { return _multi; }
151 unsigned int index(void) { return _index; }
152 bool done(void) { return _done; }
153 bool newkblock(void) { return _newkblock; }
154 };
155
156 // A working size: One of these needed, regardless of thread count. Divided according to window.
157 size_t get_a_working_size() const {
158 return ROUND_UP(sizeof(Toi) * _k_block * _Mround * _nbatches) * 2;
159 }
160
161 // As B will be pretranspose we do not need to alloc any space for it
162 size_t get_b_working_size() const {
163 return 0;
164 }
165
166 // C working size: One needed per thread.
167 size_t get_c_working_size() const {
168 return ROUND_UP(sizeof(Tri) * _x_block * strategy::out_height());
169 }
170
171 // Internal execute function.
172 // This supports both the "pretransposed" and "standard" interfaces via the template parameter.
Georgios Pinitas5aa1a0b2020-07-02 20:02:20 +0100173 void execute_pretranspose(unsigned int m_start, unsigned int m_end, unsigned int n_start, unsigned int n_end, int threadid, int, int) {
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000174 /* Make sure we've been set up correctly. */
175 assert(_B_transposed);
176 assert(_working_space);
177 assert(this->_Aptr);
178 assert(this->_Cptr);
179
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000180#ifdef CYCLE_PROFILING
181 profiler prof;
182#endif
183 strategy strat(_ci);
184
185 /* Translate 'start' and 'end' into a position within the batches and rows. */
186 const unsigned int window_per_batch = _Mround / strategy::out_height();
187 unsigned int batch_0 = m_start / window_per_batch;
188 unsigned int batch_end = m_end / window_per_batch;
189
190 /* Compute the M values to operate on */
191 unsigned int m_0 = (m_start - (batch_0 * window_per_batch)) * strategy::out_height();
192 unsigned int m_max = (m_end - (batch_end * window_per_batch)) * strategy::out_height();
193
194 unsigned int n_0 = std::min(this->_Nsize, strategy::out_width() * n_start);
195 unsigned int n_max = std::min(this->_Nsize, strategy::out_width() * n_end);
196
197 blockwalker current(*this, n_0, n_max);
198
199 int8_t *working_space_bytes = reinterpret_cast<int8_t *>(_working_space);
200
201 auto c_panel_start = working_space_bytes;
202 auto a_panel_start = c_panel_start + get_c_working_size() * _maxthreads;
203
204 auto c_panel = reinterpret_cast<Tri *>(c_panel_start + get_c_working_size() * threadid);
205 auto a_panel = reinterpret_cast<Toi *>(a_panel_start + get_a_working_size() * threadid);
206
207 /* B^t is stored in interleaved panels separated by their K-block component
208 * we want to store a pointer to the start of the current k-page
209 * then when we come to the next k-block we just add the size of the previous to
210 * this base pointer
211 */
212 const Toi *b_panel_start = _B_transposed;
213 // b_panels stores a pointer to the start of our current block inside of the k-block
214 const Toi *b_panel = b_panel_start;
215
216 // newkblock() is always true on the first iteration, so this will be set properly on the first loop.
217 unsigned b_page_size = 0;
218 int kern_k = 0;
219 for (;!current.done();current.advance()) {
220 int bblocks = iceildiv(current.xmax() - current.x0(), strategy::out_width());
221
222 if (current.newkblock()) {
223 kern_k = iceildiv(current.kmax() - current.k0(), strategy::k_unroll());
224 kern_k *= strat.k_unroll();
225
226 unsigned b_thread_start_offset = iceildiv(current.x0(), strategy::out_width());
227
228 b_panel_start += b_page_size;
229 b_panel = b_panel_start + (b_thread_start_offset * strat.out_width() * kern_k);
230 b_page_size = _Nround * kern_k;
231
232 for (unsigned int batch = batch_0; batch <= batch_end; batch++) {
233 unsigned int first_m = (batch == batch_0) ? m_0 : 0;
234 unsigned int last_m = (batch == batch_end) ? m_max : _Msize;
235
236 if (first_m >= last_m)
237 continue;
238
239 auto a_thread_panel_in = this->_Aptr
240 + (batch * this->_A_batch_stride)
241 + (current.multi() * this->_A_multi_stride);
242
243 auto a_thread_panel_out = a_panel + ((batch * _Mround + first_m) * _k_block);
244
245 strat.transforms.PrepareA(
246 a_thread_panel_out,
247 a_thread_panel_in,
248 this->_lda,
249 first_m,
250 last_m,
251 current.k0(),
Georgios Pinitas0cc50ed2020-07-06 19:10:38 +0100252 current.kmax());
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000253 }
254 }
255
256 /* Do the actual work. */
257 for (unsigned int batch = batch_0; batch <= batch_end; batch++) {
258 unsigned int first_m = (batch == batch_0) ? m_0 : 0;
259 unsigned int last_m = (batch == batch_end) ? m_max : _Msize;
260
261 const Toi *a_ptr = a_panel + (batch * _Mround + first_m) * _k_block;
262
263 if (first_m >= last_m)
264 continue;
265
266 for (unsigned int y=first_m; y<last_m; y+=strategy::out_height()) {
267 unsigned int ymax = std::min(_Msize, y + strategy::out_height());
268
269 strat.kernel(a_ptr, b_panel, c_panel, 1, bblocks, kern_k);
270 a_ptr += (strategy::out_height() * kern_k);
271
272 /* Only activate on last pass, only add bias on first pass, ask for accumulation on any non-first pass */
273 const bool first_pass = current.k0()==0;
274 const bool last_pass = current.kmax()==_Ksize;
275
276 auto c_panel_out = this->_Cptr
277 + this->_C_batch_stride * batch
278 + this->_C_multi_stride * current.multi();
279
280 auto bias = (first_pass && this->_bias)
281 ? this->_bias + (current.multi() * this->_bias_multi_stride)
282 : nullptr;
283
284 auto act = last_pass ? _act : Activation();
285
286 strat.transforms.Merge(
287 c_panel_out,
288 c_panel,
289 this->_ldc,
290 y,
291 ymax,
292 current.x0(),
293 current.xmax(),
294 bias,
295 act,
296 !first_pass); //Append
297 }
298 }
299
300 b_panel += (bblocks * strat.out_width() * kern_k);
301 }
302 }
303
304public:
305 GemmInterleavedPretransposed2d(GemmInterleavedPretransposed2d &) = delete;
306 GemmInterleavedPretransposed2d & operator= (GemmInterleavedPretransposed2d &) = delete;
307
308 /* Constructor */
309 GemmInterleavedPretransposed2d(const GemmArgs &args)
310 : _ci(args._ci)
311 , _Msize(args._Msize)
312 , _Nsize(args._Nsize)
313 , _Ksize(args._Ksize)
314 , _nbatches(args._nbatches)
315 , _nmulti(args._nmulti)
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000316 , _act(args._act)
317 , _maxthreads(args._maxthreads)
318 , _nthreads(args._maxthreads)
319
320 // Work out the rounded size of M - needed for some buffers.
321 , _Mround_div ( iceildiv(_Msize, strategy::out_height()) )
322 , _Mround ( _Mround_div * strategy::out_height() )
323
324 , _Nround_div ( iceildiv(_Nsize, strategy::out_width()) )
325 , _Nround ( _Nround_div * strategy::out_width() )
326 {
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000327 assert(_maxthreads > 0);
328
329 const unsigned int L1_size = _ci->get_L1_cache_size();
330 const unsigned int L2_size = _ci->get_L2_cache_size();
331
332 // Work out blocking parameters, or override from provided GemmConfig
333 if (args._cfg && args._cfg->inner_block_size) {
334 _k_block = args._cfg->inner_block_size;
335 } else {
336 // k_block: Find out how much of the larger array can be loaded into half the cache.
337 // This should account for associative caches.
338 _k_block = (L1_size / 2) / (sizeof(Toi) * (std::max(strategy::out_width(), strategy::out_height())));
339
340 // Needs to be (at least a single) multiple of the K unroll level.
341 _k_block /= strategy::k_unroll();
342 _k_block = std::max(_k_block, 1U) * strategy::k_unroll();
343
344 // Now tune to presented problem size; this is how many blocks we need.
345 unsigned int num_k_blocks = iceildiv(_Ksize, _k_block);
346
347 // So divide the space equally into that many blocks.
348 _k_block = iceildiv(_Ksize, num_k_blocks);
349
350 // And round UP to the K unroll level required.
351 _k_block = iceildiv(_k_block, strategy::k_unroll());
352 _k_block *= strategy::k_unroll();
353 }
354
355 if (args._cfg && args._cfg->outer_block_size) {
356 _x_block = args._cfg->outer_block_size;
357 } else {
358 // x_block: Work out how many rows (of length k_block) will fit in the L2
359 // Don't allocate more than 90% of the L2 to allow for overheads, and subtract off the L1 contents.
360 _x_block = (((L2_size * 9) / 10) - (_k_block * sizeof(Toi) * (strategy::out_width() + strategy::out_height()))) /
361 (sizeof(Toi) * _k_block);
362
363 // Needs to be (at least a single) multiple of the kernel output width.
364 _x_block /= strategy::out_width();
365 _x_block = std::max(_x_block, 1U) * strategy::out_width();
366
367 // And tune to the presented problem size.
368 unsigned int num_x_blocks = iceildiv(_Nsize, _x_block);
369 _x_block = iceildiv(_Nsize, num_x_blocks);
370
371 _x_block = iceildiv(_x_block, strategy::out_width());
372 _x_block *= strategy::out_width();
373 }
374 }
375
376 // Interface implementation - Compulsory functions
377 ndrange_t get_window_size() const override {
378 unsigned m = (_Mround / strategy::out_height()) * _nbatches;
379 unsigned n = _Nround_div;
380
Georgios Pinitas5aa1a0b2020-07-02 20:02:20 +0100381 return { m, n };
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000382 }
383
384 // set_nthreads: pass on to buffer manager to avoid it waiting for non-existant threads.
385 void set_nthreads(int nthreads) override {
386 _nthreads = std::min(nthreads, _maxthreads);
387 }
388
389 void execute(const ndcoord_t& work_range, const ndcoord_t& thread_locator, int threadid) override {
390 /* This particular GEMM implementation can only be broken up over the M & N
391 * dimensions, we inform the frame work of this limitation via the get_window_size function
392 */
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000393 const auto m_start = work_range.get_position(0);
394 const auto n_start = work_range.get_position(1);
395 const auto m_size = work_range.get_size(0);
396 const auto n_size = work_range.get_size(1);
397 const auto m_end = m_start + m_size;
398 const auto n_end = n_start + n_size;
399
400 const auto m_threadid = thread_locator.get_position(0);
401 const auto n_threadid = thread_locator.get_position(1);
402
403 execute_pretranspose(m_start, m_end, n_start, n_end, threadid, m_threadid, n_threadid);
404 }
405
Georgios Pinitas0cc50ed2020-07-06 19:10:38 +0100406 std::size_t get_working_size() const override {
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000407 /* Because we do not know how schedular will break up
408 * the task, we need to ensure that alloc enough
409 * space to be able to handle the case where every thread
410 * is parallelised across B AND also every thrread is parallelised across A
411 *
412 * If we parallelise across A, then we only need one buffer of A and 64 buffers of B
413 * If we parallelise across B, then we only need 64 buffer of B and
414 */
415 return get_c_working_size() * _maxthreads
416 + get_a_working_size() * _maxthreads
417 + 64; //to account for cacheline alignment
418 }
419
420
421 void set_working_space(void *working_space) override {
422 // Make sure everything ends up cache line aligned
423 int8_t *working_space_bytes = reinterpret_cast<int8_t *>(working_space);
424 intptr_t working_space_int = reinterpret_cast<intptr_t>(working_space);
425
426 size_t diff=0;
427
428 if (working_space_int & 0x3F) {
429 diff = 0x40 - (working_space_int & 0x3F);
430 }
431
432 working_space_bytes += diff;
433
434 _working_space = reinterpret_cast<void *>(working_space_bytes);
435 }
436
437 // Interface implementation - pretransposed
438 bool B_is_pretransposed() const override {
439 return true;
440 }
441
442 bool B_pretranspose_required() const override {
443 return _B_transposed==nullptr;
444 }
445
446 // TODO: this could almost certainly be considerably simpler.
447 size_t get_B_pretransposed_array_size() const override {
448 size_t total=0;
449 blockwalker current(*this);
450
451 do {
452 /* Figure out the size of each block. */
453 unsigned int x_size = (current.xmax() - current.x0());
454 unsigned int k_size = (current.kmax() - current.k0());
455
456 /* Round sizes up as needed. */
457 x_size = iceildiv(x_size, strategy::out_width());
458 x_size *= strategy::out_width();
459
460 k_size = iceildiv(k_size, strategy::k_unroll());
461 k_size *= strategy::k_unroll();
462
463 total += x_size * k_size * sizeof(Toi);
464 } while (current.advance());
465
466 return total;
467 }
468
469 void pretranspose_B_array(void *in_buffer, const To *B, const int ldb, const int B_multi_stride) override {
470 blockwalker current(*this);
471 Toi *buffer = reinterpret_cast<Toi *>(in_buffer);
472 _B_transposed = buffer;
473 strategy strat(_ci);
474
475 do {
476 /* Figure out the size of each block. */
477 unsigned int x_size = (current.xmax() - current.x0());
478 unsigned int k_size = (current.kmax() - current.k0());
479
480 /* Round sizes up as needed. */
481 x_size = iceildiv(x_size, strategy::out_width());
482 x_size *= strategy::out_width();
483
484 k_size = iceildiv(k_size, strategy::k_unroll());
485 k_size *= strategy::k_unroll();
486
487 strat.transforms.PrepareB(buffer, B + (current.multi() * B_multi_stride), ldb,
Georgios Pinitas0cc50ed2020-07-06 19:10:38 +0100488 current.x0(), current.xmax(), current.k0(), current.kmax());
Joseph Dobson6f8b17d2020-02-11 19:32:11 +0000489
490 buffer += (x_size * k_size);
491 } while (current.advance());
492 }
493
494 void set_pretransposed_B_data(void *in_buffer) override {
495 _B_transposed = reinterpret_cast<Toi *>(in_buffer);
496 }
497
498 ~GemmInterleavedPretransposed2d() override { }
499};
500
501} // namespace arm_gemm