core_allocator.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/env python3
  2. import os
  3. import re
  4. import subprocess
  5. import sys
  6. # Note on hyperthreading: on Xeon CPUs that support SGX2, each physical
  7. # core supports two hyperthreads (if enabled in the BIOS), and appears
  8. # as two virtual cores to the system (in /proc/cpuinfo, for example).
  9. # The two virtual cores that map to the same physical core are called
  10. # the "A side" and the "B side". Each physical core only has one AES
  11. # circuit, so if both hyperthreads on the same core are trying to do AES
  12. # at the same time, performance crashes. So by default, we only use one
  13. # hyperthread (the "A side") on a given physical core.
  14. # Get the list of virtual cores currently available to us
  15. def virtual_cores_available():
  16. ret = subprocess.run(["numactl", "-s"], capture_output=True)
  17. if ret.returncode != 0:
  18. print("Unable to run numactl", file=sys.stderr)
  19. sys.exit(1)
  20. match = re.search(r'physcpubind: ((\d+ )*\d+)', str(ret.stdout))
  21. return list(map(int,match.group(1).split(' ')))
  22. # Read /proc/cpuinfo to get a map from virtual core number to CPU number
  23. # ("physical id") and physical core ("core id"). Only track the cores
  24. # that are currently available
  25. def get_core_map():
  26. cores_available = virtual_cores_available()
  27. coremap = {}
  28. with open("/proc/cpuinfo") as p:
  29. virtcore = None
  30. cpuid = None
  31. coreid = None
  32. while True:
  33. l = p.readline()
  34. if l == "":
  35. break
  36. elif l == "\n":
  37. if virtcore is None or cpuid is None or coreid is None:
  38. print(virtcore, cpuid, coreid)
  39. print("Could not parse /proc/cpuinfo", file=sys.stderr)
  40. sys.exit(1)
  41. if virtcore in cores_available:
  42. coremap[virtcore] = (cpuid, coreid)
  43. virtcore = None
  44. cpuid = None
  45. coreid = None
  46. elif match := re.match(r'processor\s*: (\d+)', l):
  47. virtcore = int(match.group(1))
  48. elif match := re.match(r'physical id\s*: (\d+)', l):
  49. cpuid = int(match.group(1))
  50. elif match := re.match(r'core id\s*: (\d+)', l):
  51. coreid = int(match.group(1))
  52. return coremap
  53. # Return an array. The first element of the array will represent the "A
  54. # sides", the second will represent the "B sides" (and if you're on some
  55. # weird CPU with more than 2 hyperthreads per core, the next will be the
  56. # "C sides", etc.). Each element will be a map from cpuid to a list of
  57. # the available virtual cores on that CPU, at most one per physical
  58. # core.
  59. def get_core_layout():
  60. core_map = get_core_map()
  61. retarray = []
  62. while core_map:
  63. # Extract the first virtual core for each (cpuid, physical core)
  64. # from core_map
  65. current_side_map = {}
  66. virtual_cores_remaining = list(core_map.keys())
  67. for vcore in virtual_cores_remaining:
  68. (cpuid, coreid) = core_map[vcore]
  69. if cpuid not in current_side_map:
  70. current_side_map[cpuid] = {}
  71. if coreid not in current_side_map[cpuid]:
  72. current_side_map[cpuid][coreid] = vcore
  73. del core_map[vcore]
  74. current_side = {}
  75. for cpuid in current_side_map:
  76. current_side[cpuid] = list(current_side_map[cpuid].values())
  77. retarray.append(current_side)
  78. return retarray
  79. core_layout = get_core_layout()
  80. # Maximum number of cores to use for clients (but we'll prefer to use
  81. # fewer cores instead of overloading cores)
  82. CLIENT_MAX_CORES = 8
  83. # Return a core allocation for an experiment. Pass in the number of
  84. # servers, and the number of cores per server. The return value is a
  85. # pair. The first element is a list of length num_servers, each
  86. # element of which is the core allocation for one server (which will be
  87. # a list of length cores_per_server). The second element of the return
  88. # value is the core allocation for the clients (which will be a list of
  89. # length between 1 and CLIENT_MAX_CORES). If the environment variable
  90. # OVERLOAD_CORES is unset or set to 0, each available physical core will
  91. # be used at most once. If that is not possible, (None, None) will be
  92. # returned. If OVERLOAD_CORES is set to 1, then physical cores will be
  93. # reused when necessary in order to run the requested experiment, albeit
  94. # at a significant performance penalty. It must be the case in any
  95. # event that you have at least one CPU with at least cores_per_server
  96. # physical cores.
  97. def core_allocation(num_servers, cores_per_server):
  98. overload_cores = \
  99. os.getenv("OVERLOAD_CORES", '0').lower() in ('true', '1', 't')
  100. servers_allocation = []
  101. client_allocation = []
  102. # Which index into core_layout we are currently working with
  103. hyperthread_side = 0
  104. # Copy that entry of the core_layout
  105. current_cores = dict(core_layout[hyperthread_side])
  106. while len(servers_allocation) < num_servers or \
  107. len(client_allocation) < CLIENT_MAX_CORES:
  108. # Find the cpu with the most cores available
  109. cpu_most_cores = None
  110. num_most_cores = None
  111. for cpuid in current_cores:
  112. num_cores = len(current_cores[cpuid])
  113. if num_cores > 0 and \
  114. (num_most_cores is None or num_cores > num_most_cores):
  115. cpu_most_cores = cpuid
  116. num_most_cores = num_cores
  117. if num_most_cores is not None and \
  118. num_most_cores >= cores_per_server and \
  119. len(servers_allocation) < num_servers:
  120. servers_allocation.append(
  121. current_cores[cpu_most_cores][0:cores_per_server])
  122. current_cores[cpu_most_cores] = \
  123. current_cores[cpu_most_cores][cores_per_server:]
  124. continue
  125. # We could not find a suitable allocation for the next server.
  126. # Try allocating a core for clients, if we still could use some.
  127. if num_most_cores is not None and \
  128. num_most_cores >= 1 and \
  129. len(client_allocation) < CLIENT_MAX_CORES:
  130. client_allocation.append(current_cores[cpu_most_cores][0])
  131. current_cores[cpu_most_cores] = \
  132. current_cores[cpu_most_cores][1:]
  133. continue
  134. # We can't do an allocation. If we have all the server
  135. # allocations, and at least one client core allocated, that'll
  136. # be good enough.
  137. if len(servers_allocation) == num_servers and \
  138. len(client_allocation) >= 1:
  139. break
  140. # We're going to have to overload cores, if allowed
  141. if not overload_cores:
  142. return (None, None)
  143. hyperthread_side = (hyperthread_side + 1) % len(core_layout)
  144. # Copy that entry of the core_layout
  145. current_cores = dict(core_layout[hyperthread_side])
  146. return (servers_allocation, client_allocation)
  147. if __name__ == "__main__":
  148. if len(sys.argv) > 1:
  149. num_servers = int(sys.argv[1])
  150. else:
  151. num_servers = 4
  152. if len(sys.argv) > 2:
  153. cores_per_server = int(sys.argv[2])
  154. else:
  155. cores_per_server = 1
  156. print(core_allocation(num_servers,cores_per_server))