link_exe.py 13 KB

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