link_exe.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import itertools
  2. import os
  3. import os.path
  4. import sys
  5. import subprocess
  6. import optparse
  7. import textwrap
  8. # Explicitly enable local imports
  9. # Don't forget to add imported scripts to inputs of the calling command!
  10. sys.path.append(os.path.dirname(os.path.abspath(__file__)))
  11. import process_command_files as pcf
  12. import thinlto_cache
  13. from process_whole_archive_option import ProcessWholeArchiveOption
  14. from fix_py2_protobuf import fix_py2
  15. def get_leaks_suppressions(cmd):
  16. supp, newcmd = [], []
  17. for arg in cmd:
  18. if arg.endswith(".supp"):
  19. supp.append(arg)
  20. else:
  21. newcmd.append(arg)
  22. return supp, newcmd
  23. MUSL_LIBS = '-lc', '-lcrypt', '-ldl', '-lm', '-lpthread', '-lrt', '-lutil'
  24. CUDA_LIBRARIES = {
  25. '-lcublas_static': '-lcublas',
  26. '-lcublasLt_static': '-lcublasLt',
  27. '-lcudart_static': '-lcudart',
  28. '-lcudnn_static': '-lcudnn',
  29. '-lcufft_static_nocallback': '-lcufft',
  30. '-lcupti_static': '-lcupti',
  31. '-lcurand_static': '-lcurand',
  32. '-lcusolver_static': '-lcusolver',
  33. '-lcusparse_static': '-lcusparse',
  34. '-lmyelin_compiler_static': '-lmyelin',
  35. '-lmyelin_executor_static': '-lnvcaffe_parser',
  36. '-lmyelin_pattern_library_static': '',
  37. '-lmyelin_pattern_runtime_static': '',
  38. '-lnvinfer_static': '-lnvinfer',
  39. '-lnvinfer_plugin_static': '-lnvinfer_plugin',
  40. '-lnvonnxparser_static': '-lnvonnxparser',
  41. '-lnvparsers_static': '-lnvparsers',
  42. '-lnvrtc_static': '-lnvrtc',
  43. '-lnvrtc-builtins_static': '-lnvrtc-builtins',
  44. '-lnvptxcompiler_static': '',
  45. '-lnppc_static': '-lnppc',
  46. '-lnppial_static': '-lnppial',
  47. '-lnppicc_static': '-lnppicc',
  48. '-lnppicom_static': '-lnppicom',
  49. '-lnppidei_static': '-lnppidei',
  50. '-lnppif_static': '-lnppif',
  51. '-lnppig_static': '-lnppig',
  52. '-lnppim_static': '-lnppim',
  53. '-lnppist_static': '-lnppist',
  54. '-lnppisu_static': '-lnppisu',
  55. '-lnppitc_static': '-lnppitc',
  56. '-lnpps_static': '-lnpps',
  57. }
  58. class CUDAManager:
  59. def __init__(self, known_arches, nvprune_exe):
  60. self.fatbin_libs = self._known_fatbin_libs(set(CUDA_LIBRARIES))
  61. self.prune_args = []
  62. if known_arches:
  63. for arch in known_arches.split(':'):
  64. self.prune_args.append('-gencode')
  65. self.prune_args.append(self._arch_flag(arch))
  66. self.nvprune_exe = nvprune_exe
  67. def has_cuda_fatbins(self, cmd):
  68. return bool(set(cmd) & self.fatbin_libs)
  69. @property
  70. def can_prune_libs(self):
  71. return self.prune_args and self.nvprune_exe
  72. def _known_fatbin_libs(self, libs):
  73. libs_wo_device_code = {
  74. '-lcudart_static',
  75. '-lcupti_static',
  76. '-lnppc_static',
  77. }
  78. return set(libs) - libs_wo_device_code
  79. def _arch_flag(self, arch):
  80. _, ver = arch.split('_', 1)
  81. return 'arch=compute_{},code={}'.format(ver, arch)
  82. def prune_lib(self, inp_fname, out_fname):
  83. if self.prune_args:
  84. prune_command = [self.nvprune_exe] + self.prune_args + ['--output-file', out_fname, inp_fname]
  85. subprocess.check_call(prune_command)
  86. def write_linker_script(self, f):
  87. # This script simply says:
  88. # * Place all `.nv_fatbin` input sections from all input files into one `.nv_fatbin` output section of output file
  89. # * Place it after `.bss` section
  90. #
  91. # Motivation can be found here: https://maskray.me/blog/2021-07-04-sections-and-overwrite-sections#insert-before-and-insert-after
  92. # TL;DR - we put section with a lot of GPU code directly after the last meaningful section in the binary
  93. # (which turns out to be .bss)
  94. # In that case, we decrease chances of relocation overflows from .text to .bss,
  95. # because now these sections are close to each other
  96. script = textwrap.dedent("""
  97. SECTIONS {
  98. .nv_fatbin : { *(.nv_fatbin) }
  99. } INSERT AFTER .bss
  100. """).strip()
  101. f.write(script)
  102. def tmpdir_generator(base_path, prefix):
  103. for idx in itertools.count():
  104. path = os.path.abspath(os.path.join(base_path, prefix + '_' + str(idx)))
  105. os.makedirs(path)
  106. yield path
  107. def process_cuda_library_by_external_tool(cmd, build_root, tool_name, callable_tool_executor, allowed_cuda_libs):
  108. tmpdir_gen = tmpdir_generator(build_root, 'cuda_' + tool_name + '_libs')
  109. new_flags = []
  110. cuda_deps = set()
  111. # Because each directory flag only affects flags that follow it,
  112. # for correct pruning we need to process that in reversed order
  113. for flag in reversed(cmd):
  114. if flag in allowed_cuda_libs:
  115. cuda_deps.add('lib' + flag[2:] + '.a')
  116. flag += '_' + tool_name
  117. elif flag.startswith('-L') and os.path.exists(flag[2:]) and os.path.isdir(flag[2:]) and any(f in cuda_deps for f in os.listdir(flag[2:])):
  118. from_dirpath = flag[2:]
  119. from_deps = list(cuda_deps & set(os.listdir(from_dirpath)))
  120. if from_deps:
  121. to_dirpath = next(tmpdir_gen)
  122. for f in from_deps:
  123. from_path = os.path.join(from_dirpath, f)
  124. to_path = os.path.join(to_dirpath, f[:-2] + '_' + tool_name +'.a')
  125. callable_tool_executor(from_path, to_path)
  126. cuda_deps.remove(f)
  127. # do not remove current directory
  128. # because it can contain other libraries we want link to
  129. # instead we just add new directory with processed by tool libs
  130. new_flags.append('-L' + to_dirpath)
  131. new_flags.append(flag)
  132. assert not cuda_deps, ('Unresolved CUDA deps: ' + ','.join(cuda_deps))
  133. return reversed(new_flags)
  134. def process_cuda_libraries_by_objcopy(cmd, build_root, objcopy_exe):
  135. if not objcopy_exe:
  136. return cmd
  137. def run_objcopy(from_path, to_path):
  138. rename_section_command = [objcopy_exe, "--rename-section", ".ctors=.init_array", from_path, to_path]
  139. subprocess.check_call(rename_section_command)
  140. possible_libraries = set(CUDA_LIBRARIES.keys())
  141. possible_libraries.update([
  142. '-lcudadevrt',
  143. '-lcufilt',
  144. '-lculibos',
  145. ])
  146. possible_libraries.update([
  147. lib_name + "_pruner" for lib_name in possible_libraries
  148. ])
  149. return process_cuda_library_by_external_tool(list(cmd), build_root, 'objcopy', run_objcopy, possible_libraries)
  150. def process_cuda_libraries_by_nvprune(cmd, cuda_manager, build_root):
  151. if not cuda_manager.has_cuda_fatbins(cmd):
  152. return cmd
  153. # add custom linker script
  154. to_dirpath = next(tmpdir_generator(build_root, 'cuda_linker_script'))
  155. script_path = os.path.join(to_dirpath, 'script')
  156. with open(script_path, 'w') as f:
  157. cuda_manager.write_linker_script(f)
  158. flags_with_linker = list(cmd) + ['-Wl,--script={}'.format(script_path)]
  159. if not cuda_manager.can_prune_libs:
  160. return flags_with_linker
  161. return process_cuda_library_by_external_tool(flags_with_linker, build_root, 'pruner', cuda_manager.prune_lib, cuda_manager.fatbin_libs)
  162. def remove_excessive_flags(cmd):
  163. flags = []
  164. for flag in cmd:
  165. if not flag.endswith('.ios.interface') and not flag.endswith('.pkg.fake'):
  166. flags.append(flag)
  167. return flags
  168. def fix_sanitize_flag(cmd, opts):
  169. """
  170. Remove -fsanitize=address flag if sanitazers are linked explicitly for linux target.
  171. """
  172. for flag in cmd:
  173. if flag.startswith('--target') and 'linux' not in flag.lower():
  174. # use toolchained sanitize libraries
  175. return cmd
  176. assert opts.clang_ver
  177. CLANG_RT = 'contrib/libs/clang' + opts.clang_ver + '-rt/lib/'
  178. sanitize_flags = {
  179. '-fsanitize=address': CLANG_RT + 'asan',
  180. '-fsanitize=memory': CLANG_RT + 'msan',
  181. '-fsanitize=leak': CLANG_RT + 'lsan',
  182. '-fsanitize=undefined': CLANG_RT + 'ubsan',
  183. '-fsanitize=thread': CLANG_RT + 'tsan',
  184. }
  185. used_sanitize_libs = []
  186. aux = []
  187. for flag in cmd:
  188. if flag.startswith('-fsanitize-coverage='):
  189. # do not link sanitizer libraries from clang
  190. aux.append('-fno-sanitize-link-runtime')
  191. if flag in sanitize_flags and any(s.startswith(sanitize_flags[flag]) for s in cmd):
  192. # exclude '-fsanitize=' if appropriate library is linked explicitly
  193. continue
  194. if any(flag.startswith(lib) for lib in sanitize_flags.values()):
  195. used_sanitize_libs.append(flag)
  196. continue
  197. aux.append(flag)
  198. # move sanitize libraries out of the repeatedly searched group of archives
  199. flags = []
  200. for flag in aux:
  201. if flag == '-Wl,--start-group':
  202. flags += ['-Wl,--whole-archive'] + used_sanitize_libs + ['-Wl,--no-whole-archive']
  203. flags.append(flag)
  204. return flags
  205. def fix_cmd_for_musl(cmd):
  206. flags = []
  207. for flag in cmd:
  208. if flag not in MUSL_LIBS:
  209. flags.append(flag)
  210. return flags
  211. def fix_cmd_for_dynamic_cuda(cmd):
  212. flags = []
  213. for flag in cmd:
  214. if flag in CUDA_LIBRARIES:
  215. flags.append(CUDA_LIBRARIES[flag])
  216. else:
  217. flags.append(flag)
  218. return flags
  219. def gen_default_suppressions(inputs, output, source_root):
  220. import collections
  221. import os
  222. supp_map = collections.defaultdict(set)
  223. for filename in inputs:
  224. sanitizer = os.path.basename(filename).split('.', 1)[0]
  225. with open(os.path.join(source_root, filename)) as src:
  226. for line in src:
  227. line = line.strip()
  228. if not line or line.startswith('#'):
  229. continue
  230. supp_map[sanitizer].add(line)
  231. with open(output, "wb") as dst:
  232. for supp_type, supps in supp_map.items():
  233. dst.write('extern "C" const char *__%s_default_suppressions() {\n' % supp_type)
  234. dst.write(' return "{}";\n'.format('\\n'.join(sorted(supps))))
  235. dst.write('}\n')
  236. def fix_blas_resolving(cmd):
  237. # Intel mkl comes as a precompiled static library and thus can not be recompiled with sanitizer runtime instrumentation.
  238. # That's why we prefer to use cblas instead of Intel mkl as a drop-in replacement under sanitizers.
  239. # But if the library has dependencies on mkl and cblas simultaneously, it will get a linking error.
  240. # Hence we assume that it's probably compiling without sanitizers and we can easily remove cblas to prevent multiple definitions of the same symbol at link time.
  241. for arg in cmd:
  242. if arg.startswith('contrib/libs') and arg.endswith('mkl-lp64.a'):
  243. return [arg for arg in cmd if not arg.endswith('libcontrib-libs-cblas.a')]
  244. return cmd
  245. def parse_args():
  246. parser = optparse.OptionParser()
  247. parser.disable_interspersed_args()
  248. parser.add_option('--musl', action='store_true')
  249. parser.add_option('--custom-step')
  250. parser.add_option('--python')
  251. parser.add_option('--source-root')
  252. parser.add_option('--build-root')
  253. parser.add_option('--clang-ver')
  254. parser.add_option('--dynamic-cuda', action='store_true')
  255. parser.add_option('--cuda-architectures',
  256. help='List of supported CUDA architectures, separated by ":" (e.g. "sm_52:compute_70:lto_90a"')
  257. parser.add_option('--nvprune-exe')
  258. parser.add_option('--objcopy-exe')
  259. parser.add_option('--arch')
  260. parser.add_option('--linker-output')
  261. parser.add_option('--whole-archive-peers', action='append')
  262. parser.add_option('--whole-archive-libs', action='append')
  263. thinlto_cache.add_options(parser)
  264. return parser.parse_args()
  265. if __name__ == '__main__':
  266. opts, args = parse_args()
  267. args = pcf.skip_markers(args)
  268. cmd = fix_blas_resolving(args)
  269. cmd = fix_py2(cmd)
  270. cmd = remove_excessive_flags(cmd)
  271. if opts.musl:
  272. cmd = fix_cmd_for_musl(cmd)
  273. cmd = fix_sanitize_flag(cmd, opts)
  274. if opts.dynamic_cuda:
  275. cmd = fix_cmd_for_dynamic_cuda(cmd)
  276. else:
  277. cuda_manager = CUDAManager(opts.cuda_architectures, opts.nvprune_exe)
  278. cmd = process_cuda_libraries_by_nvprune(cmd, cuda_manager, opts.build_root)
  279. cmd = process_cuda_libraries_by_objcopy(cmd, opts.build_root, opts.objcopy_exe)
  280. cmd = ProcessWholeArchiveOption(opts.arch, opts.whole_archive_peers, opts.whole_archive_libs).construct_cmd(cmd)
  281. if opts.custom_step:
  282. assert opts.python
  283. subprocess.check_call([opts.python] + [opts.custom_step] + args)
  284. supp, cmd = get_leaks_suppressions(cmd)
  285. if supp:
  286. src_file = "default_suppressions.cpp"
  287. gen_default_suppressions(supp, src_file, opts.source_root)
  288. cmd += [src_file]
  289. if opts.linker_output:
  290. stdout = open(opts.linker_output, 'w')
  291. else:
  292. stdout = sys.stdout
  293. thinlto_cache.preprocess(opts, cmd)
  294. rc = subprocess.call(cmd, shell=False, stderr=sys.stderr, stdout=stdout)
  295. thinlto_cache.postprocess(opts)
  296. sys.exit(rc)