{ "cells": [ { "cell_type": "markdown", "id": "c38240c3-7646-497b-a3ac-d9cfa6c974f2", "metadata": {}, "source": [ "# Parallel processing of a stack of data stored in HDF5 with multi-threading\n", "\n", "This tutorial explains how it is possible to treat in parallel a large HDF5 dataset which does not fit into the computer memory.\n", "\n", "![Typical workflow](workflow.png)\n", "\n", "For this tutorial, a recent version of pyFAI is needed (>=0.22, summer 2022).\n", "\n", "This tutorial expains how to take benefit from multi-threading. This framework is not very popular in the Python world due to the [Global Interpreter Lock (GIL)](https://wiki.python.org/moin/GlobalInterpreterLock), but properly written C-code which does release the GIL can be very fast, sometimes as fast as GPU code (on large computers).\n", "\n", "**Credits:**\n", "\n", "* Thomas Vincent (ESRF) for the parallel decompression of HDF5 chunks and the Jupyter-slurm\n", "* Pierre Paleo (ESRF) for struggling with this kind of stuff with GPUs\n", "* Jon Wright (ESRF) for the CSC integrator, while implemented in serial is multithreading friendly + HDF5 investigation\n", "* The French-CRG for providing a manycore computer (2 x 32-core AMD EPYC 75F3)\n", "\n", "**Nota:** No GPU is needed for this tutorial!\n", "\n", "**Important:** the `bitshuffle` module needs to be compiled without OpenMP, since the tutorial aims at demonstrating that Python threads can be almost as efficient as OpenMP. If you have a doubt about OpenMP, please uncomment the environment variable OMP_NUM_THREADS reset in the second cell. This will unfortunately bias the performance measurement of pyFAI with the CSR sparse-matrix multiplication.\n", "\n", "## 1. Description of the computer.\n", "\n", "The results obtained vary a lot as function of the computer and its topology. This section details some internal details about the computer." ] }, { "cell_type": "code", "execution_count": 1, "id": "5eb5f112-bf32-4413-85ea-67eb88bf7ee9", "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "# use `widget` for better user experience; `inline` is for documentation generation" ] }, { "cell_type": "code", "execution_count": 2, "id": "638e4966-b05e-47e2-a4b0-5843b1b5ff93", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Working on computer hpc6-04.\n" ] } ], "source": [ "import sys, os, collections, struct, time, socket\n", "# Ensure OpenMP is disabled\n", "os.environ[\"OMP_NUM_THREADS\"] = \"1\"\n", "import numpy, pyFAI\n", "import h5py, hdf5plugin\n", "from queue import Queue\n", "import threading\n", "import bitshuffle\n", "from matplotlib.pyplot import subplots\n", "start_time = time.time()\n", "Item = collections.namedtuple(\"Item\", \"index data\")\n", "print(f\"Working on computer {socket.gethostname()}.\")" ] }, { "cell_type": "code", "execution_count": 3, "id": "210e4de4-184b-4c64-872e-5c1aaf080501", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Working with 64 threads. Mind OpenMP needs to be disabled in the bitshuffle code !\n" ] } ], "source": [ "nbthreads = len(os.sched_getaffinity(0))\n", "print(f\"Working with {nbthreads} threads. Mind OpenMP needs to be disabled in the bitshuffle code !\")" ] }, { "cell_type": "code", "execution_count": 4, "id": "8e649b79-f024-4635-ac8f-fc68827c60aa", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Architecture: x86_64\n", "CPU op-mode(s): 32-bit, 64-bit\n", "Byte Order: Little Endian\n", "Address sizes: 48 bits physical, 48 bits virtual\n", "CPU(s): 64\n", "On-line CPU(s) list: 0-63\n", "Thread(s) per core: 1\n", "Core(s) per socket: 32\n", "Socket(s): 2\n", "NUMA node(s): 2\n", "Vendor ID: AuthenticAMD\n", "CPU family: 25\n", "Model: 1\n", "Model name: AMD EPYC 75F3 32-Core Processor\n", "Stepping: 1\n", "Frequency boost: enabled\n", "CPU MHz: 1493.870\n", "CPU max MHz: 2950.0000\n", "CPU min MHz: 1500.0000\n", "BogoMIPS: 5888.66\n", "Virtualization: AMD-V\n", "L1d cache: 2 MiB\n", "L1i cache: 2 MiB\n", "L2 cache: 32 MiB\n", "L3 cache: 512 MiB\n", "NUMA node0 CPU(s): 0-31\n", "NUMA node1 CPU(s): 32-63\n", "Vulnerability Itlb multihit: Not affected\n", "Vulnerability L1tf: Not affected\n", "Vulnerability Mds: Not affected\n", "Vulnerability Meltdown: Not affected\n", "Vulnerability Mmio stale data: Not affected\n", "Vulnerability Retbleed: Not affected\n", "Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled v\n", " ia prctl and seccomp\n", "Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user\n", " pointer sanitization\n", "Vulnerability Spectre v2: Mitigation; Retpolines, IBPB conditional, IBRS_\n", " FW, STIBP disabled, RSB filling, PBRSB-eIBRS No\n", " t affected\n", "Vulnerability Srbds: Not affected\n", "Vulnerability Tsx async abort: Not affected\n", "Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtr\n", " r pge mca cmov pat pse36 clflush mmx fxsr sse s\n", " se2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtsc\n", " p lm constant_tsc rep_good nopl nonstop_tsc cpu\n", " id extd_apicid aperfmperf pni pclmulqdq monitor\n", " ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe\n", " popcnt aes xsave avx f16c rdrand lahf_lm cmp_l\n", " egacy svm extapic cr8_legacy abm sse4a misalign\n", " sse 3dnowprefetch osvw ibs skinit wdt tce topoe\n", " xt perfctr_core perfctr_nb bpext perfctr_llc mw\n", " aitx cpb cat_l3 cdp_l3 invpcid_single hw_pstate\n", " ssbd mba ibrs ibpb stibp vmmcall fsgsbase bmi1\n", " avx2 smep bmi2 invpcid cqm rdt_a rdseed adx sm\n", " ap clflushopt clwb sha_ni xsaveopt xsavec xgetb\n", " v1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total c\n", " qm_mbm_local clzero irperf xsaveerptr wbnoinvd \n", " arat npt lbrv svm_lock nrip_save tsc_scale vmcb\n", " _clean flushbyasid decodeassists pausefilter pf\n", " threshold v_vmsave_vmload vgif umip pku ospke v\n", " aes vpclmulqdq rdpid overflow_recov succor smca\n" ] } ], "source": [ "!lscpu" ] }, { "cell_type": "code", "execution_count": 5, "id": "97da69b1-f59d-4bf7-99ff-19b426cdccce", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "available: 2 nodes (0-1)\n", "node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31\n", "node 0 size: 257524 MB\n", "node 0 free: 244301 MB\n", "node 1 cpus: 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63\n", "node 1 size: 257995 MB\n", "node 1 free: 211831 MB\n", "node distances:\n", "node 0 1 \n", " 0: 10 32 \n", " 1: 32 10 \n" ] } ], "source": [ "!numactl --hardware" ] }, { "cell_type": "code", "execution_count": 6, "id": "345903ed-e336-4b37-b520-34790b95252d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Machine (503GB total)\n", " Package L#0\n", " NUMANode L#0 (P#0 251GB)\n", " L3 L#0 (32MB)\n", " L2 L#0 (512KB) + L1d L#0 (32KB) + L1i L#0 (32KB) + Core L#0 + PU L#0 (P#0)\n", " L2 L#1 (512KB) + L1d L#1 (32KB) + L1i L#1 (32KB) + Core L#1 + PU L#1 (P#1)\n", " L2 L#2 (512KB) + L1d L#2 (32KB) + L1i L#2 (32KB) + Core L#2 + PU L#2 (P#2)\n", " L2 L#3 (512KB) + L1d L#3 (32KB) + L1i L#3 (32KB) + Core L#3 + PU L#3 (P#3)\n", " L3 L#1 (32MB)\n", " L2 L#4 (512KB) + L1d L#4 (32KB) + L1i L#4 (32KB) + Core L#4 + PU L#4 (P#4)\n", " L2 L#5 (512KB) + L1d L#5 (32KB) + L1i L#5 (32KB) + Core L#5 + PU L#5 (P#5)\n", " L2 L#6 (512KB) + L1d L#6 (32KB) + L1i L#6 (32KB) + Core L#6 + PU L#6 (P#6)\n", " L2 L#7 (512KB) + L1d L#7 (32KB) + L1i L#7 (32KB) + Core L#7 + PU L#7 (P#7)\n", " L3 L#2 (32MB)\n", " L2 L#8 (512KB) + L1d L#8 (32KB) + L1i L#8 (32KB) + Core L#8 + PU L#8 (P#8)\n", " L2 L#9 (512KB) + L1d L#9 (32KB) + L1i L#9 (32KB) + Core L#9 + PU L#9 (P#9)\n", " L2 L#10 (512KB) + L1d L#10 (32KB) + L1i L#10 (32KB) + Core L#10 + PU L#10 (P#10)\n", " L2 L#11 (512KB) + L1d L#11 (32KB) + L1i L#11 (32KB) + Core L#11 + PU L#11 (P#11)\n", " L3 L#3 (32MB)\n", " L2 L#12 (512KB) + L1d L#12 (32KB) + L1i L#12 (32KB) + Core L#12 + PU L#12 (P#12)\n", " L2 L#13 (512KB) + L1d L#13 (32KB) + L1i L#13 (32KB) + Core L#13 + PU L#13 (P#13)\n", " L2 L#14 (512KB) + L1d L#14 (32KB) + L1i L#14 (32KB) + Core L#14 + PU L#14 (P#14)\n", " L2 L#15 (512KB) + L1d L#15 (32KB) + L1i L#15 (32KB) + Core L#15 + PU L#15 (P#15)\n", " L3 L#4 (32MB)\n", " L2 L#16 (512KB) + L1d L#16 (32KB) + L1i L#16 (32KB) + Core L#16 + PU L#16 (P#16)\n", " L2 L#17 (512KB) + L1d L#17 (32KB) + L1i L#17 (32KB) + Core L#17 + PU L#17 (P#17)\n", " L2 L#18 (512KB) + L1d L#18 (32KB) + L1i L#18 (32KB) + Core L#18 + PU L#18 (P#18)\n", " L2 L#19 (512KB) + L1d L#19 (32KB) + L1i L#19 (32KB) + Core L#19 + PU L#19 (P#19)\n", " L3 L#5 (32MB)\n", " L2 L#20 (512KB) + L1d L#20 (32KB) + L1i L#20 (32KB) + Core L#20 + PU L#20 (P#20)\n", " L2 L#21 (512KB) + L1d L#21 (32KB) + L1i L#21 (32KB) + Core L#21 + PU L#21 (P#21)\n", " L2 L#22 (512KB) + L1d L#22 (32KB) + L1i L#22 (32KB) + Core L#22 + PU L#22 (P#22)\n", " L2 L#23 (512KB) + L1d L#23 (32KB) + L1i L#23 (32KB) + Core L#23 + PU L#23 (P#23)\n", " L3 L#6 (32MB)\n", " L2 L#24 (512KB) + L1d L#24 (32KB) + L1i L#24 (32KB) + Core L#24 + PU L#24 (P#24)\n", " L2 L#25 (512KB) + L1d L#25 (32KB) + L1i L#25 (32KB) + Core L#25 + PU L#25 (P#25)\n", " L2 L#26 (512KB) + L1d L#26 (32KB) + L1i L#26 (32KB) + Core L#26 + PU L#26 (P#26)\n", " L2 L#27 (512KB) + L1d L#27 (32KB) + L1i L#27 (32KB) + Core L#27 + PU L#27 (P#27)\n", " L3 L#7 (32MB)\n", " L2 L#28 (512KB) + L1d L#28 (32KB) + L1i L#28 (32KB) + Core L#28 + PU L#28 (P#28)\n", " L2 L#29 (512KB) + L1d L#29 (32KB) + L1i L#29 (32KB) + Core L#29 + PU L#29 (P#29)\n", " L2 L#30 (512KB) + L1d L#30 (32KB) + L1i L#30 (32KB) + Core L#30 + PU L#30 (P#30)\n", " L2 L#31 (512KB) + L1d L#31 (32KB) + L1i L#31 (32KB) + Core L#31 + PU L#31 (P#31)\n", " HostBridge\n", " PCIBridge\n", " PCI 01:00.0 (RAID)\n", " Block(Disk) \"sdb\"\n", " Block(Disk) \"sda\"\n", " HostBridge\n", " PCIBridge\n", " PCI 63:00.0 (Ethernet)\n", " Net \"eno12399np0\"\n", " PCI 63:00.1 (Ethernet)\n", " Net \"eno12409np1\"\n", " PCIBridge\n", " PCIBridge\n", " PCI 62:00.0 (VGA)\n", " Package L#1\n", " NUMANode L#1 (P#1 252GB)\n", " L3 L#8 (32MB)\n", " L2 L#32 (512KB) + L1d L#32 (32KB) + L1i L#32 (32KB) + Core L#32 + PU L#32 (P#32)\n", " L2 L#33 (512KB) + L1d L#33 (32KB) + L1i L#33 (32KB) + Core L#33 + PU L#33 (P#33)\n", " L2 L#34 (512KB) + L1d L#34 (32KB) + L1i L#34 (32KB) + Core L#34 + PU L#34 (P#34)\n", " L2 L#35 (512KB) + L1d L#35 (32KB) + L1i L#35 (32KB) + Core L#35 + PU L#35 (P#35)\n", " L3 L#9 (32MB)\n", " L2 L#36 (512KB) + L1d L#36 (32KB) + L1i L#36 (32KB) + Core L#36 + PU L#36 (P#36)\n", " L2 L#37 (512KB) + L1d L#37 (32KB) + L1i L#37 (32KB) + Core L#37 + PU L#37 (P#37)\n", " L2 L#38 (512KB) + L1d L#38 (32KB) + L1i L#38 (32KB) + Core L#38 + PU L#38 (P#38)\n", " L2 L#39 (512KB) + L1d L#39 (32KB) + L1i L#39 (32KB) + Core L#39 + PU L#39 (P#39)\n", " L3 L#10 (32MB)\n", " L2 L#40 (512KB) + L1d L#40 (32KB) + L1i L#40 (32KB) + Core L#40 + PU L#40 (P#40)\n", " L2 L#41 (512KB) + L1d L#41 (32KB) + L1i L#41 (32KB) + Core L#41 + PU L#41 (P#41)\n", " L2 L#42 (512KB) + L1d L#42 (32KB) + L1i L#42 (32KB) + Core L#42 + PU L#42 (P#42)\n", " L2 L#43 (512KB) + L1d L#43 (32KB) + L1i L#43 (32KB) + Core L#43 + PU L#43 (P#43)\n", " L3 L#11 (32MB)\n", " L2 L#44 (512KB) + L1d L#44 (32KB) + L1i L#44 (32KB) + Core L#44 + PU L#44 (P#44)\n", " L2 L#45 (512KB) + L1d L#45 (32KB) + L1i L#45 (32KB) + Core L#45 + PU L#45 (P#45)\n", " L2 L#46 (512KB) + L1d L#46 (32KB) + L1i L#46 (32KB) + Core L#46 + PU L#46 (P#46)\n", " L2 L#47 (512KB) + L1d L#47 (32KB) + L1i L#47 (32KB) + Core L#47 + PU L#47 (P#47)\n", " L3 L#12 (32MB)\n", " L2 L#48 (512KB) + L1d L#48 (32KB) + L1i L#48 (32KB) + Core L#48 + PU L#48 (P#48)\n", " L2 L#49 (512KB) + L1d L#49 (32KB) + L1i L#49 (32KB) + Core L#49 + PU L#49 (P#49)\n", " L2 L#50 (512KB) + L1d L#50 (32KB) + L1i L#50 (32KB) + Core L#50 + PU L#50 (P#50)\n", " L2 L#51 (512KB) + L1d L#51 (32KB) + L1i L#51 (32KB) + Core L#51 + PU L#51 (P#51)\n", " L3 L#13 (32MB)\n", " L2 L#52 (512KB) + L1d L#52 (32KB) + L1i L#52 (32KB) + Core L#52 + PU L#52 (P#52)\n", " L2 L#53 (512KB) + L1d L#53 (32KB) + L1i L#53 (32KB) + Core L#53 + PU L#53 (P#53)\n", " L2 L#54 (512KB) + L1d L#54 (32KB) + L1i L#54 (32KB) + Core L#54 + PU L#54 (P#54)\n", " L2 L#55 (512KB) + L1d L#55 (32KB) + L1i L#55 (32KB) + Core L#55 + PU L#55 (P#55)\n", " L3 L#14 (32MB)\n", " L2 L#56 (512KB) + L1d L#56 (32KB) + L1i L#56 (32KB) + Core L#56 + PU L#56 (P#56)\n", " L2 L#57 (512KB) + L1d L#57 (32KB) + L1i L#57 (32KB) + Core L#57 + PU L#57 (P#57)\n", " L2 L#58 (512KB) + L1d L#58 (32KB) + L1i L#58 (32KB) + Core L#58 + PU L#58 (P#58)\n", " L2 L#59 (512KB) + L1d L#59 (32KB) + L1i L#59 (32KB) + Core L#59 + PU L#59 (P#59)\n", " L3 L#15 (32MB)\n", " L2 L#60 (512KB) + L1d L#60 (32KB) + L1i L#60 (32KB) + Core L#60 + PU L#60 (P#60)\n", " L2 L#61 (512KB) + L1d L#61 (32KB) + L1i L#61 (32KB) + Core L#61 + PU L#61 (P#61)\n", " L2 L#62 (512KB) + L1d L#62 (32KB) + L1i L#62 (32KB) + Core L#62 + PU L#62 (P#62)\n", " L2 L#63 (512KB) + L1d L#63 (32KB) + L1i L#63 (32KB) + Core L#63 + PU L#63 (P#63)\n", " HostBridge\n", " PCIBridge\n", " PCI c3:00.0 (SATA)\n", " HostBridge\n", " PCIBridge\n", " PCI e1:00.0 (Ethernet)\n", " Net \"eno8303\"\n", " PCI e1:00.1 (Ethernet)\n", " Net \"eno8403\"\n" ] } ], "source": [ "!lstopo --of console" ] }, { "cell_type": "markdown", "id": "d2c54705-4b6c-444b-b9f1-b763f6a8d915", "metadata": {}, "source": [ "## 2. Setup the enviroment:\n", "\n", "This is a purely virtual experiment, the tutorial tries to be representative of the processing for the beamline of Jon Wright: ESRF-ID11 this is why we will use an Eiger 4M detector with data integrated over 1000 bins. Those parameters can be tuned.\n", "\n", "Random data are generated to mimic the scattering of a liquid with Poisson noise. The input file is fairly small, since those data compress nicely. The speed of the drive used for temporary storage is likely to have a huge impact, especially if all data do not hold in memory !" ] }, { "cell_type": "code", "execution_count": 7, "id": "1cd22d82-d4fb-4d28-9960-0442989ca18c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "HDF5PluginBuildConfig(openmp=True, native=True, bmi2=True, sse2=True, avx2=True, avx512=False, cpp11=True, cpp14=True, ipp=False, filter_file_extension='.so', embedded_filters=('blosc', 'blosc2', 'bshuf', 'bzip2', 'fcidecomp', 'lz4', 'sz', 'sz3', 'zfp', 'zstd'))" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "det = pyFAI.detector_factory(\"eiger_4M\")\n", "shape = det.shape\n", "dtype = numpy.dtype(\"uint32\")\n", "filename = \"/tmp/big.h5\"\n", "h5path = \"data\"\n", "nbins = 1000\n", "cmp = hdf5plugin.Bitshuffle()\n", "hdf5plugin.get_config().build_config" ] }, { "cell_type": "code", "execution_count": 8, "id": "daf4eec8-e364-43b6-b5af-8f279c6aed38", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of frames the computer can host in memory: 30127.005\n", "Number of frames the computer can host in memory with SLURM restrictions: 3829.928\n" ] } ], "source": [ "mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')\n", "print(f\"Number of frames the computer can host in memory: {mem_bytes/(numpy.prod(shape)*dtype.itemsize):.3f}\")\n", "if os.environ.get('SLURM_MEM_PER_NODE'):\n", " print(f\"Number of frames the computer can host in memory with SLURM restrictions: {int(os.environ['SLURM_MEM_PER_NODE'])*(1<<20)/(numpy.prod(shape)*dtype.itemsize):.3f}\")" ] }, { "cell_type": "code", "execution_count": 9, "id": "bd038dc8-1e6d-4bd6-95b3-303f2d0f250b", "metadata": {}, "outputs": [], "source": [ "#The computer being limited to 64G of RAM, the number of frames actually possible is 3800.\n", "nbframes = 4096 # slightly larger than the maximum achievable ! Such a dataset should not host in memory." ] }, { "cell_type": "code", "execution_count": 10, "id": "a32bd8fb-9b64-4564-9a69-67a759bf3427", "metadata": {}, "outputs": [], "source": [ "#Prepare a frame with little count so that it compresses well\n", "geo = {\"detector\": det, \n", " \"wavelength\": 1e-10}\n", "ai = pyFAI.load(geo)\n", "q = numpy.arange(15)\n", "img = ai.calcfrom1d(q, 100/(1+q*q))\n", "frame = numpy.random.poisson(img).astype(dtype)" ] }, { "cell_type": "code", "execution_count": 11, "id": "53b49350-6b0c-4c3b-ba5b-ece50cd6fe98", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# display the image\n", "fig,ax = subplots()\n", "ax.imshow(frame)" ] }, { "cell_type": "code", "execution_count": 12, "id": "17e3d7e2-de22-4d40-8230-39295f04e239", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Performances of the different algorithms for azimuthal integration of Eiger 4M image\n", "Using algorithm histogram : 420 ms ± 6.95 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", "Using algorithm csc : 38.1 ms ± 1.21 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", "Using algorithm csr : 46.8 ms ± 676 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "print(\"Performances of the different algorithms for azimuthal integration of Eiger 4M image\")\n", "for algo in (\"histogram\", \"csc\", \"csr\"):\n", " print(f\"Using algorithm {algo:10s}:\", end=\" \")\n", " %timeit ai.integrate1d(img, nbins, method=(\"full\", algo, \"cython\"))" ] }, { "cell_type": "markdown", "id": "22969c56-1d10-4950-aebe-699da628858e", "metadata": {}, "source": [ "**Note:** The full pixel splitting is time consuming and handicaps the histogram algorithm while both sparse-matrix methods are much faster since they cache this calculation in the sparse matrix.\n", "\n", "The compared performances of sparse-matrix methods is rather surprizing since the CSC algorithm, single threaded, is faster than the CSR which runs in parallel over 2x32 cores.\n", "This result is the combination of two facotors:\n", "\n", "1. The computer is built with two processors/sockets controling each its own memory. We call this a **Non Uniform Memory Access** computer and can be checked with `numactrl --hardware`. The CSR matrix multiplication will dispatch work on both processors and thus, needs to transfer part of the image from one NUMA subsystem (socket) to the other, which is slow (3.2x slower compared to a single-socket access, according to the output of numactl). \n", "\n", "2. The very large cache of this processor: 512MB are reported by `lscpu`, but a more precise tool, `lstopo` describes them as 32MB of L3 cache shared between 4 cores. This very large cache allows the complete frame and the sparse matrix to be pre-fetched which is a great advantage for the CSC algorithm.\n", "\n", "Running the very same benchmark on an Intel 2-socket server would remove the point 2, while running on a singe socket intel workstation would remove both points and the normal results would be that CSR should be faster than CSC. The best performances on can get with the CSR algorithm should be obtained when using 4 cores (sharing the same cache L3) out of 64 on this computer. This can be done by setting the environment variable **OMP_NUM_THREADS**. Unfortunately, it also requires to restart the process, thus cannot be demonstrated easily in the notebook (without restarting). \n", "\n", "**The first message to take home is that without the knownledge of the actual computer, no high-performace computing is possible**" ] }, { "cell_type": "code", "execution_count": 13, "id": "a98654ed-372a-4886-9292-32b31ae4ec2a", "metadata": {}, "outputs": [], "source": [ "#Does not work unless one restarts the process\n", "\n", "# print(\"Performances of the different algorithms for azimuthal integration of Eiger 4M image when using only 4 cores\")\n", "# mask = os.sched_getaffinity(0)\n", "# os.sched_setaffinity(0, [0,1,2,3])\n", "# for algo in (\"histogram\", \"csc\", \"csr\"):\n", "# print(f\"Using algorithm {algo}:\", end=\" \")\n", "# %timeit ai.integrate1d(img, nbins, method=(\"full\", algo, \"cython\"))\n", "# os.sched_setaffinity(0, mask)" ] }, { "cell_type": "markdown", "id": "63768a00-0b9e-4c55-a55e-efe6a8a17358", "metadata": {}, "source": [ "## 3. Writing the test dataset on disk." ] }, { "cell_type": "code", "execution_count": 14, "id": "32d5ebc6-3473-49a2-93e7-76e7bb8f17f1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Saving of a HDF5 file with many frames ...\n", "if not os.path.exists(filename):\n", " with h5py.File(filename, \"w\") as h:\n", " ds = h.create_dataset(h5path, shape=(nbframes,)+shape, chunks=(1,)+shape, dtype=dtype, **cmp) \n", " for i in range(nbframes):\n", " ds[i] = frame + i%500 #Each frame has a different value to prevent caching effects" ] }, { "cell_type": "code", "execution_count": 15, "id": "f82b2daf-77c2-4720-9848-6ca9c273707f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "File size 9.214 GB with a compression ratio of 7.429x\n", "Write speed: 1748.975 MB/s of uncompressed data, or 97.475 fps.\n" ] } ], "source": [ "timing_write = _\n", "size=os.stat(filename).st_size\n", "print(f\"File size {size/(1024**3):.3f} GB with a compression ratio of {nbframes*numpy.prod(shape)*dtype.itemsize/size:.3f}x\")\n", "print(f\"Write speed: {nbframes*numpy.prod(shape)*dtype.itemsize/(1e6*timing_write.best):.3f} MB/s of uncompressed data, or {nbframes/timing_write.best:.3f} fps.\")" ] }, { "cell_type": "markdown", "id": "dc086333-96da-47eb-8477-9a0a5f60bc20", "metadata": {}, "source": [ "No optimisation is done for writing: this tutorial is focused on reading & processing speed.\n", "We keep nevertheless those figures for reference.\n", "\n", "## 4. Reading the dataset using the h5py/HDF5 library:\n", "### 4.1 Using the `h5py` API in a natural way\n", "We start with the simplest way to read back all those data:" ] }, { "cell_type": "code", "execution_count": 16, "id": "889b740b-2ba4-4677-a8d6-346608d90158", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Reading all frames and decompressing them, the natural way way\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " for i in range(nbframes):\n", " frame = ds[i][...]" ] }, { "cell_type": "code", "execution_count": 17, "id": "c748fcbe-128d-46ab-9aa7-149c9a5b31fd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Read speed: 2935.324 MB/s of uncompressed data, or 163.594 fps.\n" ] } ], "source": [ "timing_read0 = _\n", "print(f\"Read speed: {nbframes*numpy.prod(shape)*dtype.itemsize/(1e6*timing_read0.best):.3f} MB/s of uncompressed data,\\\n", " or {nbframes/timing_read0.best:.3f} fps.\")" ] }, { "cell_type": "markdown", "id": "4f95dca1-0301-4a04-b397-a9358816ee50", "metadata": {}, "source": [ "Reading all data from HDF5 file is as slow as (if not slower than) writing. \n", "This is mostly due to the decompression and to the many memory allocation performed.\n", "\n", "### 4.2 Pre-allocate the output buffer (for `h5py`)\n", "\n", "Now, we can try to pre-allocate the output buffer and check if it helps:" ] }, { "cell_type": "code", "execution_count": 18, "id": "a643e63d-1709-45b1-aaad-a542792b5a62", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Reading all frames and decompressing them\n", "buffer = numpy.zeros(shape, dtype=dtype)\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " for i in range(nbframes):\n", " ds.read_direct(buffer, numpy.s_[i,:,:], numpy.s_[:,:])" ] }, { "cell_type": "code", "execution_count": 19, "id": "847240c6-f9df-4626-b1a3-2197f535897c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Read speed: 3247.430 MB/s of uncompressed data, or 180.988 fps.\n" ] } ], "source": [ "timing_read1 = _\n", "print(f\"Read speed: {nbframes*numpy.prod(shape)*dtype.itemsize/(1e6*timing_read1.best):.3f} MB/s of uncompressed data,\\\n", " or {nbframes/timing_read1.best:.3f} fps.\")" ] }, { "cell_type": "code", "execution_count": 20, "id": "8613536f-6bd7-42ff-b401-f8ed80cdd8da", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " Speed-up: 10.6 %\n" ] } ], "source": [ "print(f\" Speed-up: {(timing_read0.best/timing_read1.best-1)*100:.1f} %\")" ] }, { "cell_type": "markdown", "id": "4f9267a3-d056-4f91-a183-3dcfa0053885", "metadata": {}, "source": [ "The gain exists but it is not huge (10%).\n", "\n", "## 5. Decouple HDF5 chunk reading from decompression.\n", "\n", "We will benchmark separately the file reading (i.e. reading chunks one by one) and decompressing to check the maximum achievable read speed.\n", "\n", "### 5.1 Benchmarking of the chunk reading using the `read_direct_chunk` from `h5py`" ] }, { "cell_type": "code", "execution_count": 21, "id": "3492bfe6-c71e-40a2-98c4-7fd2cd7d7560", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Reading all frames without decompressing them\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " for i in range(ds.id.get_num_chunks()):\n", " filter_mask, chunk = ds.id.read_direct_chunk(ds.id.get_chunk_info(i).chunk_offset)" ] }, { "cell_type": "code", "execution_count": 22, "id": "95cccf4c-f63f-4ecb-9155-41e2a1821380", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Read speed: 10900.036 MB/s of compressed data.\n", "HDF5 direct chunk read speed: 4512.810 fps (without decompression).\n" ] } ], "source": [ "timing_read2 = _\n", "print(f\"Read speed: {size/(1e6*timing_read2.best):.3f} MB/s of compressed data.\")\n", "print(f\"HDF5 direct chunk read speed: {nbframes/timing_read2.best:.3f} fps (without decompression).\")" ] }, { "cell_type": "markdown", "id": "8e1a4654-45d3-4ade-bda5-1d33ebd125a0", "metadata": {}, "source": [ "The reading part data is really fast, it is apparently mostly by the disk speed or by the memory (if the compressed dataset stays in memory). " ] }, { "cell_type": "markdown", "id": "8969804e-77af-464e-8f9e-11e0e35541bd", "metadata": {}, "source": [ "### 5.2 Benchmarking of the decompression (single threaded)\n", "\n", "The function `decompress_bslz4_chunk` can be used to decompress one chunk.\n", "We benchmark it on one chunk" ] }, { "cell_type": "code", "execution_count": 23, "id": "341d4b27-bbc3-4010-a577-820d20f89d72", "metadata": {}, "outputs": [], "source": [ "def decompress_bslz4_chunk(payload, dtype, chunk_shape):\n", " \"\"\"This function decompresses ONE chunk with bitshuffle-LZ4. \n", " The library needs to be compiled without OpenMP when using threads !\n", " \n", " :param payload: string with the compressed data as read by h5py.\n", " :param dtype: data type of the stored content\n", " :param chunk_shape: shape of one chunk\n", " :return: decompressed chunk\"\"\"\n", " total_nbytes, block_nbytes = struct.unpack(\">QI\", payload[:12])\n", " block_size = block_nbytes // dtype.itemsize\n", "\n", " arr = numpy.frombuffer(payload, dtype=numpy.uint8, offset=12) # No copy here\n", " chunk_data = bitshuffle.decompress_lz4(arr, chunk_shape, dtype, block_size)\n", " return chunk_data" ] }, { "cell_type": "code", "execution_count": 24, "id": "e836bd52-7b5f-4535-81d4-2bfc1a5dfaf9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Read chunk #123 which is 2605242 bytes long.\n", "The decompressed frame is 17942760 bytes long\n", "This frame is compressed with a ratio of 6.9 x.\n", "Benchmarking the decompression: 3.98 ms ± 5.97 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", "Decompression speed (single threaded): 251.329 fps\n", "Maximum read+decompression speed (single threaded): 238.070 fps\n", "Maximum read+decompression speed (64-threads): 3524.092 fps\n" ] } ], "source": [ "frame_id = 123\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " filter_mask, chunk = ds.id.read_direct_chunk(ds.id.get_chunk_info(frame_id).chunk_offset)\n", " \n", "print(f\"Read chunk #{frame_id} which is {len(chunk)} bytes long.\")\n", "frame = decompress_bslz4_chunk(chunk, dtype, shape)\n", "print(f\"The decompressed frame is {frame.nbytes} bytes long\")\n", "print(f\"This frame is compressed with a ratio of {frame.nbytes/len(chunk):.1f} x.\")\n", "print(\"Benchmarking the decompression: \", end=\"\")\n", "timing_decompression = %timeit -o decompress_bslz4_chunk(chunk, dtype, shape)\n", "print(f\"Decompression speed (single threaded): {1/timing_decompression.best:.3f} fps\")\n", "print(f\"Maximum read+decompression speed (single threaded): {1/(timing_decompression.best+timing_read2.best/nbframes):.3f} fps\")\n", "print(f\"Maximum read+decompression speed ({nbthreads}-threads): {1/(timing_decompression.best/nbthreads+timing_read2.best/nbframes):.3f} fps\")" ] }, { "cell_type": "markdown", "id": "ec96510d-1648-4e59-8d34-cdf37997e711", "metadata": {}, "source": [ "At this stage it is interesting to compare the maximum achievable speed in parallel and the raw read speed.\n", "\n", "This difference is known as [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law) which states that performances of a parallel program become limited limited by the serial part of it when the number of threads increases. In other words, find all the serial sections and squeeze them to get performances.\n", "\n", "Some part of the code are made serial to prevent data corruption. One typical example are the commands seek+read in file. If several threads are doing this at the same time, the file-pointer is likely to be changed and the read will return the wrong data. Serializing this section, for example by using locks, mutex, semaphores, ... is a simple way to prevent such issues. Let's list some of the lock we have in this example:\n", "\n", "- `h5py` has a lock called `phil` which serializes the access to the HDF5 library\n", "- `HDF5` has a global lock preventing files from being modified from different processes\n", "- `Python` has a global interpreter lock `GIL` which ensures only one python object is manipulated at a time.\n", "\n", "The later is widely commented and an urban legend says it prevents multithreading in Python. \n", "You will at the end of the tutorial how much this is True (or not). \n" ] }, { "cell_type": "markdown", "id": "bc5939d1-5c25-4f35-9b46-e9684110dfc9", "metadata": {}, "source": [ "### 5.3 Benchmark the analysis of the HDF5 file\n", "\n", "To come back on the parallel reading, the different locks from `h5py` and `HDF5` are preventing us from a parallel access to the data.\n", "Can we dive deeper into the `HDF5` file and retrieve the position of the different chunks and their size ? \n", "If so, it would be possible read chunks without the `h5py`/`HDF5` library, working around their different locks.\n", "\n", "Let's check the parsing of the HDF5 structure of the dataset" ] }, { "cell_type": "code", "execution_count": 25, "id": "b301837a-9f6c-43d7-b1cc-f3dc64998bad", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Each chunk descriptor is an object like: \n", "StoreInfo(chunk_offset=(0, 0, 0), filter_mask=0, byte_offset=4536, size=1972400)\n", "It represents a very small amount of data: 72 bytes.\n", "All 4096 frames, weighting 9893.291 MB, can be represented by 327.960 kB\n" ] } ], "source": [ "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " res = [ds.id.get_chunk_info(i) for i in range(ds.id.get_num_chunks())]\n", "print(f\"Each chunk descriptor is an object like: \\n{res[0]}\")\n", "print(f\"It represents a very small amount of data: {sys.getsizeof(res[0])} bytes.\")\n", "print(f\"All {nbframes} frames, weighting {size/1e6:.3f} MB, can be represented by {(sys.getsizeof(res)+sys.getsizeof(res[0])*nbframes)/1000:.3f} kB\")" ] }, { "cell_type": "code", "execution_count": 26, "id": "5201a9dd-fd9e-4e87-a2bf-f045b8029c08", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Parsing speed\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " res = [ds.id.get_chunk_info(i) for i in range(ds.id.get_num_chunks())]" ] }, { "cell_type": "code", "execution_count": 27, "id": "27a4d511-7aa0-491a-8f01-37125c9e2fe5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Parse speed: 41444.687 MB/s of compressed data.\n", "HDF5 parse speed (without reading): 17158.845 fps.\n" ] } ], "source": [ "timing_parse = _\n", "print(f\"Parse speed: {size/(1e6*timing_parse.best):.3f} MB/s of compressed data.\")\n", "print(f\"HDF5 parse speed (without reading): {nbframes/timing_parse.best:.3f} fps.\")" ] }, { "cell_type": "code", "execution_count": 28, "id": "e112c3a7-060b-4cce-a6e1-6bb060bb21b8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "db3ee77199ed8aef5aac34b91f86b3deac7bea66 using HDF5\n", "db3ee77199ed8aef5aac34b91f86b3deac7bea66 using direct file access\n" ] } ], "source": [ "# Validation that the data read by HDF5 and via the file interface matches\n", "import hashlib\n", "idx = 10\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " indexes = [ds.id.get_chunk_info(i) for i in range(ds.id.get_num_chunks())]\n", " filter_mask, ref = ds.id.read_direct_chunk(indexes[idx].chunk_offset)\n", "# and validate the indexes\n", "with open(filename, \"rb\") as f:\n", " item = indexes[idx]\n", " f.seek(item.byte_offset)\n", " res = f.read(item.size)\n", "print(f\"{hashlib.sha1(ref).hexdigest()} using HDF5\\n{hashlib.sha1(res).hexdigest()} using direct file access\")" ] }, { "cell_type": "markdown", "id": "cebd4b8d-f11c-4f65-9a8e-6f270ce0a9f4", "metadata": {}, "source": [ "So the HDF5 chunk parsing is the only part of the code needing to be serial, so the maximum achievable speed is very high: 12 kfps.\n", "\n", "If Amdahl's law is providing us with the upper performance limit, one should take care to optimize all the code to be run in parallel. \n", "\n", "Here are two ways to read the different chunks, either using the `Python file` interface or `numpy.memmap`. \n", "Their performances are expected to be similar to what `HDF5 direct chunk read` provides, the idea is to use them to bypass the locks in `HDF5`.\n", "\n", "### 5.4 Benchmark the chunk reading using the `h5py` direct chunk read" ] }, { "cell_type": "code", "execution_count": 29, "id": "4f5a31a7-b5ab-4d68-b55f-76f3159f3cb7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Reading all frames without decompressing them\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " for chunk_descr in indexes:\n", " filter_mask, chunk = ds.id.read_direct_chunk(chunk_descr.chunk_offset)" ] }, { "cell_type": "code", "execution_count": 30, "id": "64a1861b-e09b-4753-afe0-16db72131408", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Read speed (h5py direct chunk read): 15994.548 MB/s of compressed data.\n", "Chunk read (from h5py) speed (without decompression): 6622.030 fps.\n" ] } ], "source": [ "timing_read2a = _\n", "print(f\"Read speed (h5py direct chunk read): {size/(1e6*timing_read2a.best):.3f} MB/s of compressed data.\")\n", "print(f\"Chunk read (from h5py) speed (without decompression): {nbframes/timing_read2a.best:.3f} fps.\")" ] }, { "cell_type": "markdown", "id": "177bd11f-1a31-4965-9ae1-23985a2ed084", "metadata": {}, "source": [ "### 5.5 Benchmark the chunk reading using the Python file interface" ] }, { "cell_type": "code", "execution_count": 31, "id": "e06a3939-fe2a-485f-b774-229c75565334", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Reading all frames without using the HDF5 library (neither decompressing them)\n", "with open(filename, \"rb\") as f:\n", " for chunk_descr in indexes:\n", " f.seek(chunk_descr.byte_offset)\n", " chunk = f.read(chunk_descr.size)" ] }, { "cell_type": "code", "execution_count": 32, "id": "ae59eaeb-0b0b-4f36-a4f7-990b7c61100f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Read speed (Python file): 16127.053 MB/s of compressed data.\n", "File read (from Python) speed (without decompression): 6676.890 fps.\n", "Pure reading using the Python (file interface) is 0.8 % faster than HDF5 direct chunk read.\n", "But it removes the file-locking issue from HDF5 !\n" ] } ], "source": [ "timing_read3 = _\n", "print(f\"Read speed (Python file): {size/(1e6*timing_read3.best):.3f} MB/s of compressed data.\")\n", "print(f\"File read (from Python) speed (without decompression): {nbframes/timing_read3.best:.3f} fps.\")\n", "print(f\"Pure reading using the Python (file interface) is {100*timing_read2a.best/(timing_read3.best)-100:.1f} % faster than HDF5 direct chunk read.\")\n", "print(\"But it removes the file-locking issue from HDF5 !\")" ] }, { "cell_type": "markdown", "id": "47590db3-de63-4179-9921-1d88d6a0a64b", "metadata": {}, "source": [ "### 5.5 Benchmark the chunk reading using `numpy.memmap`" ] }, { "cell_type": "code", "execution_count": 33, "id": "59e6da20-bc46-42dd-830c-6f9b46c6e367", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -r1 -n1 -o -q\n", "#Reading positions via HDF5 but chunks are read via numpy.memmap\n", "f = numpy.memmap(filename, mode=\"r\")\n", "for chunk_descr in indexes:\n", " chunk = numpy.array(f[chunk_descr.byte_offset:chunk_descr.byte_offset+chunk_descr.size])\n", "del f" ] }, { "cell_type": "code", "execution_count": 34, "id": "06209e35-1813-44f4-b340-b3e60b359a8d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Read speed (numpy.memmap): 13048.649 MB/s of compressed data.\n", "File read (numpy.memmap) speed (without decompression): 5402.375 fps.\n", "Pure reading using the numpy.memmap is -18.4 % faster than using the h5py/HDF5 interface\n", "This removes the file-locking issue from HDF5 !\n" ] } ], "source": [ "timing_read4 = _\n", "print(f\"Read speed (numpy.memmap): {size/(1e6*timing_read4.best):.3f} MB/s of compressed data.\")\n", "print(f\"File read (numpy.memmap) speed (without decompression): {nbframes/timing_read4.best:.3f} fps.\")\n", "print(f\"Pure reading using the numpy.memmap is {100*timing_read2a.best/(timing_read4.best)-100:.1f} % faster than using the h5py/HDF5 interface\")\n", "print(\"This removes the file-locking issue from HDF5 !\")" ] }, { "cell_type": "markdown", "id": "3cdcc158-260c-445c-acbb-8da51e3be6ca", "metadata": {}, "source": [ "Numpy's memmap apprears to be much slow than the equivalent python file read.\n", "\n", "We found out that the reading of data, initially in the order of 1 minute can be decomposed into:\n", "\n", " * 0.3s for the reading of the chunk description\n", " * 1s for the reading of the chunks themselves\n", " * 1 minute for the decompression of the data.\n", "\n", "Two parallelization schemes appear clearly:\n", "1. read chunks in serial mode with h5py and decompress+integrate in parallel.\n", "2. read chunk descriptors in serial mode with h5py and parallelize the reading, decompression and integration.\n", "\n", "But befor we can investigate those two routes, we first need to establish some baseline for the complete serial processing: read, decompress, integrate.\n", "\n", "## 6. Azimuthal integration\n", "\n", "### 6.1 Serial workflow\n" ] }, { "cell_type": "markdown", "id": "9fccb178-94db-4bb4-a5e1-9cab60af0aeb", "metadata": {}, "source": [ "#### 6.1.1 Prepare the azimuthal integrator\n", "To allow the full parallelization of different integrators working in parallel, one must limit the number of Python call performed, this is why we need to extract the Cython integrator from AzimuthalIntegator. The integrator used here is a sparse matrix multiplication one with a CSC representation which is single-threaded. This engine is usually not the fastest but it is multitheading friendly.\n", "\n", "The figures obtained should be similar to the one obtaind in chapter 2, the overhead from the azimuthal integrator being tuned to be minimal." ] }, { "cell_type": "code", "execution_count": 35, "id": "0814ccb4-2906-44e6-9061-bb36c5139b3e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Timing for the direct azimuthal integration: 37.1 ms ± 44.1 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", "The maximum achievable integration speed on a single core is 27.041 fps which does not look fancy.\n", "But parallelized over 64 threads, it could reach: 1730.592 fps!\n" ] } ], "source": [ "geo = {\"detector\": det, \n", " \"wavelength\": 1e-10}\n", "ai = pyFAI.load(geo)\n", "omega = ai.solidAngleArray()\n", "res0 = ai.integrate1d(frame, nbins, method=(\"full\", \"csc\", \"cython\"))\n", "engine = ai.engines[res0.method].engine\n", "#This is how the engine works:\n", "res1 = engine.integrate_ng(frame, solidangle=omega)\n", "assert numpy.allclose(res0.intensity, res1.intensity) # validates the equivalence of both approaches:\n", "print(\"Timing for the direct azimuthal integration: \", end=\"\")\n", "timing_integration = %timeit -o engine.integrate_ng(frame, solidangle=omega)\n", "print(f\"The maximum achievable integration speed on a single core is {1/timing_integration.best:.3f} fps which does not look fancy.\")\n", "print(f\"But parallelized over {nbthreads} threads, it could reach: {nbthreads/timing_integration.best:.3f} fps!\")" ] }, { "cell_type": "markdown", "id": "6178d4b3-86f0-4428-a147-90cff651ebd3", "metadata": {}, "source": [ "### 6.1.2 Benchmarking of the serial workflow\n", "\n", "This code tries to be simple and elegant. \n", "It provides the reference values on the one hand and the baseline performances on the other." ] }, { "cell_type": "code", "execution_count": 36, "id": "59e5bc41-f960-47a3-a9d2-133c2131e60d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%timeit -o -r1 -n1 -q\n", "#Naive implementation ... read+integrate\n", "result0 = numpy.empty((nbframes, nbins), dtype=numpy.float32)\n", "method = (\"full\", \"csc\", \"cython\")\n", "\n", "with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " for i, frame in enumerate(ds):\n", " result0[i] = ai.integrate1d(frame, nbins, method=method).intensity" ] }, { "cell_type": "code", "execution_count": 37, "id": "ce61a7ec-577e-4e6a-bf59-e0cc8f60478d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "A naive implementation provides only 22.895 fps.\n" ] } ], "source": [ "timing_naive = _\n", "# print(f\"The maximum achievable decompression+integration speed is {1/(timing_decompress.best+timing_integration.best):.3f} fps in serial \\n\\\n", "# and {nbthreads*1/(timing_decompress.best+timing_integration.best):.3f} fps in parallel on {nbthreads} threads\\n\\\n", "print(f\"A naive implementation provides only {nbframes/(timing_naive.best):.3f} fps.\")" ] }, { "cell_type": "markdown", "id": "0131a856-0a8b-4845-91a8-287abc0edf01", "metadata": {}, "source": [ "## 6.2 Pool of threads, queues, \n", "\n", "Unlike processes, threads share the same memory space (with the GIL preventing read/write collision).\n", "Threads are a great idea which allow multiple flow of execution to occure in parallel but threads come with a cost.\n", "Thus it is stupid to have as many threads as tasks to perform. \n", "It is better to have a limited number of threads, on the order of the number of cores, and have them processing several frames.\n", "\n", "We will define a pool of threads, a list of threads, started and ready to crunch some data.\n", "Communication between threads can be made via `Queues`.\n", "Each worker waits on the input-queue (`qin`) for something to process and puts the result into the output queue (`qout`).\n", "Since we want the processing to tidy up at the end, if a worker gets a `None` this means it is time to end the thread. \n", "This is sometimes called a \"kill-pill\". \n", "The `end_pool` function distributes as many \"kill-pills\" as needed to end all threads in the pool. \n", "\n", "In this section we define some tools to create and stop a pool of worker and also a dummy_worker which does nothing:" ] }, { "cell_type": "code", "execution_count": 38, "id": "def90b10-3d93-4051-98c4-6de6e7db3e37", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "None\n", "None\n", "None\n", "None\n" ] } ], "source": [ "# a few of utility functions\n", "def dummy_worker(qin, qout, funct=lambda item: item):\n", " \"\"\"Dummy worker which takes something in qin, applies funct on it and puts the result in qout\"\"\"\n", " while True:\n", " item = qin.get()\n", " if item is None:\n", " qout.put(None)\n", " qin.task_done()\n", " return\n", " qout.put(funct(item))\n", " qin.task_done()\n", "\n", "def build_pool(nbthreads, qin, qout, worker=None, funct=None):\n", " \"\"\"Build a pool of threads with workers, and starts them\"\"\"\n", " pool = []\n", " for i in range(nbthreads):\n", " if funct is not None:\n", " worker = dummy_worker\n", " thread = threading.Thread(target=worker, name=f\"{worker.__name__}_{i:02d}\", args=(qin, qout, funct))\n", " elif worker is None:\n", " worker = dummy_worker\n", " thread = threading.Thread(target=worker, name=f\"{worker.__name__}_{i:02d}\", args=(qin, qout))\n", " else:\n", " thread = threading.Thread(target=worker, name=f\"{worker.__name__}_{i:02d}\", args=(qin, qout, filename))\n", " thread.start()\n", " pool.append(thread)\n", " return pool\n", "\n", "def end_pool(pool):\n", " \"\"\"Ends all threads from a pool by sending them a \"kill-pill\"\"\"\n", " for thread in pool:\n", " qin.put(None)\n", "\n", "\n", "#Small validation to check it works: \n", "qin = Queue()\n", "qout = Queue()\n", "pool=build_pool(4, qin, qout, dummy_worker)\n", "end_pool(pool)\n", "qin.join()\n", "while not qout.empty():\n", " print(qout.get())\n", " qout.task_done()\n", "qout.join()" ] }, { "cell_type": "markdown", "id": "dc93cd8b-3f93-4f21-b8a9-97e4bd08ca93", "metadata": {}, "source": [ "### 6.3 Parallelize decompression + processing\n", "\n", "In this example, all chunks are read by the HDF5 library and put in a queue for the processing.\n", "As a consequence, all chunks are likely to be held in memory at the same time, which is equivalent of the size of the compressed HDF5 file, 10GB. \n", "This could be a problem on many computer and we choose to limit the number of chunks in memory to ~10x more than the number of threads.\n", "The implementation of the the slow-down mechanism is done via the size of the input queue (into which the reader puts chunks).\n" ] }, { "cell_type": "code", "execution_count": 39, "id": "b0cffcf6-ede0-4cf0-8e38-f247e8c15352", "metadata": {}, "outputs": [], "source": [ "def reader_chunks(filename, h5path, queue):\n", " \"\"\"Function reading the HDF5 file and enqueuing raw-chunks into the queue.\n", " \n", " :param filename: name of the HDF5 file\n", " :param h5path: path to the dataset within the HDF5 file\n", " :param queue: queue where to put read chunks\n", " :return: number of chunks\"\"\"\n", " with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " for i in range(ds.id.get_num_chunks()):\n", " filter_mask, chunk = ds.id.read_direct_chunk(ds.id.get_chunk_info(i).chunk_offset)\n", " if filter_mask==0:\n", " while queue.full():\n", " # slow down to prevent filling up memory\n", " os.sched_yield()\n", " queue.put(Item(i, chunk))\n", " return i+1" ] }, { "cell_type": "code", "execution_count": 40, "id": "098b0fc0-4c60-4e73-80a6-3a1e3f5ed867", "metadata": {}, "outputs": [], "source": [ "def decompress_integrate_funct(item):\n", " \"function to be used within a dummy_worker: takes an item and returns an item\"\n", " frame = decompress_bslz4_chunk(item.data, dtype, shape)\n", " return Item(item.index, engine.integrate_ng(frame, solidangle=omega).intensity)\n" ] }, { "cell_type": "code", "execution_count": 41, "id": "bf81e2e9-a2d0-45fb-8108-446549a27ba0", "metadata": {}, "outputs": [], "source": [ "def parallel_decompress_integrate(filename, h5path, nbthreads):\n", " qin = Queue(nbthreads*10)\n", " qout = Queue()\n", " pool = build_pool(nbthreads, qin, qout, funct=decompress_integrate_funct)\n", " nchunks = reader_chunks(filename, h5path, qin)\n", " output = numpy.empty((nchunks, nbins), numpy.float32)\n", " end_pool(pool)\n", " qin.join()\n", " while not qout.empty():\n", " item = qout.get()\n", " if item is not None:\n", " output[item.index] = item.data\n", " qout.task_done()\n", " qout.join()\n", " return output\n", " \n", "# parallel_decompress_integrate(filename, h5path, nbthreads)" ] }, { "cell_type": "code", "execution_count": 42, "id": "3b510f2f-49b3-41a2-8b71-81a41f725ec0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Timimg of serial read (h5py direct) and 64x(decompression+integration): \n", "8.79 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Direct read + // integration reaches 466.170 fps.\n", "The speed-up is 20.361x for a computer with 64.\n" ] } ], "source": [ "print(f\"Timimg of serial read (h5py direct) and {nbthreads}x(decompression+integration): \")\n", "timing_dcr = %timeit -o -r1 -n1 parallel_decompress_integrate(filename, h5path, nbthreads)\n", "print(f\"Direct read + // integration reaches {nbframes/(timing_dcr.best):.3f} fps.\")\n", "print(f\"The speed-up is {timing_naive.best/timing_dcr.best:.3f}x for a computer with {nbthreads} threads.\")" ] }, { "cell_type": "markdown", "id": "8cdf7557-f6fe-46d7-bdca-770e2e62c194", "metadata": {}, "source": [ "### 6.3 Parallelize read + decompression + processing\n", "We will now investigate the case where even the reading is made in the worker thread.\n", "One advantage is that all chunk-descriptions can be hosted in memory (hundreeds of kilobytes) and one does not need to take care of memory filling up with raw data.\n", "\n", "Here is the reader for such type of processing:" ] }, { "cell_type": "code", "execution_count": 43, "id": "82e58870-ebb0-45ff-aec2-f203f25a9f44", "metadata": {}, "outputs": [], "source": [ "def reader_descr(filename, h5path, queue):\n", " \"\"\"Function reading the HDF5 file and enqueuing chunk-descriptor into the queue.\n", " \n", " :param filename: name of the HDF5 file\n", " :param h5path: path to the dataset within the HDF5 file\n", " :param queue: queue where to put read chunks\n", " :return: number of chunks\"\"\"\n", " with h5py.File(filename, \"r\") as h:\n", " ds = h[h5path]\n", " for i in range(ds.id.get_num_chunks()):\n", " queue.put(Item(i, ds.id.get_chunk_info(i)))\n", " return i+1" ] }, { "cell_type": "code", "execution_count": 44, "id": "24158f69-2577-42c6-9cc1-8e9a1ad74581", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The reader is providing performances close to those benchmarked at section #5.3:\n", "290 ms ± 32.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", "It is measured 12.108 % slower.\n", "The reader is able to reach 15305.633 fps\n" ] } ], "source": [ "print(\"The reader is providing performances close to those benchmarked at section #5.3:\")\n", "timing_reader_descr = %timeit -o reader_descr(filename, h5path, Queue())\n", "print(f\"It is measured {100*(timing_reader_descr.best/timing_parse.best-1):.3f} % slower.\")\n", "print(f\"The reader is able to reach {nbframes/timing_reader_descr.best:.3f} fps\")" ] }, { "cell_type": "markdown", "id": "4fee001e-da99-4e3d-b266-0dd8fd18e08f", "metadata": {}, "source": [ "#### 6.3.1 Parallelize read + decompression + processing using the Python file interface" ] }, { "cell_type": "code", "execution_count": 45, "id": "13b0dd04-9f28-412f-98d6-1511eb163698", "metadata": {}, "outputs": [], "source": [ "def worker_python(qin, qout, filename):\n", " with open(filename, \"rb\") as f:\n", " while True:\n", " item = qin.get() \n", " qin.task_done()\n", " if item is None:\n", " return\n", " idx, chunk_descr = item\n", " f.seek(chunk_descr.byte_offset)\n", " chunk = f.read(chunk_descr.size)\n", " frame = decompress_bslz4_chunk(chunk, dtype, shape)\n", " qout.put(Item(idx, engine.integrate_ng(frame, solidangle=omega).intensity))\n", " del chunk, frame" ] }, { "cell_type": "code", "execution_count": 46, "id": "1147ec52-99be-474e-b7c8-1c456c05e091", "metadata": {}, "outputs": [], "source": [ "def parallel_read_decompress_integrate(filename, h5path, nbthreads, worker):\n", " qin = Queue()\n", " qout = Queue()\n", " pool = build_pool(nbthreads, qin, qout, worker=worker)\n", " nchunks = reader_descr(filename, h5path, qin)\n", " output = numpy.empty((nchunks, nbins), numpy.float32)\n", " end_pool(pool)\n", " qin.join()\n", " while not qout.empty():\n", " item = qout.get()\n", " if item is not None:\n", " output[item.index] = item.data\n", " qout.task_done()\n", " qout.join()\n", " return output" ] }, { "cell_type": "code", "execution_count": 47, "id": "72ee1bc4-5d8a-4eaf-99e8-a9323b0d1c93", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Timimg of serial descriptor read and 64x(read+decompression+integration): \n", "9.01 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Parallel read+integration reaches 454.561 fps.\n", "The speed-up is 19.854x for a computer with 64.\n" ] } ], "source": [ "print(f\"Timimg of serial descriptor read and {nbthreads}x(read+decompression+integration): \")\n", "timing_python_file = %timeit -o -r1 -n1 parallel_read_decompress_integrate(filename, h5path, nbthreads, worker_python)\n", "print(f\"Parallel read+integration reaches {nbframes/(timing_python_file.best):.3f} fps.\")\n", "print(f\"The speed-up is {timing_naive.best/timing_python_file.best:.3f}x for a computer with {nbthreads} threads.\")" ] }, { "cell_type": "markdown", "id": "aa63fdcd-1d58-4ca1-b51f-6d0ab3bbaf0a", "metadata": {}, "source": [ "#### 6.3.1 Parallelize read + decompression + processing using the `numpy.memmap` interface" ] }, { "cell_type": "code", "execution_count": 48, "id": "8001b38d-1837-45a5-838c-15e12abd0995", "metadata": {}, "outputs": [], "source": [ "def worker_numpy(qin, qout, filename):\n", " f = numpy.memmap(filename, mode=\"r\")\n", " while True:\n", " item = qin.get() \n", " qin.task_done()\n", " if item is None:\n", " del f\n", " return\n", " idx, chunk_descr = item\n", " chunk = f[chunk_descr.byte_offset:chunk_descr.byte_offset+chunk_descr.size]\n", " frame = decompress_bslz4_chunk(chunk, dtype, shape)\n", " qout.put(Item(idx, engine.integrate_ng(frame, solidangle=omega).intensity))\n", " del chunk, frame" ] }, { "cell_type": "code", "execution_count": 49, "id": "a0906da8-4b10-452b-b4a5-9fa0f49159a9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Timimg of serial descriptor read and 64x(read+decompression+integration): \n", "7.57 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Parallel read+integration reaches 541.320 fps.\n", "The speed-up is 23.644x for a computer with 64.\n" ] } ], "source": [ "print(f\"Timimg of serial descriptor read and {nbthreads}x(read+decompression+integration): \")\n", "timing_numpy_file = %timeit -o -r1 -n1 parallel_read_decompress_integrate(filename, h5path, nbthreads, worker_numpy)\n", "print(f\"Parallel read+integration reaches {nbframes/(timing_numpy_file.best):.3f} fps.\")\n", "print(f\"The speed-up is {timing_naive.best/timing_numpy_file.best:.3f}x for a computer with {nbthreads} threads.\")" ] }, { "cell_type": "markdown", "id": "c314a9d6-4878-480a-8625-ba6b295bf96f", "metadata": {}, "source": [ "Effective implementation using multithreading:\n", "* One reader which reads the dataset chunk-by-chunk or descriptor by descriptor and makes them available via an input-queue, called `qin`.\n", "* A pool of workers: pool of the size of the number of cores. Each worker is doing the (reading of one chunk if needed), decompression of one chunk into one frame and the azimuthal integration of that frame. The integrated result is put into an output-queue, called `qout`.\n", "* 2 queues: `qin` and `qout`, the former could need to be limited in size to prevent the filling-up of the memory when complete chunks are put into it.\n", "* The gathering of the data is performed in the main thread (as does the reader). Each piece of data is associated with its index in the dataset using the Item named-tuple.\n", "\n", "Nota: I had a hard time to perform both reading and writing with HDF5 (even in different files). This is why the result is reconstructed in memory and the saving performed at the very end. Could be a bug in h5py." ] }, { "cell_type": "markdown", "id": "769fd274-e668-4065-ba8f-681a56906838", "metadata": {}, "source": [ "## 7. Display some results\n", "Since the input data were all synthetic and similar, no great science is expected from this... but one can ensure each frame differs slightly from the neighbors with a pattern of 500 frames. " ] }, { "cell_type": "code", "execution_count": 50, "id": "478cf30b-a1ca-4c62-8031-c364c2543816", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 4min 59s, sys: 13.1 s, total: 5min 12s\n", "Wall time: 7.84 s\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 50, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "%time result = parallel_read_decompress_integrate(filename, h5path, nbthreads, worker_numpy)\n", "fig,ax = subplots(figsize=(8,8))\n", "ax.imshow(result)" ] }, { "cell_type": "markdown", "id": "d52a13fe-3265-4e74-866e-8056e105dce5", "metadata": {}, "source": [ "## 7. Evolution of the performances with the number of threads" ] }, { "cell_type": "code", "execution_count": 51, "id": "a89e908a-d950-48d6-8ff2-2b39cdda629e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Using 64 threads\n", "8.58 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8.64 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "7.38 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 56 threads\n", "8.4 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8.62 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "7.15 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 48 threads\n", "6.54 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "7.93 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "7.49 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 40 threads\n", "7.8 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8.08 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "7.48 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 36 threads\n", "7.48 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8.34 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 32 threads\n", "9.05 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8.5 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8.38 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 28 threads\n", "9.91 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "9.14 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "8.05 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 24 threads\n", "11.2 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "10.2 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "9.56 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 20 threads\n", "13.8 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "11.7 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "10.9 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 16 threads\n", "17.8 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "13.5 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "13.1 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 12 threads\n", "24.3 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "17.5 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "17.1 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 8 threads\n", "38 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "25.3 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "23.3 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 4 threads\n", "1min 19s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "48.2 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "45.9 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 2 threads\n", "2min 44s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "1min 35s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "1min 32s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "Using 1 threads\n", "5min 46s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "3min 5s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n", "3min ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" ] } ], "source": [ "performances_h5py = {}\n", "performances_file = {}\n", "performances_memmap = {}\n", "for i in (64, 56, 48, 40, 36, 32, 28, 24, 20,16, 12, 8, 4, 2, 1):\n", " print(f\"Using {i} threads\")\n", " \n", " t = %timeit -r1 -n1 -o parallel_decompress_integrate(filename, h5path, i)\n", " performances_h5py[i] = nbframes/t.best\n", "\n", " t = %timeit -r1 -n1 -o parallel_read_decompress_integrate(filename, h5path, i, worker_python)\n", " performances_file[i] = nbframes/t.best\n", "\n", " t = %timeit -r1 -n1 -o parallel_read_decompress_integrate(filename, h5path, i, worker_numpy)\n", " performances_memmap[i] = nbframes/t.best\n" ] }, { "cell_type": "code", "execution_count": 56, "id": "1d6c9040-6de2-47b4-b598-528b96833de3", "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "fig, ax = subplots(figsize=(10,8))\n", "ax.plot(list(performances_h5py.keys()),list(performances_h5py.values()), \"o-\", label=\"h5py direct chunk\")\n", "ax.plot(list(performances_file.keys()),list(performances_file.values()), \"o-\", label=\"python file\")\n", "ax.plot(list(performances_memmap.keys()),list(performances_memmap.values()), \"o-\", label=\"numpy memmap\")\n", "ax.legend()\n", "ax.set_xlabel(\"Number of threads\")\n", "ax.set_ylabel(\"Number of frames processed by second (fps)\")\n", "ax.set_title(\"Performances to read HDF5 + azimuthal integration of 4Mpix images\")\n", "pass" ] }, { "cell_type": "markdown", "id": "094b673d-f373-431b-92ed-4c9ef9ff3c79", "metadata": {}, "source": [ "## 8. Conclusion\n", "\n", "Reading Bitshuffle-LZ4 data can be parallelized using multi-threading in Python. \n", "\n", "The procedure is a bit tedious but not out of reach for a Python programmer: few threads and a couple of queues. \n", "This burden is worth when coupling decompression with azimuthal integration to reduce the amount of data stored in memory.\n", "\n", "The performances obtained on a 64-core computer are close to what can be obtained from a GPU: ~500 fps\n", "The speed-up obtained with the procedure is 30x on a 64-core computer versus single threaded implementation, which demonstrates multithreading is worth the burden.\n", "\n", "One notices the effort for going arount the different locks from `HDF5` and `h5py` did not bring much more performances. This could be linked to a common limiting factor: the `GIL`. \n", "This demonstration shows multithreaded python is possible but the number of effectively parallel threads is \n", "limited around 40-48 threds on the 2x32 core computer. \n", "Other methods exists to have more simulaneous core: multiprocessing, but also GPU processing which exposed in other notebooks." ] }, { "cell_type": "markdown", "id": "0d2b8105-71d9-4440-93f9-0a30f8f507cc", "metadata": {}, "source": [ "Thanks again to the French-CRG for the computer." ] }, { "cell_type": "code", "execution_count": 53, "id": "b90f5458-9fd4-4d83-bfbd-017e3a9365dd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total processing time: 1988.758 s\n" ] } ], "source": [ "print(f\"Total processing time: {time.time()-start_time:.3f} s\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.12" } }, "nbformat": 4, "nbformat_minor": 5 }