Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# Contributing to Arm RAN Acceleration Library (Arm RAL)
Describes the requirements for contributing code to Arm RAN
Acceleration Library (Arm RAL):
- The license;
- How to write and submit patches;
- Naming of functions;
- Documentation style;
- Code style, and how to automatically check it;
- Structure of tests and benchmarks.
## Licensing information
Use of Arm RAN Acceleration Library is subject to a BSD-3-Clause
license, the text of which can be found in the `license_terms` folder
of your product installation. We will receive inbound contributions
under the same license.
## Writing and submitting patches
Contributions are managed via the RAL project
[https://gitlab.arm.com/networking/ral](https://gitlab.arm.com/networking/ral)
on [Arm's Gitlab](https://gitlab.arm.com). You will need to ask for
access to Arm's Gitlab in order to be able to fork RAL and raise merge
requests. Details on how to do this are given
[here](https://gitlab.arm.com/documentation/contributions). Once you
have access you can submit your patch for review as a [merge
request](https://gitlab.arm.com/networking/ral/-/merge_requests).
Patches must be based against the current head of `main` of RAL.
Every patch must compile successfully and pass all tests. It is good
practice to split the development of new functions into multiple
patches to aid reviewing: present the initial unoptimized
implementation and accompanying tests in one patch, and the optimized
implementation in a second patch.
Submit your patch for review as a [Merge
Request](https://gitlab.arm.com/networking/ral/-/merge_requests).
All patches must be accompanied by a commit message. An acceptable
commit message comprises a one line summary less than 50 characters in
length, followed by a single blank line, followed by the body of the
commit message. The body should detail the changes in the patch with
any relevant reasoning.
## Function naming
Arm RAL functions are named according to:
armral_<algorithm>_<precision>{_variant}
where:
- *algorithm* is a word or words that summarises the main purpose of
the function;
- *precision* indicates the working precision of the internals of the
implementation, which may not always be the same as the precision of
the input and output arguments.
For Fast Fourier Transform (FFT) functions use:
- `cf32`: complex 32-bit floating point;
- `cs16`: complex signed 16-bit integer.
For all other functions use:
- `f32`: 32-bit floating point;
- `i16`: signed 16-bit integer.
- *variant* is an optional suffix to distinguish different
implementations of the same *algorithm* at the same *precision*.
Examples from the library:
Function | Algorithm | Precision | Variant
-----------------------------------|------------------------------------------|-------------------------------|------------------------
`armral_fft_create_plan_cf32` | Create an FFT plan | complex 32-bit floating point | None
`armral_fft_execute_cs16` | Execute an FFT | complex signed 16-bit integer | None
`armral_cmplx_mat_mult_2x2_f32_iq` | Complex-valued 2x2 matrix multiplication | 32-bit floating point | Separate I and Q arrays
`armral_cmplx_vecdot_i16_32bit` | Complex-valued vector dot-product | signed 16-bit integer | 32-bit accumulator
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
## Directory structure
The directory structure of Arm RAL is:
```
+-- CMakeLists.txt
+-- README.md
+-- RELEASE_NOTES.md
+-- bench
| +-- CRC
| +-- bench.py
| +-- main.cpp
| +-- ...
+-- docs
| +-- ...
+-- examples
| +-- ...
+-- include
| +-- armral.h
+-- license_terms
| +-- BSD-3-Clause.txt
| +-- ...
+-- simulation
| +-- ...
+-- src
| +-- BasicMathFun
| +-- MatrixInv
| +-- arm_cmplx_hermitian_mat_inversion_f32.cpp
| +-- ...
| +-- ...
| +-- ...
+-- test
| +-- CRC
| +-- main.cpp
| +-- ...
+-- utils
| +-- ...
```
The `src` subdirectory contains the source files for user-facing
functions, grouped by functionality into separate subdirectories
(i.e. `src/BasicMathFun/MatrixInv` in the diagram above). If your
patch adds new source files, you must add the path to them to the
`ARMRAL_LIB_SOURCES` variable in the top-level `CMakeLists.txt`
file. `test` contains tests for user-facing functions and `bench`
contains benchmarks. There is specific information about writing tests
and benchmarks below.
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
## C vs C++
We write all new functionality in C++ in order to exploit a number of
language features.
### Custom allocators and non-allocating function variants
We only directly call `malloc` and `free` in the library's FFT
functions (`armral/src/LowerPHY/FFT`). All other functions must use
the custom allocators defined in `src/utils/allocators.hpp`. These
offer two advantages:
1. Developers do not need to ensure dynamically-allocated memory is
freed after use.
2. All user-facing functions (defined in `include/armral.h`) that need
to allocate memory internally must also provide a non-allocating
version that allows users to pass in a pre-allocated buffer. Using Arm
RAL's custom allocators simplifies writing these variants because they
offer a counting allocator in addition to one that uses `malloc`.
C-style variable length arrays (VLAs) can only be used in the FFT
functions (`armral/src/LowerPHY/FFT`).
### Namespaces
All symbols in the library must be clearly identified as coming from
Arm RAL. User-facing functions specified in `include/armral.h` are
identified by the prefix `armral_`. Using C++ enables us to enclose
other library functions in namespaces. These namespaces must begin
with `armral::` and can themselves contain further namespaces to
identify their role; for example, the functions that implement LDPC
encoding and decoding are all contained in the `armral::ldpc`
namespace. Anonymous namespaces can be used to restrict the scope of
functions to particular translation units and are preferable to using
the `static` keyword.
### No dependency on C++ standard library at runtime
We require that Arm RAL does not have a dependency on the C++ runtime
library as this enables `libarmral` to be linked against on systems
that do not have the C++ runtime library installed. This means that
constructs like `std::vector` must not be used by functions in the
`src` directory and only compile-time C++ constructs are
allowed. However, there is no restriction on the use of C++ runtime
constructs in testing and benchmarking code: for example,
`std::vector` can be used to hold input data for benchmarks.
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
## Documentation
Documentation for each user-facing function is written as a Doxygen
comment immediately preceding the function's prototype in
`include/armral.h`. Arm RAL uses the Javadoc style, which is a C-style
multi-line comment that starts with `/**`:
```c
/**
* This algorithm performs the multiplication `A x` for matrix `A` and vector
* `x`, and assumes that:
* + Matrix and vector elements are complex float values.
* + Matrices are stored in memory in row-major order.
*
* @param[in] m The number of rows in matrix `A` and the length of
* the output vector `y`.
* @param[in] n The number of columns in matrix `A` and the length
* of the input vector `x`.
* @param[in] p_src_a Points to the first input matrix.
* @param[in] p_src_x Points to the input vector.
* @param[out] p_dst Points to the output matrix.
* @return An `armral_status` value that indicates success or failure.
*/
armral_status armral_cmplx_mat_vec_mult_f32(uint16_t m, uint16_t n,
const armral_cmplx_f32_t *p_src_a,
const armral_cmplx_f32_t *p_src_x,
armral_cmplx_f32_t *p_dst);
```
The comment begins with a description of the purpose of the function
and implementation details are subsequently given in one or more
paragraphs. If the function implements an algorithm described in an
external publication, for example a technical standard, provide a
reference to that publication in the comment.
In-line mathematical quantities and parameter names must be enclosed
in backticks, for example \`A x\` in the preceding code
sample. Enclose larger equations on their own lines in `<pre> </pre>`
tags:
```
* Computes the regularized pseudo-inverse of a single matrix. The `N-by-M`
* regularized pseudo-inverse `C` of an `M-by-N` matrix `A` with `M <= N` is
* defined as:
*
* <pre>
* C = A^H * (A * A^H + λ * I)^-1
* </pre>
```
The documentation must describe any restrictions on the inputs, for
example when the length of an array needs to be a multiple of a
certain number. Consider marking particularly important restrictions
as *warnings*. For example, the documentation for
`armral_cmplx_pseudo_inverse_direct_f32` says:
```
* \warning This method is numerically unstable for matrices that are not very
* well conditioned.
```
Warnings are made prominent in the HTML version of the documentation
that is generated from the Doxygen.
The comment must finish with the ordered list of the function's
parameters followed by the return value. Indicate which parameters are
inputs and which are outputs.
## C/C++ code style
C/C++ code style is maintained through the use of `clang-format` and
`clang-tidy`. You must run these tools on the code before submitting a
patch; instructions on how to run these tools are given below.
`clang-format` and `clang-tidy` are part of the [LLVM
Project](https://llvm.org/). Arm RAL is tested with version 17.0.0 of
the tools.
Matching your coding style as close as possible to the `clang-tidy`
style will minimize the number of changes that `clang-tidy` will
- Use snake case for names of variables and functions,
i.e. `this_is_a_variable` instead of `thisIsAVariable`.
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
- Symbol names start with a lower case letter. This means that `_m`
for a member variable, for example, will not be accepted.
- Always use curly braces for single line `if` statements, `for` loops
and `while` loops.
- Opening curly braces for `if` statements, `for` loops and `while`
loops are on the same line as the `if`, `for` or `while`.
- Closing curly braces are the first non-white-space character on a
new line. Their alignment must match the first character of the
matching `if`/`for`/`while` statement. `else` statements are on the
same line as a closing curly brace for the corresponding `if` or `else
if` statement.
### Running clang-format
Run `clang-format` on the current commit with:
git clang-format HEAD~
This will correctly format any files modified in the current
commit. You must then update your commit with the reformatted files.
### Running clang-tidy
Before running `clang-tidy` you must compile the library with an LLVM
compiler, i.e. `clang` and `clang++`, and tell CMake to write out the
compilation commands by setting `-DCMAKE_EXPORT_COMPILE_COMMANDS=On`:
mkdir <build>
cd <build>
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DBUILD_TESTING=On <path>
make
Substituting:
- `<build>` with a build directory name. The library builds in the
specified directory.
- `<path>` with the path to the root directory of the library source.
Then run `clang-tidy` with a list of files to check:
cd <build>
clang-tidy -p <build> <file_1> <file_2> ... <file_N> -header-filter=.*
where `<file_X>` is the path to a modified file in the library
source. Fix any errors and update your commit with the modified files.
## Python code style
Python code style is maintained through the use of the `flake8`
linter. Install `flake8` using `pip`:
pip install flake8
and run it on an individual Python file:
python -m flake8 --config=<path>/flake8.txt <filename>
Where:
- `<path>` is the path to the root directory of the library source.
- `<filename>` is the name of the Python file to format.
This will produce a list of errors, which you must fix manually. Once
you have rerun `flake8` and it does not report any errors, add your
updated Python file to the current commit.
## Writing tests
Each function with a prototype in `armral.h` must be accompanied by a
set of tests that run the function on a set of representative inputs
and compare the result to known-good output. This reference output is
preferably a separate reimplementation of the function. In some
situations it may be necessary to compare against arrays of constant
values instead but this should be avoided wherever possible.
Arm RAL tests must exercise every path through the function that leads
to a successful exit. Setting the CMake variable
`ARMRAL_ENABLE_COVERAGE=On` enables the compiler flags needed to
visualize code coverage with [gcovr](https://gcovr.com/en/stable/).
The test inputs should cover the full range of values that a user can
provide; if it is feasible to run a test using the largest value that
can be stored in a variable, for example, then such a test should be
included.
In the top-level `CMakeLists.txt` add an `add_armral_test()` entry
pointing to the source file for the tests. The source-code for the
test must be placed in a subdirectory of `<path>/test`, where `<path>`
is the root directory of the library source. Usually the source for
all the tests of a single Arm RAL function is contained in a single
`main.cpp` file.
Successful tests must return `EXIT_SUCCESS` from the `main()`
function; failing tests must return `EXIT_FAILURE`.
### Testing with AddressSanitizer
It is recommended to use
[AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html)
to test your patches for memory errors as patches will not be accepted
unless this passes. Setting the CMake variable `ARMRAL_ENABLE_ASAN=On`
enables the flags needed to compile and link Arm RAL and its tests
with AddressSanitizer. The `make check` target will then run the tests
using AddressSanitizer and will fail if an error is detected.
## Writing benchmarks
Each function with a prototype in `armral.h` must be
accompanied by a set of benchmarks that run the function on a set of
representative inputs.
### Files to add
To create a set of benchmark cases for a newly added function, a
directory should be added to `<path>/bench`, where `<path>` is the
root directory of the library source. This directory may contain
layers of subdirectories if necessary. The lowest directories in the
tree must contain a `bench.py` file that specifies the cases to run,
and a `main.cpp` file that executes the function being
benchmarked. See [Directory structure](#directory-structure) for an
example.
#### bench.py
The `bench.py` files write parsable JSON to standard output with
fields for the case name (`"name"`), the arguments that will need to
be used to run the function (`"args"`), and the number of repetitions
that should be done (`"reps"`).
```json
{
"exe_name": "benchmark/name",
"cases": [
{
"name": "benchmark_name",
"args": "1 2 3",
"reps": 1000
},
{... }
]
}
```
The following code block provides a template for the `bench.py` script.
```py
#!/usr/bin/env python3
# Arm RAN Acceleration Library
# Copyright 2020-2023 Arm Limited and/or its affiliates <open-source-office@arm.com>
import json
from pathlib import Path
import os
def get_path(x): return x if Path(x).is_file() else os.path.join("armral", x)
exe_name = get_path(<bench_exe_name>)
j = {
"exe_name": exe_name,
"cases": []
}
reps = <number of reps>
argArr = <list of argument values>
for <arg0, arg1, ...> in argArr:
case = {
"name": "<function>_{}_{}...".format(<arg0>,<arg1>, ...),
"args": "{} {} ...".format(<arg0>, <arg1>, ...),
"reps": reps
}
j["cases"].append(case)
print(json.dumps(j))
```
Items in angle brackets `< >` are changed as appropriate according to the
following descriptions.
- `<bench_exe_name>`: The name of the executable, e.g.
`bench_mu_law_compression_8bit` (see [Naming scheme](#naming-scheme)).
- `<number of reps>`: The number of times the case should be run for
profiling (see [Number of repetitions](#number-of-repetitions)).
- `<list of argument values>`: The arguments that will be required in order
to run the function that is to be benchmarked. This can be a list of individual
elements, or can, for example, be a list of tuples if multiple arguments are
required for each case. The length of the list determines how many cases are
generated. See [Number of cases](#number-of-cases) for guidance on how many
cases there should be.
- `<function>`: A snake case string to identify the function being
benchmarked for a particular case, e.g. `mu_law_compression_8bit`.
- `<arg0, arg1, ...>`: The arguments in the argument list.
#### main.cpp
The `main.cpp` is the file that gets compiled into the benchmark
executable. The `main.cpp` file must:
- contain a `main` function which handles a list of command line arguments.
- contain a separate function that calls the function being benchmarked.
- return `EXIT_SUCCESS` on completion.
The following code block provides a basic template.
```cpp
/*
Arm RAN Acceleration Library
Copyright 2020-2023 Arm Limited and/or its affiliates <open-source-office@arm.com>
*/
#include "armral.h"
#include <cstdio>
#include <cstdlib>
namespace {
void <run_function_perf>(<type_arg0> <arg0>,
<type_arg1> <arg1>,
...,
uint32_t num_reps) {
printf("[<FUNC DESCRIPTOR>] <arg0_description> = %<arg0_specifier>,
<arg1_description> = %<arg1_specifier>, ...,
number of repetitions = %u\n", <arg0>, <arg1>, ..., num_reps);
// Define necessary variables here
<type_var0> <var0> = ...
<type_var1> <var1> = ...
...
for (uint32_t i = 0; i < num_reps; ++i) {
<armral_func>(<arg0>, <arg1>, ..., <var0>, <var1>, ...);
}
}
} // anonymous namespace
int main(int argc, char **argv) {
if (argc != <num_args>) {
// <arg0> - <description of arg0>
// <arg1> - <description of arg1>
// ...
// num_reps - The number of times to repeat the function
fprintf(stderr, "Usage: %s <arg0> <arg1> ... num_reps\n", argv[0]);
exit(EXIT_FAILURE);
}
auto <arg0> = (<type_arg0>)atoi(argv[1]);
auto <arg1> = (<type_arg1>)atoi(argv[2]);
...
auto num_reps = (uint32_t)atoi(argv[<num_args> - 1]);
<run_function_perf>(<arg0>, <arg1>, ..., num_reps);
return EXIT_SUCCESS;
}
```
The items in angle brackets `< >` are changed as appropriate according to the
following descriptions.
- `<run_function_perf>`: The name of the function that repeatedly calls the
function being benchmarked, e.g. `run_mu_law_compression_8bit_perf` (see
[Naming scheme](#naming-scheme)).
- `<type_arg0>`, `<type_arg1>`: The types of the arguments which are passed
in on the command line.
- `<FUNC DESCRIPTOR>`: An uppercase string to identify the function, e.g.
`"MU LAW COMPRESSION 8BIT"`.
- `<arg0_description>`, `<arg1_description>`: Descriptions to identify the
arguments when printing.
- `<arg0_specifier>`, `<arg1_specifier>`: The format specifiers for printing
the arguments.
- `<type_var0>`, `<type_var1>`: The types of the variables defined locally
in `<run_function_perf>`.
- `<var0>`, `<var1>`: The names of variables defined locally in
`<run_function_perf>`.
- `<armral_func>`: The name of the library function being benchmarked (e.g.
`armral_mu_law_compr_8bit`).
- `<num_args>`: The number of arguments which are passed to the executable
on the command line. This is equal to the number of arguments in the `args`
field of the JSON object + 1 (since the filename is the first argument).
- `<arg0>`, `<arg1>`: The names of the arguments which are passed to the
executable on the command line. These are the names of the arguments provided
in the `args` field of the JSON object generated by `bench.py`.
- `<description of arg0>`, `<description of arg1>`: A description of each
command line argument.
##### Outputs
Print statements may be added to the `main.cpp` file to describe the
benchmark being run, an example of which is given in the `main.cpp`
template above. It is useful to include a description of the function being
benchmarked, as well as the argument values and the number of
repetitions.
##### CMakeLists.txt entry
Once the new `main.cpp` file has been created, an entry must be added to
`CMakeLists.txt` with the form:
`add_armral_bench(<name> <path to main.cpp>)`
where `<name>` is the `exe_name` without `bench_` at the front (e.g.
`mu_law_compression_8bit`). The entry goes with the other benchmark
entries as part of the `if(BUILD_TESTING)` logic.
#### Directory structure
Benchmarks for different functions should be separated into different
files. For example, for Mu Law compression and decompression there are
different functions for 8-bit, 9-bit and 14-bit (de)compression. These
should be in separate benchmarking executables. The Mu Law directory
structure in `bench` therefore looks like:
```
+-- MuLaw
| +-- Compression
| +-- 8bit
| +-- bench.py
| +-- main.cpp
| +-- 9bit
| +-- bench.py
| +-- main.cpp
| +-- 14bit
| +-- bench.py
| +-- main.cpp
| +-- Decompression
| +-- 8bit
| +-- bench.py
| +-- main.cpp
| +-- 9bit
| +-- bench.py
| +-- main.cpp
| +-- 14bit
| +-- bench.py
| +-- main.cpp
```
### Naming scheme
This section describes how the added directories, executables and
functions should be named.
#### Directory name
The name of the directory (and any subdirectories) added to to
`<path>/bench` should be descriptive of the function being benchmarked,
and should be in upper camel case, e.g. `MuLaw`.
#### Executable name
The name of the executable should be `bench_` followed by a
descriptive name of the benchmark in snake case e.g.
`bench_mu_law_compression_8bit`.
#### Function name
The name of the function used in the `main.cpp` file should start with
`run_`, be followed by a descriptive name of the function being
benchmarked in snake case, and end with `_perf`, e.g.
`run_mu_law_compression_8bit_perf`.
### Number of cases
The number of cases which is output by the `bench.py` file for a
particular function is determined by how many different sets of
parameters are used as inputs to the function. To keep benchmarking
runtimes manageable, the number of cases for a function should be kept to
between 4 and 8. The cases should try to cover a broad set of allowed
parameters.
As an example, for 8-bit Mu Law compression the different cases
correspond to different numbers of physical resource blocks. There are 4
different values for the resource block chosen (1, 2, 10 and 100), hence
there are 4 benchmarking cases for this function.
### Number of repetitions
In order to obtain stable performance results, the function being
benchmarked is run a number of times, and the average taken. The number
of repetitions is given in the `reps` field of the JSON object for a
particular case. The number of repetitions should be sufficient to
provide reproducible performance numbers. However, keep in mind how long the
benchmark will take to run. In general, shorter benchmarks should have a higher
number of repetitions, and longer benchmarks should have fewer repetitions.