blob: 8c3217cbc7ccd858e9a88e99d926792562959b89 [file] [log] [blame]
Jakub Sujak9b72a6c2023-11-28 14:40:22 +00001#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2024 Arm Limited.
5#
6# SPDX-License-Identifier: MIT
7#
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files (the "Software"), to
10# deal in the Software without restriction, including without limitation the
11# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12# sell copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in all
16# copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24# SOFTWARE.
25
26"""
27Updates the Doxygen documentation pages with a table of operators supported by Compute Library.
28
29The script builds up a table in XML format internally containing the different operators and their respective supported
30compute backends, data types and layouts, and the equivalent operator in the Android Neural Networks API. The list of
31operators is pulled from the OperatorList.h header file and further implementation details are provided in the function
32headers for the backend-specific operator e.g., NEStridedSlice.h.
33
34Usage:
35 python update_supported_ops.py
36"""
37
38import argparse
39import logging
40import re
41from enum import Enum
42from pathlib import Path
43
44
45class States(Enum):
46 INIT = 0
47 DESCRIPTION = 1
48 DESCRIPTION_END = 2
49 IN_CLASS = 3
50 DATA_TYPE_START = 4
51 DATA_TYPE_END = 5
52 NN_OPERATOR = 6
53 NN_OPERATOR_END = 7
54 SKIP_OPERATOR = 8
55 DATA_LAYOUT_START = 9
56 DATA_LAYOUT_END = 10
57
58
59class OperatorsTable:
60 def __init__(self):
61 self.project_dir = Path(__file__).parents[1] # ComputeLibrary directory
62 self.xml = ""
63
64 def generate_operator_list(self):
65 operator_list_head_file = self.project_dir / "arm_compute" / "runtime" / "OperatorList.h"
66 neon_file_name_prefix = str(self.project_dir / "arm_compute" / "runtime" / "NEON" / "functions" / "NE")
67 cl_file_name_prefix = str(self.project_dir / "arm_compute" / "runtime" / "CL" / "functions" / "CL")
68
69 logging.debug(operator_list_head_file)
70
71 f = open(operator_list_head_file, 'r')
72 # Iterates over the lines of the file
73 state = States.INIT
74 operator_desc = ""
75 nn_op_list = []
76 for line in f:
77 # /** ActivationLayer
78 # *
79 # * Description:
80 # * Function to simulate an activation layer with the specified activation function.
81 # *
82 # * Equivalent Android NNAPI Op:
83 # * ANEURALNETWORKS_ELU
84 # * ANEURALNETWORKS_HARD_SWISH
85 # * ANEURALNETWORKS_LOGISTIC
86 # * ANEURALNETWORKS_RELU
87 # * ANEURALNETWORKS_RELU1
88 # * ANEURALNETWORKS_RELU6
89 # * ANEURALNETWORKS_TANH
90 # *
91 # */
92 # Check for "/**" of the start of the operator
93 r = re.search('^\s*/\*\*(.*)', line)
94 if r and state == States.INIT:
95 # Skip below ones
96 if re.search('.*\(not ported\)', line):
97 state = States.SKIP_OPERATOR
98 continue
99 if re.search('.*\(only CL\)', line):
100 state = States.SKIP_OPERATOR
101 continue
102 if re.search('.*\(no CL\)', line):
103 state = States.SKIP_OPERATOR
104 continue
105 if re.search('.*\(skip\)', line):
106 state = States.SKIP_OPERATOR
107 continue
108 # Check" */"
109 r = re.match('\s*\*/\s*$', line)
110 if r and state == States.SKIP_OPERATOR:
111 state = States.INIT
112 continue
113 # Check " *"
114 r = re.match('\s*\*\s*$', line)
115 if r and state == States.SKIP_OPERATOR:
116 continue
117 # Check non " *" lines
118 r = re.search('^\s*\*(.*)', line)
119 if r and state == States.SKIP_OPERATOR:
120 continue
121
122 # Check for "/**" of the start of the operator
123 r = re.search('^\s*/\*\*(.*)', line)
124 if r and state == States.INIT:
125 tmp = r.groups()[0]
126 class_name = tmp.strip()
127 logging.debug(class_name)
128 continue
129
130 # Check whether "Description: " exists
131 r = re.search('\s*\*\s*Description:\s*', line)
132 if r and state == States.INIT:
133 state = States.DESCRIPTION
134 continue
135 # Treat description ends with a blank line only with " *"
136 r = re.match('\s*\*\s*$', line)
137 if r and state == States.DESCRIPTION:
138 logging.debug(operator_desc)
139 state = States.DESCRIPTION_END
140 continue
141 # Find continuing class description in the following lines
142 r = re.search('^\s*\*(.*)', line)
143 if r and state == States.DESCRIPTION:
144 tmp = r.groups()[0]
145 operator_desc = operator_desc + ' ' + tmp.strip()
146 continue
147
148 # Check whether "Equivalent AndroidNN Op: " exists
149 r = re.search('\s*\*\s*Equivalent Android NNAPI Op:\s*', line)
150 if r and state == States.DESCRIPTION_END:
151 state = States.NN_OPERATOR
152 continue
153 # Treat AndroidNN Op ends with a blank line only with " *"
154 r = re.match('\s*\*\s*$', line)
155 if r and state == States.NN_OPERATOR:
156 logging.debug(nn_op_list)
157 state = States.NN_OPERATOR_END
158 # Check NE#class_name
159 neon_file_name = neon_file_name_prefix + class_name + ".h"
160 logging.debug(neon_file_name)
161 # Check CL#class_name
162 cl_file_name = cl_file_name_prefix + class_name + ".h"
163 logging.debug(cl_file_name)
164 # Check whether CL/Neon file exists
165 if Path(neon_file_name).is_file() and Path(cl_file_name).is_file():
166 if neon_file_name.find("NEElementwiseOperations.h") != -1:
167 logging.debug(neon_file_name)
168 self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "13")
169 elif neon_file_name.find("NEElementwiseUnaryLayer.h") != -1:
170 logging.debug(neon_file_name)
171 self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "8")
172 else:
173 self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "2")
174 self.generate_operator_info(neon_file_name)
175 self.generate_operator_cl_begin()
176 self.generate_operator_info(cl_file_name)
177 else:
178 if neon_file_name.find("NELogical.h") != -1:
179 logging.debug(neon_file_name)
180 self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "3")
181 else:
182 self.generate_operator_common_info(class_name, operator_desc, nn_op_list, "1")
183 if Path(neon_file_name).is_file():
184 self.generate_operator_info(neon_file_name)
185 if Path(cl_file_name).is_file():
186 self.generate_operator_info(cl_file_name)
187 continue
188
189 # Find continuing AndroidNN Op in the following lines
190 r = re.search('^\s*\*(.*)', line)
191 if r and state == States.NN_OPERATOR:
192 tmp = r.groups()[0]
193 nn_op = tmp.strip()
194 nn_op_list.append(nn_op)
195 continue
196
197 # Treat operator ends with a blank line only with " */"
198 r = re.match('\s*\*/\s*$', line)
199 if r and state == States.NN_OPERATOR_END:
200 operator_desc = ""
201 nn_op_list = []
202 state = States.INIT
203 continue
204 f.close()
205
206 def generate_operator_info(self, file_name):
207 logging.debug(file_name)
208 f = open(file_name, 'r')
209 # iterates over the lines of the file
210 state = States.INIT
211 data_type_list = []
212 data_layout_list = []
213 io_list = []
214 class_no = 0
215 for line in f:
216 # Locate class definition by "class...: public IFunction",
217 # There are also exceptions, which will need to support in later version
218 r = re.match("\s*class\s+(\S+)\s*:\s*(public)*", line)
219 if r and state == States.INIT:
220 class_name = r.groups()[0]
221 logging.debug("class name is %s" % (class_name))
222 state = States.IN_CLASS
223 continue
224
225 r = re.match("\s*\}\;", line)
226 if r and state == States.IN_CLASS:
227 state = States.INIT
228 continue
229
230 # * Valid data layouts:
231 # * - All
232 r = re.search('\s*\*\s*Valid data layouts:', line)
233 if r and state == States.IN_CLASS:
234 state = States.DATA_LAYOUT_START
235 continue
236 # Treat data configuration ends with a blank line only with " *"
237 r = re.match('\s*\*\s*$', line)
238 if r and state == States.DATA_LAYOUT_START:
239 state = States.DATA_LAYOUT_END
240 continue
241 # Data layout continues
242 r = re.search('\s*\*\s*\-\s*(.*)', line)
243 if r and state == States.DATA_LAYOUT_START:
244 tmp = r.groups()[0]
245 tmp = tmp.strip()
246 logging.debug(tmp)
247 data_layout_list.append(tmp)
248
249 # * Valid data type configurations:
250 # * |src0 |dst |
251 # * |:--------------|:--------------|
252 # * |QASYMM8 |QASYMM8 |
253 # * |QASYMM8_SIGNED |QASYMM8_SIGNED |
254 # * |QSYMM16 |QSYMM16 |
255 # * |F16 |F16 |
256 # * |F32 |F32 |
257 r = re.search('\s*\*\s*Valid data type configurations:\s*', line)
258 if r and state == States.DATA_LAYOUT_END:
259 state = States.DATA_TYPE_START
260 logging.debug(line)
261 continue
262 # Treat data configuration ends with a blank line only with " *"
263 r = re.match('\s*\*\s*$', line)
264 if r and state == States.DATA_TYPE_START:
265 logging.debug(class_name)
266 logging.debug(data_layout_list)
267 logging.debug(io_list)
268 logging.debug(data_type_list)
269 class_no = class_no + 1
270 if class_no > 1:
271 logging.debug(class_no)
272 self.generate_operator_cl_begin()
273 self.generate_operator_dl_dt_info(class_name, data_layout_list, io_list, data_type_list)
274 state = States.INIT
275 data_type_list = []
276 data_layout_list = []
277 continue
278 # Data type continues
279 r = re.search('\s*\*(.*)', line)
280 if r and state == States.DATA_TYPE_START:
281 tmp = r.groups()[0]
282 tmp = tmp.strip()
283 if re.search('\|\:\-\-\-', tmp):
284 # Skip the table split row "|:-----"
285 continue
286 else:
287 tmp = tmp.strip()
288 if re.search('.*(src|input|dst)', tmp):
289 io_list = tmp.split('|')
290 else:
291 data_type = tmp.split('|')
292 logging.debug(data_type)
293 data_type_list.append(data_type)
294 continue
295
296 f.close()
297
298 def generate_operator_cl_begin(self):
299 self.xml += "<tr>\n"
300
301 def generate_operator_common_info(self, class_name, operator_desc, nn_op_list, rowspan):
302 tmp = "<tr>\n"
303 # Store class name
304 tmp += " <td rowspan=\"" + rowspan + "\">" + class_name + "\n"
305 tmp += " <td rowspan=\"" + rowspan + "\" style=\"width:200px;\">" + operator_desc + "\n"
306 tmp += " <td rowspan=\"" + rowspan + "\">\n"
307 tmp += " <ul>\n"
308 for item in nn_op_list:
309 tmp += " <li>"
310 tmp += item.strip()
311 tmp += "\n"
312 tmp += " </ul>\n"
313 self.xml += tmp
314
315 def generate_operator_dl_dt_info(self, class_name, data_layout, io_list, data_type_list):
316 tmp = " <td>" + class_name + "\n"
317 # Store data layout info
318 tmp += " <td>\n"
319 tmp += " <ul>\n"
320 for item in data_layout:
321 tmp += " <li>"
322 tmp += item.strip()
323 tmp += "\n"
324 tmp += " </ul>\n"
325 tmp += " <td>\n"
326 # Store data type table
327 tmp += " <table>\n"
328 tmp += " <tr>"
329 for io in io_list:
330 # Make sure it's not empty string
331 if len(io) != 0:
332 tmp += "<th>"
333 tmp += io.strip()
334 tmp += "\n"
335 for item in data_type_list:
336 tmp += " <tr>"
337 for i in item:
338 # Make sure it's not empty string
339 if len(i) != 0:
340 tmp += "<td>"
341 tmp += i.strip()
342 tmp += "\n"
343 tmp += " </table>\n"
344 self.xml += tmp
345
346 def generate_table_prefix(self):
347 tmp = "<table>\n"
348 tmp += "<caption id=\"multi_row\"></caption>\n"
349 tmp += "<tr>\n"
350 tmp += " <th>Function\n"
351 tmp += " <th>Description\n"
352 tmp += " <th>Equivalent Android NNAPI Op\n"
353 tmp += " <th>Backends\n"
354 tmp += " <th>Data Layouts\n"
355 tmp += " <th>Data Types\n"
356 self.xml += tmp
357
358 def generate_table_ending(self):
359 self.xml += "</table>\n"
360
361 def dump_xml(self):
362 print(self.xml)
363
364 def update_dox_file(self):
365 operator_list_dox = self.project_dir / "docs" / "user_guide" / "operator_list.dox"
366
367 with open(operator_list_dox, "r") as f:
368 dox_content = f.read()
369
370 # Check that there is only one non-indented table (This table should be the operator list)
371 x = re.findall("\n<table>", dox_content)
372 y = re.findall("\n</table>", dox_content)
373 if len(x) != 1 or len(y) != 1:
374 raise RuntimeError("Invalid .dox file")
375
376 repl_str = "\n" + self.xml[:-1] # Extra / removed "\n" characters needed to make up for search regex
377 new_file = re.sub("\n<table>(.|\n)*\n<\/table>", repl_str, dox_content)
378
379 with open(operator_list_dox, "w") as f:
380 f.write(new_file)
381 print("Successfully updated operator_list.dox with the XML table of supported operators.")
382
383
384if __name__ == "__main__":
385 parser = argparse.ArgumentParser(
386 description="Updates the Compute Library documentation with a table of supported operators."
387 )
388 parser.add_argument(
389 "--dump_xml",
390 type=bool,
391 default=False,
392 required=False,
393 help="Dump the supported operators table XML to stdout",
394 )
395 parser.add_argument(
396 "--debug",
397 type=bool,
398 default=False,
399 required=False,
400 help="Enables logging, helpful for debugging. Default: False",
401 )
402 args = parser.parse_args()
403
404 if args.debug:
405 logging.basicConfig(format="%(message)s", level=logging.DEBUG)
406
407 table_xml = OperatorsTable()
408 table_xml.generate_table_prefix()
409 table_xml.generate_operator_list()
410 table_xml.generate_table_ending()
411 table_xml.update_dox_file()
412
413 if args.dump_xml:
414 table_xml.dump_xml()