diff --git a/max_domain_size_info.py b/max_domain_size_info.py new file mode 100644 index 0000000000000000000000000000000000000000..31d73a3b0ae8cc12ce9c0106c274519c0093c8a5 --- /dev/null +++ b/max_domain_size_info.py @@ -0,0 +1,172 @@ +"""Information table what the maximum domain size that fits in caches, main memory and optionally GPU memory + + +Examples: + + Pass different memory sizes and get information about the maximum domain sizes that fit into this memory + >>> import numpy as np + >>> memory_sizes = {'L1': '32 KB', 'L3': '1MB'} + >>> MaxDomainSizeInfo(memory_sizes, array_number=2, data_type=np.float64) + Mem| Size| D2Q9| D3Q19| D3Q27 + ------------------------------------------------------ + L1| 32 KB| 15.1| 4.8| 4.2 + L3| 1MB| 85.3| 15.1| 13.4 + + This means that a 2D domain of size 15^2 will fit in L1 cache, similarly a 3D domain of size 4^3. + + Instead of passing the memory sizes explicitly, Python can automatically query for the sizes of caches, main memory, + and GPU memory. To enable these automatic query, omit the memory_size dict. + + >>> info = MaxDomainSizeInfo(array_number=2, data_type=np.float64) + + (The output is not given here, since it depends on your machine.) + + +""" +import numpy as np +import warnings + +# Optional packages cpuinfo, pycuda and psutil for hardware queries +try: + from cpuinfo import get_cpu_info +except ImportError: + get_cpu_info = None + +try: + from pycuda.autoinit import device +except ImportError: + device = None + +try: + from psutil import virtual_memory +except ImportError: + virtual_memory = None + + +def square_size(memory_size, pdfs=9, array_number=2, data_type=np.float64): + """Maximum 2D domain size fitting in memory of given size. + + Args: + memory_size: memory size in bytes + pdfs: how many pdfs are stored per cell + array_number: how many pdf arrays, (2 for source-destination swap, 1 for AA pattern) + data_type: pdf type + + Returns: + square side length of maximum domain size + """ + num_doubles = memory_size / data_type().itemsize + cells = num_doubles / (pdfs * array_number) + return cells ** (1 / 2) + + +def cube_size(memory_size, pdfs=19, array_number=2, data_type=np.float64): + """Similar to square_size, but returns edge length of cube""" + num_doubles = memory_size / data_type().itemsize + cells = num_doubles / (pdfs * array_number) + return cells ** (1 / 3) + + +def convert_memory_size(m) -> int: + """Converts strings like '5 MB' or '1GB' into bytes.""" + if isinstance(m, str): + m = m.lower().strip() + if m.endswith('kb'): + factor = 1024 + elif m.endswith('mb'): + factor = 1024 ** 2 + elif m.endswith('gb'): + factor = 1024 ** 3 + else: + raise ValueError("Unknown unit") + quantity = float(m[:-2]) + return quantity * factor + else: + return m + + +def cache_domain_size_overview(memory_name_to_size, array_number=2, data_type=np.float64): + result = [] + + for mem_name, size in memory_name_to_size.items(): + d = {'Mem': mem_name, + 'Size': size, } + for dim, pdfs in [(2, 9), (3, 19), (3, 27)]: + func = square_size if dim == 2 else cube_size + stencil_name = 'D{}Q{}'.format(dim, pdfs) + d[stencil_name] = func(convert_memory_size(size), pdfs=pdfs, + array_number=array_number, data_type=data_type) + + result.append(d) + return result + + +def memory_sizes_of_current_machine(): + result = {} + + if get_cpu_info: + cpu_info = get_cpu_info() + result.update({'L1': cpu_info['l1_data_cache_size'], + 'L2': cpu_info['l2_cache_size'], + 'L3': cpu_info['l3_cache_size']}) + + if device: + size = device.total_memory() / (1024 * 1024) + result['GPU'] = "{0:.0f} MB".format(size) + + if virtual_memory: + mem = virtual_memory() + result['Free RAM'] = "{0:.0f} MB".format((mem.total - mem.used) / (1024 * 1024)) + result['Total RAM'] = "{0:.0f} MB".format(mem.total / (1024 * 1024)) + + if not result: + warnings.warn("Couldn't query for any local memory size." + "Install py-cpuinfo to get cache sizes, psutil for RAM size and pycuda for GPU memory size.") + + return result + + +class MaxDomainSizeInfo: + def __init__(self, memory_sizes=None, array_number=2, data_type=np.float64): + if memory_sizes is None: + memory_sizes = memory_sizes_of_current_machine() + self.cache_sizes = memory_sizes + self.data_type = data_type + self.array_number = array_number + + def _build_table(self): + header = ['Mem', 'Size', 'D2Q9', 'D3Q19', 'D3Q27'] + + def to_str(e): + if isinstance(e, float): + return format(e, ">10.1f") + elif isinstance(e, list): + return [to_str(a) for a in e] + else: + return format(e, ">10") + + rows = [to_str(header)] + + data = cache_domain_size_overview(self.cache_sizes, array_number=self.array_number, data_type=self.data_type) + for info in data: + rows.append([to_str(info[e]) for e in header]) + return rows + + def _repr_html_(self): + import ipy_table + # noinspection PyProtectedMember + return ipy_table.make_table(self._build_table())._repr_html_() + + def __str__(self): + lines = [] + header, *content = self._build_table() + + lines.append('|'.join(header)) + lines.append('-' * len(lines[0])) + + for row in content: + lines.append('|'.join(row)) + return "\n".join(lines) + + def __repr__(self): + return self.__str__()