diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index c66e373e7..019830b55 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -101,3 +101,7 @@ ConfigureNVBench(RETRIEVE_BENCH # - reduce_by_key benchmarks ---------------------------------------------------------------------- set(RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/reduce_by_key.cu") ConfigureBench(RBK_BENCH "${RBK_BENCH_SRC}") + +################################################################################################### +ConfigureNVBench(BLOOM_FILTER_BENCH + "bloom_filter/bloom_filter_bench.cu") diff --git a/benchmarks/analysis/notebooks/bloom_filter_bench.ipynb b/benchmarks/analysis/notebooks/bloom_filter_bench.ipynb new file mode 100644 index 000000000..6d2880aa6 --- /dev/null +++ b/benchmarks/analysis/notebooks/bloom_filter_bench.ipynb @@ -0,0 +1,261 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Preparation" + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 1, + "source": [ + "!pip3 install pandas\n", + "!pip3 install matplotlib\n", + "\n", + "# Import libraries\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib\n", + "from collections import namedtuple\n", + "\n", + "#plt.style.use('seaborn-white')" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Requirement already satisfied: pandas in /home/djuenger/miniconda3/lib/python3.9/site-packages (1.3.1)\n", + "Requirement already satisfied: numpy>=1.17.3 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from pandas) (1.21.1)\n", + "Requirement already satisfied: pytz>=2017.3 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from pandas) (2021.1)\n", + "Requirement already satisfied: python-dateutil>=2.7.3 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from pandas) (2.8.2)\n", + "Requirement already satisfied: six>=1.5 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from python-dateutil>=2.7.3->pandas) (1.16.0)\n", + "Requirement already satisfied: matplotlib in /home/djuenger/miniconda3/lib/python3.9/site-packages (3.4.2)\n", + "Requirement already satisfied: pillow>=6.2.0 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (8.3.1)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (1.3.1)\n", + "Requirement already satisfied: pyparsing>=2.2.1 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (2.4.7)\n", + "Requirement already satisfied: cycler>=0.10 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (0.10.0)\n", + "Requirement already satisfied: numpy>=1.16 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (1.21.1)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (2.8.2)\n", + "Requirement already satisfied: six in /home/djuenger/miniconda3/lib/python3.9/site-packages (from cycler>=0.10->matplotlib) (1.16.0)\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "source": [ + "# helper functions\n", + "\n", + "style_ = namedtuple(\"style_\", [\"color\", \"marker\", \"linestyle\"])\n", + "\n", + "def load_csv_files(csv_files):\n", + " dfs = {}\n", + " for key, fname in csv_files.items():\n", + " df = pd.read_csv(fname)\n", + " dfs[key] = df[df[\"Skipped\"] == \"No\"]\n", + " return dfs\n", + "\n", + "def filter_bench(dfs, query):\n", + " if isinstance(dfs, dict):\n", + " filtered_dfs = {}\n", + " for key in dfs.keys():\n", + " filtered_dfs[key] = dfs[key].query(query)\n", + " return filtered_dfs\n", + " else:\n", + " return dfs.query(query)\n", + "\n", + "def plot_bench(dfs, xlabel, styles, show_legend=True, title=None, ofname=None, show_xlabel=True, show_ylabel=True, log_xscale=False, log_yscale=False, font_size=14):\n", + " fig, ax = plt.subplots(1, 1)\n", + "\n", + " ax.tick_params(labelsize=font_size)\n", + " if(show_ylabel):\n", + " ax.set_xlabel(xlabel, fontsize=font_size)\n", + " if(show_ylabel):\n", + " ax.set_ylabel(\"Operations per second\", fontsize=font_size)\n", + " if(log_xscale):\n", + " ax.set_xscale('log')\n", + " if(log_yscale):\n", + " ax.set_yscale('log')\n", + " ax.set_title(title, fontsize=font_size)\n", + " ax.grid()\n", + "\n", + " for key, df in dfs.items(): \n", + " style = styles[key]\n", + "\n", + " Y = df[\"NumInputs\"].unique()[0]/df[\"GPU Time (sec)\"]\n", + "\n", + " if xlabel in df.columns:\n", + " X = df[xlabel]\n", + " \n", + " ax.plot(X, Y, label=key, color=style.color, marker=style.marker, linestyle=style.linestyle)\n", + " ax.scatter(X, Y, color=style.color, marker=style.marker, linestyle=style.linestyle)\n", + " else:\n", + " ax.axhline(y=Y.iloc[0], label=key, color=style.color, linestyle=style.linestyle)\n", + "\n", + " if(show_legend):\n", + " plt.legend(fontsize=font_size - 4)\n", + "\n", + " if(ofname):\n", + " plt.savefig(ofname, dpi=1200, format='png', bbox_inches='tight')\n", + "\n", + " plt.show()" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 8, + "source": [ + "# GMEM\n", + "dfs = load_csv_files({\"V100\" : \"../results/bloom_filter_v100.csv\", \n", + " \"A100\" : \"../results/bloom_filter_a100.csv\"})\n", + " \n", + "dfs[\"V100\"][\"Filter Size [MB]\"] = dfs[\"V100\"][\"NumBits\"] / 8 / 1000 / 1000\n", + "dfs[\"A100\"][\"Filter Size [MB]\"] = dfs[\"A100\"][\"NumBits\"] / 8 / 1000 / 1000\n", + "\n", + "dfs[\"V100 INSERT\"] = dfs[\"V100\"].query('Operation == \"INSERT\"')\n", + "dfs[\"V100 CONTAINS\"] = dfs[\"V100\"].query('Operation == \"CONTAINS\"')\n", + "dfs[\"A100 INSERT\"] = dfs[\"A100\"].query('Operation == \"INSERT\"')\n", + "dfs[\"A100 CONTAINS\"] = dfs[\"A100\"].query('Operation == \"CONTAINS\"')\n", + "del dfs[\"V100\"]\n", + "del dfs[\"A100\"]\n", + "\n", + "styles = {\n", + " \"V100 INSERT\" : style_('b', 'x', '-'),\n", + " \"V100 CONTAINS\" : style_('b', 'x', '--'),\n", + " \"A100 INSERT\" : style_('r', 'o', '-'),\n", + " \"A100 CONTAINS\" : style_('r', 'o', '--')}\n", + "\n", + "query = 'Skipped == \"No\" and\\\n", + " KeyType == \"I32\" and\\\n", + " SlotType == \"I32\" and\\\n", + " NumHashes == 2 and\\\n", + " NumInputs == 1000000000'\n", + "\n", + "print(\"INSERT/CONTAINS on V100/A100 (GMEM)\")\n", + "plot_bench(filter_bench(dfs, query), \"Filter Size [MB]\", styles=styles, log_xscale=True)\n", + "\n", + "query = query + ' and NumBits > 100000000'\n", + "plot_bench(filter_bench(dfs, query), \"Filter Size [MB]\", styles=styles)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "INSERT/CONTAINS on V100/A100 (GMEM)\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n \n \n \n \n 2021-08-23T07:05:06.147587\n image/svg+xml\n \n \n Matplotlib v3.4.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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DYWfv2SJ530iqsCdnYazc6hdW/fHxMCLL+bdd+QInHqq7s/M1N/l7Fycx7Vvr8p/50647bYCt6125536dvLzz3DGGZ7lW7u28N9miRjGjAHHANUjdevCxRdDnTqweze0agX//a8WV7Ky4omOhqQkeOopz9erVq0anTt35qOPPuLKK69kwYIF9OvXr1BlnZKSQpUqVYhxvAXXr18/R8Hu2rWLBg0aABATE0PlypVJSUmhRo0ahf6uiRMnct999+VR/tWqVeOKK66gUaNGnH/++fTq1Ytrr72WqCi1lE+fPp158+YBULVqVVasWJHnmgcOHGDz5s2ce+65hd47lESm8h86VE0pEyaoAmzQQO3YTlNLuCCiJihXM5QrcXEwYoTn82vVUhu9J1q3zn0rcek89ju9Aho1ghdegFGj3J/vfEOwWNAX6jp14K+/oGFD3S4pTtOPU/nPnj275Bf1EaeCXrVqVZ76l19+mXXr1rFs2TKmTZvGZ599xpw5cwDPZp+vvvqKdu3asXnzZsaMGUNt50AuDIlM5Q+q6MNN2Qeb6GioXl2LCyecr9K1a8PIkeodtW9fwfMLe+OxRBSFjdCdOE09992nL6STJuWdA3CSmnrMa7/1K6+8krFjx/LTTz9x9OhROnbsWOjx1atX5+DBg2RmZhITE8POnTup55iXqlevHjt27KB+/fpkZmZy6NChHLNNUUycOJEpU6bkvFE4adu2LW3btuX666+nSZMmOcrfE06b/9atWznjjDPo168fSUlJXskQbCLP28fiOw89BFH5HoWoKHj66dDIYwk7XG38Dzygn/36aX1JSExMpHv37gwdOpRrr722yONFhO7du7No0SIA5s6dy5VXXgnAFVdcwdy5cwFYtGgRPXr08NoF8qKLLuLAgQP88ssvAKSlpeWZb1izZg2NfFgj1KRJE+6++24ee+wxr88JNlb5W3R+5LXX9J1eRN/ns7PV28hiQb16nJO9oJ9vvaX1JeXaa69l7dq1BZR/165dueaaa1i+fDn169fPcQt97LHHePLJJ2natCkpKSnceOONANx4442kpKTQtGlTnnzySR599FGf5Jg4cSI7duwAdGJ56tSptGjRgqSkJCZNmpRn1D99+vQ8rp7u3EpHjBjBl19+6XZfOFAqcvh26tTJ2Hj+/qPI9srOhrPOUuPupk1QxpfqR+rztXHj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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n \n \n \n \n 2021-08-23T07:05:06.881367\n image/svg+xml\n \n \n Matplotlib v3.4.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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guIef/xxrrnmGgBOP/106taty9lnnx2UZ/369Zxwwgm0adOGCy+8kAMHDgCQmZnJhRdeSJs2bTjhhBNITU3NJzM1NZVOnToBkJKSgogwb9683PSzzz6blJQUAObPn8+xxx5Lly5d6NixI88//zwAEyZMoFmzZiQnJ+eGnTt3kpKSQp06dUhOTqZ9+/bceuutrFy5MjdP/fr1ad26Nb169aJ///I/DbsiGoxknJdcwzBinEmTYPHi4LjFi118cRg2bBihc5Nz5sxh2LBhANx222288sor+crdfvvt3HTTTaxbt4569erx0ksvAfDSSy9Rr1491q1bx0033cTtt99eqA5JSUk8+GD+Y4CysrIYOXIk8+bN49tvv+Wbb76hT58+uek33XQTK1asyA1169YFoHfv3qxYsYJvvvmG+fPns3v37tw8gwYNYvLkyXz22Wd89FH5O9YoU4MhIokikiwiyT7ZLXzXLXzpD4vIooD8l4nIcBHpICLtRORWYDRuPsUwjBjn+ONh6NA8o7F4sbs+/vji1XfBBRfw7rvv5vYQUlNT+f333+nduzcAp556KocddlhQGVXl448/5oILLgDgsssu46233gLg7bff5rLLLsute9GiRf7504h06dKFOnXq8OGHHwbFp6enc/DgQRo0aABAfHw87dp539dco0YNkpOT2bRpk+cyZU2RXIOUAt2AwOeNe31hJnA50AQ4KqTM3UBL3GT3T8AIVbX5C8OIAW68EVasKDhP06YwcCA0aQKbN0OHDnDvvS6EIzkZHn88fFr9+vXp3r0777//Pueeey5z5sxh6NChBY7Hb9++nbp16xIX5253SUlJuTflTZs20bx5cwDi4uKoU6cOaWlpNGzYsMA2jR07lnHjxjFgwIAg3QYNGkTLli059dRTOfvssxk2bBhVqrjn8qlTpzJrlrt11atXj8UhXa8dO3awdu1aTj755AJllyeF9jBEpJqIbBGRY0oqTFVTVFXChMt96ZeraquA/DNVtaOq1lLV2qrazYyFYVQs6tVzxuK339xrvXAn6xSBwGGpwOGossR/U//000+D4l988UUWLVpE9+7dmTJlCiNGjMhNCxySCjQWS5cupUuXLjRr1oyBAwfSuHHjsmlEMSi0h6GqWSKSBRS+YcMwjEOKSD2BQPzDUOPGwbPPwvjx0Ldv8WWee+653HTTTXz99dfs3buXrl27Fpi/fv367Ny5k4MHDxIXF8fGjRtp1qwZAM2aNWPDhg0kJSVx8OBBdu3alTukVBhjx47lgQceyO25+OncuTOdO3fmkksuoXXr1syYMaPAenr37s38+fNZv349PXr0YOjQoSQnJ3vSoazxOofxFHCniJT1EJZhGBUYv7F47TW47z73GjinURwSExPp27cvI0aM8NS7EBH69u3LG2+4/b4zZ87k3HPPBWDQoEHMnOlOn37jjTfo16+f5+Wmp512Gjt27OC7774D3EZH/2opgBUrVtCyZUvP7WrdujV33HEHEydO9FymrPFqMHoD5wKbRGSRz4FgboiifoZhVGC++soZCX+Pom9fd/3VVyWrd9iwYXz77bf5DEbv3r0ZMmQIixYtIikpKXcJ7sSJE3nsscdo06YNaWlpXHnllQBceeWVpKWl0aZNGx577DEeeeSRIukxduxYNmzYALjJ9UmTJtGuXTuSk5MZP358UO9i6tSpQctqwy3hHTVqFJ988knYtFjAq2uQlwtKV9UrSk2jUqJbt266bNmyIpVJSUkJWgZXEbE2xAaVuQ1r1qyhQ4cOZa9QMUlPT8+3cqqiEa02hPsuRSSiaxCvO71jziAYhmEYZUuR9mGISDcRuVBEavmua9m8hmEYxqGBp5u9iDTCOR7sjlst1Rb4BXgM2A/cEC0FDcMwjNjAaw9jKrAVaADsDYh/HTittJUyDMMwYg+vw0mnAqeq6o6QJWc/Ay1KXSvDMAwj5vDaw6gBHAgTfzhuSMowDMOo5Hg1GJ/gfD35URGpCtwOLApbwjAMo5QpD/fmAD/99BNnnnkmbdu25bjjjmPo0KFs3boVcO5BunfvTvv27Wnfvj3Tp0/PLTdhwgRq1qzJH3/8kRuXmJhIWlpa7n6Mxo0bB7k+P3DgANu2baNatWo899xzQXq0atWKbdu2AW5D4i233JKbNmXKFCZMmADAjz/+SJ8+fUhOTqZDhw6MHDnS60dcIF4Nxj+Aq0XkQyAed6jR90Av4M5S0cQwjEpHZXBvvn//fs466yyuueYa1q5dy9dff821117Ln3/+yZYtWxg+fDjPPfccP/zwA59++inPP/887777bm75hg0b8uijjwbV2aBBg1y/UqNGjQryM1W9enVef/11evTowezZsyN+FvHx8bz55pu5BiSQMWPG5Na5Zs0arr8+3/l3xcKTwVDV74HOwBfAQtz52q8Dx6rqz6WiiWEYlY7K4N783//+Nz179uScc87JjevTpw+dOnXimWee4fLLL+e4444DnHGYNGlS0I7xESNGMHfuXLZv3+65nbNnz+bRRx9l06ZNbNy4MWyeuLg4Ro4cydSpU/Olbd68maSkpNzrzp07e5ZdEJ73YajqFlW9R1XPVtUzVfVuVbWDjAzjEKdPn/xh2jSXdsIJee7NW7Z0r02bwq+/uvRt2/KXLYhA9+ZA1NybB7Jq1aqIDg5Xr16dL61bt26sXr069zoxMZERI0bwxBNPFNw4Hxs2bGDz5s10796doUOHMnfu3Ih5R48ezauvvsquXbuC4m+66Sb69evHGWecwdSpU9m5c6cn2YXh2WCISBMRuU9E3vCF+0SkaaloYRhGpaUyujcvKmPGjGHmzJmkp6cXmnfu3LkMHToUgIsuuqjAYanatWtz6aWX8uSTTwbFX3HFFaxZs4YhQ4aQkpJCjx49yMzMLFkj8GgwRGQAbgnthbh9GHuBocA6EbF9GIZxCJOSkj9ce61Lq1nTuTPfu9e5N9+7111ffrlLb9gwf9nCOPfcc1m0aFGx3JsDYd2bAxHdmx9zzDEsX748bN0dO3bMl7Z8+XKOOSb4+KC6desyfPhwnnnmmULbN3v2bGbMmEGrVq0YNGgQ3333HevWrYuY/8Ybb+Sll15iz549QfFNmzZlxIgRvP3228TFxbFq1apCZReG1x7Gk8CLQHtVvdQX2gMvAN76WYZhHHJUBvfmw4cP5/PPPw+ayP7kk09YtWoVo0ePZsaMGazwHTuYlpbG7bffzj/+8Y98etx88808//zzuYYrHD/99BMZGRls2rSJ1NRUUlNTufPOO3N1D0f9+vUZOnRo7kQ+wIIFC8jKygJgy5YtpKWl5RrJkuDVYLQCntb8rm2fwR2fahiGkY/K4N68Ro0azJ8/n6eeeoq2bdvSsWNHpk2bxuGHH06TJk2YNWsWV199Ne3bt+fEE09kxIgRQRPkfho2bMh5551X4NDQ7NmzOe+884LiBg8eXKDBALjllluCVkstXLiQTp060aVLFwYOHMjkyZNL5yQ/VS00AEuBwWHiBwOfeamjrEPXrl21qCxevLjIZWINa0NsUJnb8P3335etIiVk9+7d5a1CiYlWG8J9l8AyjXBf9eoaZBowVUTaAv/zxfUArgHuEJHjAgzQ1yU3Y4ZhGEas4dVgvOp7faiANHCebKuWSCPDMAwjJvFqMFpHVQvDMAwj5vF64t6v0VbEMAzDiG2KdOKeYRiGcehiBsMwDMPwhBkMwzAqHG+99RYiwg8//BAUHw335qmpqXTq1AmAlJQURIR58+blpp999tmk+Laoz58/n2OPPZYuXbrQsWNHnn/+ecC5OQ90YZ6cnMzOnTtJSUmhTp06JCcn0759e2699VZWrlyZm6d+/fq0bt2aXr160b9//9L6+IqNGQzDMKLLq69Cq1ZQpYp7ffXVwkoUyuzZsznppJPy+VmKhnvzUJKSknjwwQfzxWdlZTFy5EjmzZvHt99+yzfffEOfAG+KgS7MV6xYQd26dQG32XDFihV88803zJ8/n927d+fmGTRoEJMnT+azzz7jo48+KsInFB28+pKqIiJVAq4bi8hVItIreqoZhlHhefVVGDnSuadVda8jR5bIaGRkZPDpp5/y0ksv5TsbIxruzUPp0qULderU4cMPPwyKT09P5+DBg7m+qOLj42nXrp3ndtWoUYPk5ORcT7qxiNdlte8CC4AnRCQRWAbUAhJF5EpV/Ve0FDQMI4a58Ubw+VEKy//+B6GuMPbuhSuvhBdeCF8mORkefzxilW+//Tann346Rx99NA0aNGD58uUFOiAsjnvzhg0bRm4TMHbsWMaNG8eAAQNy4+rXr8+gQYNo2bIlp556KmeffTbDhg2jShX3rD116lRmzZoFQL169Vgc4lBrx44drF27lpNPPrlA2eWJ1yGpbsDHvvfnA7uBI4CrgVujoJdhGJWBSH6TSuBqe/bs2Vx00UVA4e6/o4X/pv7pp58Gxb/44ossWrSI7t27M2XKFEaMGJGbFjgkFWgsli5dSpcuXWjWrBkDBw4sHZ9PUcJrDyMR2Ol7fxrwX1XNEpGPcQ4IDcM4FCmgJwC4OYtfw2zjatnSmy/zELZv387HH3/MypUrERGys7MRESZPnhzxEKVA9+ZxcXFh3ZsnJSVFdG8eibFjx/LAAw/k9lz8dO7cmc6dO3PJJZfQunVrZsyYUWA9vXv3Zv78+axfv54ePXowdOhQkpOTPelQ1njtYfwG9BKRWsBAwD94Vx93NoZhGEZ+HnzQHYoRSM2aLr4YvPHGG1xyySX8+uuvpKamsmHDBlq3bs3SpUsjlimpe/NInHbaaezYsYPvvvsOcHMrKQFGcMWKFbRs6d2Zd+vWrbnjjjuYOHGi5zJljVeD8RjwCrAR2AR84os/GVgZBb0Mw6gMXHwxTJ/uehQi7nX6dBdfDCK5//YPS0XDvXlBjB07NvcAJlVl0qRJtGvXjuTkZMaPHx/Uu5g6dWrQstpwS3hHjRrFJ598EjYtJojkxjY0AF2B84DEgLizgF5e6yjLYO7NKy7WhtjA3JvHDhXNvTmquhxYHhL3boTshmEYRiXDs8EQkROAU3Gro4KGslR1TCnrZRiGYcQYngyGiNwKTALWAb/jzr3wU/AuF8MwDKNS4LWHcQMwRlWfjqYyhmEYRuzidZVUbeC9kgoTkZNF5B0R2SQiKiKXeyjTWUSWiMg+X7l7xOu6N8MwDKPU8GowZgOnl4K8RGAVrseyr7DMIlIbt+djK3C8r9xtwM2loIthGIZRBLwajA3AvSLyqojcLiI3BwavwlT1PVW9S1XfAHI8FLkYqAlcpqqrfOUmAjdbL8MwDl3K0r05wE8//cSZZ55J27ZtOe644xg6dChbt24FnHuQ7t270759e9q3b8/06dNzy02YMIGaNWvyxx9/5MYlJiaSlpaWux+jcePGQa7PDxw4wLZt26hWrRrPPfdckB6tWrVi27ZtgNuQeMstt+SmTZkyhQkTJgDw448/0qdPH5KTk+nQoQMjR44swqcbGa9zGFcBGcCJvhCI4jb2RYOewFJVDeyNfADcD7QC1gdmFpGRwEiARo0aBe269ELoTs2KiLUhNqjMbahTpw7p6eme64l77TXi770X2bgRTUoic/x4Dg4dWiLdXnnlFXr27MmMGTMYO3Zsbvzo0aMZMWIE//znP3N1zM7O5pZbbmHUqFFccMEF3HjjjTzzzDNcddVVvPDCCyQmJvLNN9/wxhtvcMstt+Rz5bF//37OOOMMHn74Yc444wzA+X9KTU0lPT2dYcOG8e9//5vk5GTS0tI477zzqFevHqeffjqZmZk0aNCAhx9+mPvuuy+3zurVq+fuTn/ooYdITExkzBi32DQzM5NXXnmF448/nlmzZnHxxReTnZ1Neno6qkpGRgbx8fHEx8fzn//8h+uvv54GDRqQmZlJZmYm6enpXHvttYwaNYqzzjoLgNWrV4f9zvbv31+032mkDRrRDjgDdHkheRYC/wyJa4EzUj0LKmsb9you1obYoFQ27s2apVqzpqpzbu5CzZouvpikp6dr06ZN9ccff9Sjjz46X/rixYv1rLPOyr3etWuXNmjQQLOyslRV9fPPP9fTTjtNVVVPO+00/fzzz1VVNSsrSxs0aKA5OTlB9b300kt6ySWXhNXl7rvv1nHjxgXFffTRR3rSSSepqur48eN1/Pjx2rJlS01LS1NV1Vq1agXlHz9+vE6ePDkornfv3vrll1/qUUcdpRs2bMjduNeyZUv9888/c+t56KGH9K677lJV1cmTJ+v48eNVVbVz5866bNmysDoHUtSNe0U+QElEEn0+pQzDMKBPn/xh2jSXduedzp15IHv3wg03uPfbtuUvWwjh3JsXRHHcmweyatWqiO7TV69enS+tW7durF69Ovc6MTGRESNG8MQTTxTaNoANGzawefNmunfvztChQ5k7d27EvKNHj+bVV19l165dQfE33XQT/fr144wzzmDq1Kns3LnTk+zC8GwwRGS0iPwG7AJ2i8ivInJtqWgRmS1Ao5C4RgFphmHEMhs3ho8PuSkXhVhwb15UxowZw8yZMz0N5c2dO5ehviG7wtpXu3ZtLr30Up588smg+CuuuII1a9YwZMgQUlJS6NGjB5klcCnvx+vGvbuAO4EpgN8BfG/gERGprapF89jlnS+AiSKSoKr7fXEDcJsHU6Mk0zCMolDQGHiLFpHdmwM0bFgkN+fl4d78mGOOYcmSJWHr7tixI8uXL8/1fguwfPlyjjnmmKB8devWZfjw4TzzTOGnQcyePZstW7bwqu9Uwt9//51169Zx7LHHhs1/4403ctxxx3HFFVcExTdt2pQRI0YwYsQIOnXqVGBPySteexijgJGqeq+qLvKFCcA1vuAJ33BWsogk+2S38F238KU/LCKLAor8G+c+fYaIdBKR84E7gMd8Y22GYcQylcC9+fDhw/n88895990813mffPIJq1atYvTo0cyYMYMVvlMH09LSuP322/nHP/6RT4+bb76Z559/noMHD0bU9aeffiIjI4NNmzaRmppKamoqd955Z67u4ahfvz5Dhw7NPaccYMGCBWRlZQGwZcsW0tLSco1kSfBqMI4AvgoT/3/kHzIqiG7AN75QA7jX996/fKAJcJQ/s6ruwvUomuKOhX0GeJTorcoyDKM0qQTuzWvUqMH8+fN56qmnaNu2LR07dmTatGkcfvjhNGnShFmzZnH1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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 9, + "source": [ + "# GMEM vs. L2\n", + "df = pd.read_csv(\"../results/bloom_filter_scope_hit_ratio_60.csv\")\n", + "df = df[df[\"Skipped\"] == \"No\"]\n", + "df[\"Filter Size [MB]\"] = df[\"NumBits\"] / 8 / 1000 / 1000\n", + "\n", + "dfs = {}\n", + "dfs[\"GMEM INSERT\"] = df.query('Scope == \"GMEM\" and Operation == \"INSERT\"')\n", + "dfs[\"GMEM CONTAINS\"] = df.query('Scope == \"GMEM\" and Operation == \"CONTAINS\"')\n", + "dfs[\"L2 INSERT\"] = df.query('Scope == \"L2\" and Operation == \"INSERT\"')\n", + "dfs[\"L2 CONTAINS\"] = df.query('Scope == \"L2\" and Operation == \"CONTAINS\"')\n", + "\n", + "styles = {\n", + " \"GMEM INSERT\" : style_('b', 'x', '-'),\n", + " \"GMEM CONTAINS\" : style_('b', 'x', '--'),\n", + " \"L2 INSERT\" : style_('r', 'o', '-'),\n", + " \"L2 CONTAINS\" : style_('r', 'o', '--')}\n", + " \n", + "query = 'KeyType == \"I32\" and\\\n", + " SlotType == \"I32\" and\\\n", + " NumHashes == 2 and\\\n", + " NumInputs == 50000000'\n", + "\n", + "print(\"INSERT on A100 (GMEM vs. L2)\")\n", + "plot_bench(filter_bench(dfs, query), \"Filter Size [MB]\", styles=styles)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "INSERT on A100 (GMEM vs. L2)\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n \n \n \n \n 2021-08-23T07:05:09.899832\n image/svg+xml\n \n \n Matplotlib v3.4.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + } + ], + "metadata": { + "interpreter": { + "hash": "fab55a90acef312968e5bff70ae91c3267a5b896b51d076af77c4418fdb5d582" + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.9.5 64-bit ('base': conda)" + }, + "language_info": { + "name": "python", + "version": "3.9.5", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/benchmarks/bloom_filter/bloom_filter_bench.cu b/benchmarks/bloom_filter/bloom_filter_bench.cu new file mode 100644 index 000000000..e8da3691e --- /dev/null +++ b/benchmarks/bloom_filter/bloom_filter_bench.cu @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include + +#include +#include +#include + +#include + +#include + +namespace cg = cooperative_groups; + +static constexpr nvbench::int64_t block_size = 256; +static constexpr nvbench::int64_t stride = 4; + +enum class FilterOperation { INSERT, CONTAINS }; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + FilterOperation, + [](FilterOperation op) { + switch (op) { + case FilterOperation::INSERT: return "INSERT"; + case FilterOperation::CONTAINS: return "CONTAINS"; + default: return "ERROR"; + } + }, + [](FilterOperation op) { + switch (op) { + case FilterOperation::INSERT: return "FilterOperation::INSERT"; + case FilterOperation::CONTAINS: return "FilterOperation::CONTAINS"; + default: return "ERROR"; + } + }) + +enum class FilterScope { GMEM, L2 }; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + FilterScope, + [](FilterScope s) { + switch (s) { + case FilterScope::GMEM: return "GMEM"; + case FilterScope::L2: return "L2"; + default: return "ERROR"; + } + }, + [](FilterScope s) { + switch (s) { + case FilterScope::GMEM: return "FilterScope::GMEM"; + case FilterScope::L2: return "FilterScope::L2"; + default: return "ERROR"; + } + }) + +enum class DataScope { GMEM, REGS }; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + DataScope, + [](DataScope s) { + switch (s) { + case DataScope::GMEM: return "GMEM"; + case DataScope::REGS: return "REGS"; + default: return "ERROR"; + } + }, + [](DataScope s) { + switch (s) { + case DataScope::GMEM: return "DataScope::GMEM"; + case DataScope::REGS: return "DataScope::REGS"; + default: return "ERROR"; + } + }) + +template +void add_size_summary(nvbench::state& state) +{ + using filter_type = + cuco::bloom_filter, Slot>; + + auto const num_keys = state.get_int64("NumInputs"); + auto const num_bits = state.get_int64("NumBits"); + auto const num_hashes = state.get_int64("NumHashes"); + + filter_type filter(num_bits, num_hashes); + + auto& summ = state.add_summary("nv/filter/size/mb"); + summ.set_string("hint", "FilterMB"); + summ.set_string("short_name", "FilterMB"); + summ.set_string("description", "Size of the Bloom filter in MB."); + summ.set_float64("value", filter.get_num_slots() * sizeof(Slot) / 1000 / 1000); +} + +template +void add_fpr_summary(nvbench::state& state) +{ + using filter_type = + cuco::bloom_filter, Slot>; + + auto const num_keys = state.get_int64("NumInputs"); + auto const num_bits = state.get_int64("NumBits"); + auto const num_hashes = state.get_int64("NumHashes"); + + thrust::device_vector keys(num_keys * 2); + thrust::sequence(thrust::device, keys.begin(), keys.end(), 1); + thrust::device_vector result(num_keys, false); + + auto tp_begin = keys.begin(); + auto tp_end = tp_begin + num_keys; + auto tn_begin = tp_end; + auto tn_end = keys.end(); + + filter_type filter(num_bits, num_hashes); + filter.insert(tp_begin, tp_end); + filter.contains(tn_begin, tn_end, result.begin()); + + float fp = thrust::count(thrust::device, result.begin(), result.end(), true); + + auto& summ = state.add_summary("nv/filter/fpr"); + summ.set_string("hint", "FPR"); + summ.set_string("short_name", "FPR"); + summ.set_string("description", "False-positive rate of the bloom filter."); + summ.set_float64("value", fp / num_keys); +} + +template +__global__ void __launch_bounds__(BLOCK_SIZE) + insert_kernel(Filter mutable_view, InputIt first, InputIt last) +{ + std::size_t tid = block_size * blockIdx.x + threadIdx.x; + auto it = first + tid; + + while (it < last) { + mutable_view.insert(*it); + it += gridDim.x * BLOCK_SIZE; + } +} + +template +__global__ void __launch_bounds__(BLOCK_SIZE) + contains_kernel(Filter view, InputIt first, InputIt last, OutputIt results) +{ + std::size_t tid = block_size * blockIdx.x + threadIdx.x; + + while ((first + tid) < last) { + *(results + tid) = view.contains(*(first + tid)); + tid += gridDim.x * BLOCK_SIZE; + } +} + +template +__global__ void __launch_bounds__(BLOCK_SIZE) + insert_kernel(Filter mutable_view, nvbench::int64_t num_keys) +{ + using key_type = typename Filter::key_type; + + auto g = cg::this_grid(); + + for (key_type key = g.thread_rank(); key < num_keys; key += g.size()) { + mutable_view.insert(key); + } +} + +template +__global__ void __launch_bounds__(BLOCK_SIZE) + contains_kernel(Filter view, nvbench::int64_t num_keys) +{ + using key_type = typename Filter::key_type; + + auto g = cg::this_grid(); + + for (key_type key = g.thread_rank(); key < num_keys; key += g.size()) { + volatile bool contains = view.contains(key); + } +} + +template +void nvbench_cuco_bloom_filter(nvbench::state& state, + nvbench::type_list, + nvbench::enum_type, + nvbench::enum_type>) +{ + auto num_keys = state.get_int64("NumInputs"); + auto num_bits = state.get_int64("NumBits"); + auto num_hashes = state.get_int64("NumHashes"); + + [[maybe_unused]] thrust::device_vector keys; + [[maybe_unused]] thrust::device_vector results; + + if constexpr (DScope == DataScope::GMEM) { + keys.resize(num_keys); + thrust::sequence(thrust::device, keys.begin(), keys.end(), 1); + + if constexpr (Op == FilterOperation::CONTAINS) { results.resize(num_keys); } + } + + using filter_type = + cuco::bloom_filter, Slot>; + + filter_type filter(num_bits, num_hashes); + auto mutable_view = filter.get_device_mutable_view(); + auto view = filter.get_device_view(); + std::size_t const grid_size = SDIV(num_keys, stride * block_size); + + state.add_element_count(num_keys); + state.add_global_memory_writes(num_keys); + + add_fpr_summary(state); + add_size_summary(state); + + if constexpr (Op == FilterOperation::CONTAINS) { + insert_kernel<<>>(mutable_view, num_keys); + } + + cudaStream_t stream; + cudaStreamCreate(&stream); + + if constexpr (FScope == FilterScope::L2) + cuco::register_l2_persistence( + stream, filter.get_slots(), filter.get_slots() + filter.get_num_slots()); + + state.set_cuda_stream(nvbench::make_cuda_stream_view(stream)); + + state.exec([&](nvbench::launch& launch) { + if constexpr (Op == FilterOperation::INSERT) { + filter.initialize(launch.get_stream()); + if constexpr (DScope == DataScope::GMEM) { + insert_kernel<<>>( + mutable_view, keys.begin(), keys.end()); + } + if constexpr (DScope == DataScope::REGS) { + insert_kernel + <<>>(mutable_view, num_keys); + } + } + if constexpr (Op == FilterOperation::CONTAINS) { + if constexpr (DScope == DataScope::GMEM) { + contains_kernel<<>>( + view, keys.begin(), keys.end(), results.begin()); + } + if constexpr (DScope == DataScope::REGS) { + contains_kernel + <<>>(view, num_keys); + } + } + }); + + if constexpr (FScope == FilterScope::L2) cuco::unregister_l2_persistence(stream); +} + +using key_type_range = nvbench::type_list; +using slot_type_range = nvbench::type_list; +using op_range = nvbench::enum_type_list; +using filter_scope_range = nvbench::enum_type_list; +using data_scope_range = nvbench::enum_type_list; + +// A100 L2 = 40MB ~ 330'000'000 bits +// smem = 48kb ~ 390'0000 bits +// 1GB ~ 8'500'000'000 bits +// 4GB ~ 34'000'000'000 bits + +NVBENCH_BENCH_TYPES(nvbench_cuco_bloom_filter, + NVBENCH_TYPE_AXES(nvbench::type_list, + nvbench::type_list, + op_range, + filter_scope_range, + data_scope_range)) + .set_name("cuco_bloom_filter_l2") + .set_type_axes_names({"KeyType", "SlotType", "FilterOperation", "FilterScope", "DataScope"}) + .set_max_noise(3) + .add_int64_axis("NumInputs", {10'000'000, 100'000'000}) + .add_int64_axis("NumBits", {300'000'000}) + .add_int64_axis("NumHashes", {2}); + +NVBENCH_BENCH_TYPES(nvbench_cuco_bloom_filter, + NVBENCH_TYPE_AXES(key_type_range, + slot_type_range, + op_range, + nvbench::enum_type_list, + data_scope_range)) + .set_name("cuco_bloom_filter_gmem") + .set_type_axes_names({"KeyType", "SlotType", "FilterOperation", "FilterScope", "DataScope"}) + .set_max_noise(3) + .add_int64_axis("NumInputs", {1'000'000'000, 100'000'000}) + .add_int64_axis("NumBits", {8'500'000'000, 34'000'000'000}) + .add_int64_axis("NumHashes", {6}); \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index db40f3cf2..521990b57 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -37,6 +37,8 @@ ConfigureExample(STATIC_MAP_HOST_BULK_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/stati ConfigureExample(STATIC_MAP_DEVICE_SIDE_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_map/device_view_example.cu") ConfigureExample(STATIC_MAP_CUSTOM_TYPE_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_map/custom_type_example.cu") ConfigureExample(STATIC_MULTIMAP_HOST_BULK_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_multimap/host_bulk_example.cu") +ConfigureExample(BLOOM_FILTER_HOST_BULK_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/bloom_filter/host_bulk_example.cu") +ConfigureExample(BLOOM_FILTER_L2_RESIDENCY_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/bloom_filter/l2_residency_example.cu") foreach(arch IN LISTS CMAKE_CUDA_ARCHITECTURES) if("${arch}" MATCHES "^6") diff --git a/examples/bloom_filter/host_bulk_example.cu b/examples/bloom_filter/host_bulk_example.cu new file mode 100644 index 000000000..8bec29ce6 --- /dev/null +++ b/examples/bloom_filter/host_bulk_example.cu @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2021, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +#include + +int main(void) +{ + // Generate 10'000 keys and insert the first 5'000 into the filter. + int const num_keys = 10'000; + int const num_tp = num_keys * 0.5; + int const num_tn = num_keys - num_tp; + + // Spawn a filter with 1'000'000 bits and 6-bit patterns for each key. + cuco::bloom_filter filter{num_tp * 10, 6}; + + thrust::device_vector keys(num_keys); + thrust::sequence(keys.begin(), keys.end(), 1); + + auto tp_begin = keys.begin(); + auto tp_end = tp_begin + num_tp; + auto tn_begin = tp_end; + auto tn_end = keys.end(); + + // Insert the first half of the keys. + filter.insert(tp_begin, tp_end); + + thrust::device_vector tp_result(num_tp, false); + thrust::device_vector tn_result(num_keys - num_tp, false); + + // Query the filter for the previously inserted keys. + // This should result in a true-positive rate of TPR=1. + filter.contains(tp_begin, tp_end, tp_result.begin()); + + // Query the filter for the keys that are not present in the filter. + // Since bloom filters are probalistic data structures, the filter + // exhibits a false-positive rate FPR>0 depending on the number of bits in + // the filter and the number of hashes used per key. + filter.contains(tn_begin, tn_end, tn_result.begin()); + + float tp_rate = + float(thrust::count(thrust::device, tp_result.begin(), tp_result.end(), true)) / float(num_tp); + float fp_rate = + float(thrust::count(thrust::device, tn_result.begin(), tn_result.end(), true)) / float(num_tn); + + std::cout << "TPR=" << tp_rate << " FPR=" << fp_rate << std::endl; + + return 0; +} diff --git a/examples/bloom_filter/l2_residency_example.cu b/examples/bloom_filter/l2_residency_example.cu new file mode 100644 index 000000000..528f061c1 --- /dev/null +++ b/examples/bloom_filter/l2_residency_example.cu @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include + +int main(void) +{ + int const num_keys = 10'000'000; + int const num_bits = 300'000'000; // 37 MB; fits in the L2 of an A100 + int const num_hashes = 2; // sufficient for small filters + + // Spawn a 37MB filter and 2-bit patterns for each key. + cuco::bloom_filter filter{num_bits, num_hashes}; + + // Create a CUDA stream in which this operation is performed. + cudaStream_t stream; + cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking); + + thrust::device_vector keys(num_keys); + thrust::sequence(keys.begin(), keys.end(), 1); + thrust::device_vector contains(num_keys); + + // Insert all keys and subsequently query them against the filter; measure runtime + cudaEvent_t gmem_start, gmem_stop; + cudaEventCreate(&gmem_start); + cudaEventCreate(&gmem_stop); + + cudaEventRecord(gmem_start, stream); + filter.insert(keys.begin(), keys.end(), stream); + filter.contains(keys.begin(), keys.end(), contains.begin(), stream); + cudaEventRecord(gmem_stop, stream); + cudaStreamSynchronize(stream); + + float gmem_delta; + cudaEventElapsedTime(&gmem_delta, gmem_start, gmem_stop); + std::cout << "Insert+query filter in global memory: " << gmem_delta << "ms\n"; + + // Re-initialize the filter, i.e., set all bits to zero + filter.initialize(stream); + cudaStreamSynchronize(stream); + + // Make the filter persistent in the GPU's L2 cache + cuco::register_l2_persistence( + stream, filter.get_slots(), filter.get_slots() + filter.get_num_slots()); + + // Insert all keys and subsequently query them against the filter; measure runtime + cudaEvent_t l2_start, l2_stop; + cudaEventCreate(&l2_start); + cudaEventCreate(&l2_stop); + + cudaEventRecord(l2_start, stream); + filter.insert(keys.begin(), keys.end(), stream); + filter.contains(keys.begin(), keys.end(), contains.begin(), stream); + cudaEventRecord(l2_stop, stream); + cudaStreamSynchronize(stream); + + float l2_delta; + cudaEventElapsedTime(&l2_delta, l2_start, l2_stop); + std::cout << "Insert+query filter in L2: " << l2_delta << "ms\n"; + + // Flush the L2 so it can be used for other tasks + cuco::unregister_l2_persistence(stream); + + return 0; +} diff --git a/include/cuco/bloom_filter.cuh b/include/cuco/bloom_filter.cuh new file mode 100644 index 000000000..da576292b --- /dev/null +++ b/include/cuco/bloom_filter.cuh @@ -0,0 +1,636 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#if defined(CUCO_HAS_CUDA_BARRIER) +#include +#endif + +#if defined(CUCO_HAS_CUDA_ANNOTATED_PTR) +#include +#endif + +#include +#include + +namespace cuco { + +/** + * @brief A GPU-accelerated, filter for approximate set membership queries. + * + * Allows constant time concurrent inserts or concurrent find operations from threads in device + * code. + * + * Current limitations: + * - Does not support erasing keys + * - Capacity is fixed and will not grow automatically + * + * The `bloom_filter` supports two types of operations: + * - Host-side "bulk" operations + * - Device-side "singular" operations + * + * The host-side bulk operations include `insert` and `contains`. These + * APIs should be used when there are a large number of keys to insert or lookup + * in the map. For example, given a range of keys specified by device-accessible + * iterators, the bulk `insert` function will insert all keys into the map. + * + * The singular device-side operations allow individual threads to perform + * independent insert or contains operations from device code. These + * operations are accessed through non-owning, trivially copyable "view" types: + * `device_view` and `mutable_device_view`. The `device_view` class is an + * immutable view that allows only non-modifying operations such as `contains`. + * The `mutable_device_view` class only allows `insert` operations. + * The two types are separate to prevent erroneous concurrent 'insert'/'contains' + * operations. + * + * Example: + * \code{.cpp} + * // TODO + * \endcode + * + * + * @tparam Key Arithmetic type used for key + * @tparam Scope The scope in which insert/find operations will be performed by + * individual threads. + * @tparam Allocator Type of allocator used for device storage + * @tparam Slot Type of bloom filter partition + */ +template , + typename Slot = std::uint64_t> +class bloom_filter { + public: + using key_type = Key; ///< Key type + using slot_type = Slot; ///< Filter slot type + using atomic_slot_type = cuda::atomic; ///< Filter slot type + using iterator = atomic_slot_type*; ///< Filter slot iterator type + using const_iterator = atomic_slot_type const*; ///< Filter slot const iterator type + using allocator_type = Allocator; ///< Allocator type + using slot_allocator_type = typename std::allocator_traits::rebind_alloc< + atomic_slot_type>; ///< Type of the allocator to (de)allocate slots + +#if !defined(CUCO_HAS_INDEPENDENT_THREADS) + static_assert(atomic_slot_type::is_always_lock_free, + "A slot type larger than 8B is supported for only sm_70 and up."); +#endif + + bloom_filter(bloom_filter const&) = delete; + bloom_filter(bloom_filter&&) = delete; + bloom_filter& operator=(bloom_filter const&) = delete; + bloom_filter& operator=(bloom_filter&&) = delete; + + /** + * @brief Construct a fixed-size filter with the specified number of bits. + * + * @param num_bits The total number of bits in the filter + * @param num_hashes The number of hashes to be applied to a key + * @param alloc Allocator used for allocating device storage + * @param stream The CUDA stream this operation is executed in + */ + bloom_filter(std::size_t num_bits, + std::size_t num_hashes, + Allocator const& alloc = Allocator{}, + cudaStream_t stream = 0); + + /** + * @brief Destroys the filter and frees its contents. + * + */ + ~bloom_filter(); + + /** + * @brief (Re-) initializes the filter, i.e., set all bits to 0. + * + * @param stream The CUDA stream this operation is executed in + */ + void initialize(cudaStream_t stream = 0); + + /** + * @brief Inserts all keys in the range `[first, last)`. + * + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the filter's `key_type` + * @tparam Hash1 Unary callable type + * @tparam Hash2 Unary callable type + * @tparam Hash3 Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param stream The CUDA stream this operation is executed in + * @param hash1 First hash function; used to determine a filter slot + * @param hash2 Second hash function; used to generate a signature of the key + * @param hash3 Third hash function; used to generate a signature of the key + */ + template , + typename Hash2 = Hash1, + typename Hash3 = Hash2> + void insert(InputIt first, + InputIt last, + cudaStream_t stream = 0, + Hash1 hash1 = Hash1{}, + Hash2 hash2 = Hash2{1}, + Hash3 hash3 = Hash3{2}); + + /** + * @brief Indicates whether the keys in the range `[first, last)` are + * contained in the filter. + * + * Writes a `bool` to `(output + i)` indicating if the signature of key + * `*(first + i)` is present in the filter. + * + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the filter's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to `bool` + * @tparam Hash1 Unary callable type + * @tparam Hash2 Unary callable type + * @tparam Hash3 Unary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param stream The CUDA stream this operation is executed in + * @param hash1 First hash function; used to determine a filter slot + * @param hash2 Second hash function; used to generate a signature of the key + * @param hash3 Third hash function; used to generate a signature of the key + */ + template , + typename Hash2 = Hash1, + typename Hash3 = Hash2> + void contains(InputIt first, + InputIt last, + OutputIt output_begin, + cudaStream_t stream = 0, + Hash1 hash1 = Hash1{}, + Hash2 hash2 = Hash2{1}, + Hash3 hash3 = Hash3{2}); + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + iterator get_slots() noexcept { return slots_; } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + const_iterator get_slots() const noexcept { return slots_; } + + /** + * @brief Gets the total number of bits in the filter (rounded up to the + * next multiple of block size). + * + * @return The total number of bits in the filter. + */ + std::size_t get_num_bits() const noexcept { return num_bits_; } + + /** + * @brief Gets the total number of slots in the filter. + * + * @return The total number of slots in the filter. + */ + std::size_t get_num_slots() const noexcept { return num_slots_; } + + /** + * @brief Gets the number of hashes to apply to a key. + * + * @return The number of hashes to apply to a key. + */ + std::size_t get_num_hashes() const noexcept { return num_hashes_; } + + private: + class device_view_base { + protected: + // Import member type definitions from `bloom_filter` + using key_type = Key; ///< Key type + using slot_type = slot_type; ///< Filter slot type + using atomic_slot_type = atomic_slot_type; ///< Filter slot type + using iterator = atomic_slot_type*; ///< Filter slot iterator type + using const_iterator = atomic_slot_type const*; ///< Filter slot const iterator type + + private: + atomic_slot_type* slots_{}; ///< Pointer to flat slots storage + std::size_t num_bits_{}; ///< Total number of bits + std::size_t num_slots_{}; ///< Total number of slots + std::size_t num_hashes_{}; ///< Number of hashes to apply + + protected: + __host__ __device__ device_view_base(atomic_slot_type* slots, + std::size_t num_bits, + std::size_t num_hashes) noexcept + : slots_{slots}, + num_bits_{SDIV(num_bits, detail::type_bits()) * detail::type_bits()}, + num_slots_{SDIV(num_bits, detail::type_bits())}, + num_hashes_{num_hashes} + { + } + + /** + * @brief Returns the slot for a given key `k` + * + * @tparam Hash Unary callable type + * @param k The key to get the slot for + * @param hash The unary callable used to hash the key + * @return Pointer to the slot for `k` + */ + template + __device__ iterator key_slot(Key const& k, Hash hash) noexcept + { + return &slots_[hash(k) % num_slots_]; + } + + /** + * @brief Returns the slot for a given key `k` + * + * @tparam Hash Unary callable type + * @param k The key to get the slot for + * @param hash The unary callable used to hash the key + * @return Pointer to the slot for `k` + */ + template + __device__ const_iterator key_slot(Key const& k, Hash hash) const noexcept + { + return &slots_[hash(k) % num_slots_]; + } + + /** + * @brief Returns the bit pattern for a given key `k` + * + * @tparam Hash1 Unary callable type + * @tparam Hash2 Unary callable type + * @param k The key to calculate the pattern for + * @param hash1 First hash function; used to generate a signature of the key + * @param hash2 Second hash function; used to generate a signature of the key + * @return Bit pattern for key `k` + */ + template + __device__ slot_type key_pattern(Key const& k, Hash1 hash1, Hash2 hash2) const noexcept; + + /** + * @brief Initializes the given array of slots using the threads in the group `g`. + * + * @note This function synchronizes the group `g`. + * + * @tparam CG The type of the cooperative thread group + * @param g The cooperative thread group used to initialize the slots + * @param slots Pointer to the array of slots to initialize + * @param num_slots Number of slots to initialize + */ + template + __device__ static void initialize_slots(CG g, atomic_slot_type* slots, std::size_t num_bits) + { + auto num_slots = SDIV(num_bits, detail::type_bits()); + auto tid = g.thread_rank(); + while (tid < num_slots) { + new (&slots[tid]) atomic_slot_type{0}; + tid += g.size(); + } + g.sync(); + } + + public: + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __host__ __device__ iterator get_slots() noexcept { return slots_; } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __host__ __device__ const_iterator get_slots() const noexcept { return slots_; } + + /** + * @brief Gets the total number of bits in the filter (rounded up to the + * next multiple of block size). + * + * @return The total number of bits in the filter. + */ + __host__ __device__ std::size_t get_num_bits() const noexcept { return num_bits_; } + + /** + * @brief Gets the total number of slots in the filter. + * + * @return The total number of slots in the filter. + */ + __host__ __device__ std::size_t get_num_slots() const noexcept { return num_slots_; } + + /** + * @brief Gets the number of hashes to apply to a key. + * + * @return The number of hashes to apply to a key. + */ + __host__ __device__ std::size_t get_num_hashes() const noexcept { return num_hashes_; } + + /** + * @brief Returns iterator to the first slot. + * + * @note Unlike `std::map::begin()`, the `begin_slot()` iterator does _not_ point to the first + * occupied slot. Instead, it refers to the first slot in the array of contiguous slot storage. + * Iterating from `begin_slot()` to `end_slot()` will iterate over all slots. + * + * There is no `begin()` iterator to avoid confusion. + * + * @return Iterator to the first slot + */ + __device__ iterator begin_slot() noexcept { return slots_; } + + /** + * @brief Returns iterator to the first slot. + * + * @note Unlike `std::map::begin()`, the `begin_slot()` iterator does _not_ point to the first + * occupied slot. Instead, it refers to the first slot in the array of contiguous slot storage. + * Iterating from `begin_slot()` to `end_slot()` will iterate over all slots. + * + * There is no `begin()` iterator to avoid confusion. + * + * @return Iterator to the first slot + */ + __device__ const_iterator begin_slot() const noexcept { return slots_; } + + /** + * @brief Returns a const_iterator to one past the last slot. + * + * @return A const_iterator to one past the last slot + */ + __host__ __device__ const_iterator end_slot() const noexcept { return slots_ + num_slots_; } + + /** + * @brief Returns an iterator to one past the last slot. + * + * @return An iterator to one past the last slot + */ + __host__ __device__ iterator end_slot() noexcept { return slots_ + num_slots_; } + }; + + public: + /** + * @brief Mutable, non-owning view-type that may be used in device code to + * perform singular inserts into the filter. + * + * `device_mutable_view` is trivially-copyable and is intended to be passed by + * value. + * + * Example: + * \code{.cpp} + * cuco::static_map m{100'000, -6}; + * + * // Inserts a sequence of keys {0, 1, 2, 3} + * thrust::for_each(thrust::make_counting_iterator(0), + * thrust::make_counting_iterator(50'000), + * [filter = bf.get_mutable_device_view()] + * __device__ (auto i) mutable { + * filter.insert(i); + * }); + * \endcode + */ + class device_mutable_view : public device_view_base { + public: + // Import member type definitions from `bloom_filter` + using key_type = Key; ///< Key type + using slot_type = slot_type; ///< Filter slot type + using atomic_slot_type = atomic_slot_type; ///< Filter slot type + using iterator = atomic_slot_type*; ///< Filter slot iterator type + using const_iterator = atomic_slot_type const*; ///< Filter slot const iterator type + + /** + * @brief Construct a mutable view of the array pointed to by `slots`. + * + * @param slots Pointer to beginning of the initialized slots array + * @param num_bits The total number of bits in the filter + * @param num_hashes The number of hashes to be applied to a key + */ + __host__ __device__ device_mutable_view(atomic_slot_type* slots, + std::size_t num_bits, + std::size_t num_hashes) noexcept + : device_view_base{slots, num_bits, num_hashes} + { + } + + public: + /** + * @brief Construct a mutable view of the array pointed to by `slots` and + * initializes the slot array. + * + * @tparam CG Type of the cooperative group this operation is executed with + * @param g Cooperative group this operation is executed with + * @param slots Pointer to beginning of the array used for slot storage + * @param num_bits The total number of bits in the filter + * @param num_hashes The number of hashes to be applied to a key + * @return A device_mutable_view object based on the given parameters + */ + template + __device__ static device_mutable_view make_from_uninitialized_slots( + CG g, void* const slots, std::size_t num_bits, std::size_t num_hashes) noexcept + { + device_view_base::initialize_slots(g, reinterpret_cast(slots), num_bits); + return device_mutable_view{reinterpret_cast(slots), num_bits, num_hashes}; + } + + /** + * @brief Inserts the specified key into the filter. + * + * Returns a `bool` denoting whether the key's signature was not already + * present in the slot. + * + * @tparam Hash1 Unary callable type + * @tparam Hash2 Unary callable type + * @tparam Hash3 Unary callable type + * @param key The key to insert + * @param hash1 First hash function; used to determine a filter slot + * @param hash2 Second hash function; used to generate a signature of the key + * @param hash3 Third hash function; used to generate a signature of the key + * @return `true` if the pattern was not already in the filter, + * `false` otherwise. + */ + template , + typename Hash2 = Hash1, + typename Hash3 = Hash2> + __device__ bool insert(key_type const& key, + Hash1 hash1 = Hash1{}, + Hash2 hash2 = Hash2{1}, + Hash3 hash3 = Hash3{2}) noexcept; + }; // class device mutable view + + /** + * @brief Non-owning view-type that may be used in device code to + * perform singular find and contains operations for the filter. + * + * `device_view` is trivially-copyable and is intended to be passed by + * value. + * + */ + class device_view : public device_view_base { + public: + // Import member type definitions from `bloom_filter` + using key_type = Key; ///< Key type + using slot_type = slot_type; ///< Filter slot type + using atomic_slot_type = atomic_slot_type; ///< Filter slot type + using iterator = atomic_slot_type*; ///< Filter slot iterator type + using const_iterator = atomic_slot_type const*; ///< Filter slot const iterator type + + /** + * @brief Construct a mutable view of the array pointed to by `slots`. + * + * @param slots Pointer to beginning of the initialized slots array + * @param num_bits The total number of bits in the filter + * @param num_hashes The number of hashes to be applied to a key + */ + __host__ __device__ device_view(atomic_slot_type* slots, + std::size_t num_bits, + std::size_t num_hashes) noexcept + : device_view_base{slots, num_bits, num_hashes} + { + } + + /** + * @brief Construct a `device_view` from a `device_mutable_view` object + * + * @param mutable_filter object of type `device_mutable_view` + */ + __host__ __device__ explicit device_view(device_mutable_view mutable_filter) + : device_view_base{mutable_filter.get_slots(), + mutable_filter.get_num_bits(), + mutable_filter.get_num_hashes()} + { + } + + /** + * @brief Makes a copy of given `device_view` using non-owned memory. + * + * This function is intended to be used to create shared memory copies of + * small static filters, although global memory can be used as well. + * + * Example: + * @code{.cpp} + * //TODO + * @endcode + * + * @tparam CG The type of the cooperative thread group + * @param g The cooperative thread group used to copy the slots + * @param source_device_view `device_view` to copy from + * @param memory_to_use Array large enough to support `num_slots` slots. + * Object does not take the ownership of the memory + * @return Copy of passed `device_view` + */ + template + __device__ static device_view make_copy(CG g, + void* const memory_to_use, + device_view source_device_view) noexcept + { + atomic_slot_type* const dest_slots = reinterpret_cast(memory_to_use); + atomic_slot_type const* const src_slots = source_device_view.get_slots(); + +#if defined(CUDA_HAS_CUDA_BARRIER) + __shared__ cuda::barrier barrier; + if (g.thread_rank() == 0) { init(&barrier, g.size()); } + g.sync(); + + cuda::memcpy_async(g, + dest_slots, + src_slots, + sizeof(atomic_slot_type) * source_device_view.get_num_slots(), + barrier); + + barrier.arrive_and_wait(); +#else + for (std::size_t i = g.thread_rank(); i < source_device_view.get_num_slots(); i += g.size()) { + new (&dest_slots[i]) atomic_slot_type{src_slots[i].load(cuda::memory_order_relaxed)}; + } + g.sync(); +#endif + + return device_view( + dest_slots, source_device_view.get_num_bits(), source_device_view.get_num_hashes()); + } + + /** + * @brief Indicates whether the key's signature is present in the filter. + * + * If the siganture of the key `k` was inserted into the filter, `contains` + * returns `true`. Otherwise, it returns `false`. + * + * @tparam Hash1 Unary callable type + * @tparam Hash2 Unary callable type + * @tparam Hash3 Unary callable type + * @param k The key to search for + * @param hash1 First hash function; used to determine a filter slot + * @param hash2 Second hash function; used to generate a signature of the key + * @param hash3 Third hash function; used to generate a signature of the key + * @return A boolean indicating whether the key's signature is present in + * the filter. + */ + template , + typename Hash2 = Hash1, + typename Hash3 = Hash2> + __device__ bool contains(Key const& k, + Hash1 hash1 = Hash1{}, + Hash2 hash2 = Hash2{1}, + Hash3 hash3 = Hash3{2}) const noexcept; + }; + + /** + * @brief Constructs a device_view object based on the members of the `bloom_filter` object. + * + * @return A device_view object based on the members of the `bloom_filter` object + */ + device_view get_device_view() const noexcept + { + return device_view(slots_, num_bits_, num_hashes_); + } + + /** + * @brief Constructs a device_mutable_view object based on the members of the `bloom_filter` + * object + * + * @return A device_mutable_view object based on the members of the `bloom_filter` object + */ + device_mutable_view get_device_mutable_view() const noexcept + { + return device_mutable_view(slots_, num_bits_, num_hashes_); + } + + private: + atomic_slot_type* slots_{nullptr}; ///< Pointer to flat slot storage + std::size_t num_bits_{}; ///< Total number of bits in the filter + std::size_t num_slots_{}; ///< Total number of slots in the filter + std::size_t num_hashes_{}; ///< Number of hash functions to apply (k) + slot_allocator_type slot_allocator_{}; ///< Allocator used to allocate slots +}; +} // namespace cuco + +#include diff --git a/include/cuco/detail/__config b/include/cuco/detail/__config index 197354a4f..d7764a09f 100644 --- a/include/cuco/detail/__config +++ b/include/cuco/detail/__config @@ -27,4 +27,9 @@ #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 700) #define CUCO_HAS_INDEPENDENT_THREADS +#endif + +#if defined(CUDART_VERSION) && (CUDART_VERSION >= 11500) && defined(__CUDA_ARCH__) && \ +(__CUDA_ARCH__ >= 700) +#define CUCO_HAS_CUDA_ANNOTATED_PTR #endif \ No newline at end of file diff --git a/include/cuco/detail/bloom_filter.inl b/include/cuco/detail/bloom_filter.inl new file mode 100644 index 000000000..e1bfa45b4 --- /dev/null +++ b/include/cuco/detail/bloom_filter.inl @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include + +namespace cuco { + +template +bloom_filter::bloom_filter(std::size_t num_bits, + std::size_t num_hashes, + Allocator const& alloc, + cudaStream_t stream) + : num_bits_{SDIV(std::max(std::size_t{1}, num_bits), detail::type_bits()) * + detail::type_bits()}, + num_slots_{SDIV(std::max(std::size_t{1}, num_bits), detail::type_bits())}, + num_hashes_{std::clamp(num_hashes, std::size_t{1}, detail::type_bits())}, + slot_allocator_{alloc} +{ + slots_ = std::allocator_traits::allocate(slot_allocator_, num_slots_); + + initialize(stream); +} + +template +bloom_filter::~bloom_filter() +{ + std::allocator_traits::deallocate(slot_allocator_, slots_, num_slots_); +} + +template +void bloom_filter::initialize(cudaStream_t stream) +{ + std::size_t constexpr block_size = 256; + std::size_t constexpr stride = 4; + std::size_t const grid_size = SDIV(num_slots_, stride * block_size); + + detail::initialize<<>>(slots_, num_slots_); +} + +template +template +void bloom_filter::insert( + InputIt first, InputIt last, cudaStream_t stream, Hash1 hash1, Hash2 hash2, Hash3 hash3) +{ + auto num_keys = std::distance(first, last); + if (num_keys == 0) { return; } + + std::size_t constexpr block_size = 256; + std::size_t constexpr stride = 4; + std::size_t const grid_size = SDIV(num_keys, stride * block_size); + detail::insert<<>>( + first, last, get_device_mutable_view(), hash1, hash2, hash3); +} + +template +template +void bloom_filter::contains(InputIt first, + InputIt last, + OutputIt output_begin, + cudaStream_t stream, + Hash1 hash1, + Hash2 hash2, + Hash3 hash3) +{ + auto num_keys = std::distance(first, last); + if (num_keys == 0) { return; } + + std::size_t constexpr block_size = 256; + std::size_t constexpr stride = 4; + std::size_t const grid_size = SDIV(num_keys, stride * block_size); + detail::contains<<>>( + first, last, output_begin, get_device_view(), hash1, hash2, hash3); +} + +template +template +__device__ Slot bloom_filter::device_view_base::key_pattern( + Key const& key, Hash1 hash1, Hash2 hash2) const noexcept +{ + slot_type pattern = 0; + std::size_t k = 0; + std::size_t i = 0; + + auto h1 = hash1(key); + // odd number to be co-prime with the number of bits in the slot + auto h2 = hash2(key) | 1; + + while (k < num_hashes_) { + // extended double hashing + slot_type const bit = + slot_type{1} << ((h1 + (i * h2) + ((i * i * i - i) / 6)) % detail::type_bits()); + + if (not(pattern & bit)) { + pattern += bit; + k++; + } + i++; + } + + return pattern; +} + +template +template +__device__ bool bloom_filter::device_mutable_view::insert( + Key const& key, Hash1 hash1, Hash2 hash2, Hash3 hash3) noexcept +{ + auto slot = key_slot(key, hash1); + auto const pattern = key_pattern(key, hash2, hash3); + auto const result = slot->fetch_or(pattern, cuda::std::memory_order_relaxed); + + // return `true` if the key's pattern was not already present in the filter, + // else return `false`. + return (result & pattern) != pattern; +} + +template +template +__device__ bool bloom_filter::device_view::contains( + Key const& key, Hash1 hash1, Hash2 hash2, Hash3 hash3) const noexcept +{ + auto slot = key_slot(key, hash1); + auto const pattern = key_pattern(key, hash2, hash3); + auto const result = slot->load(cuda::std::memory_order_relaxed); + + // return `true` if the key's pattern was already present in the filter, + // else return `false`. + return (result & pattern) == pattern; +} +} // namespace cuco diff --git a/include/cuco/detail/bloom_filter_kernels.cuh b/include/cuco/detail/bloom_filter_kernels.cuh new file mode 100644 index 000000000..ed65c8c62 --- /dev/null +++ b/include/cuco/detail/bloom_filter_kernels.cuh @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +namespace cuco { +namespace detail { + +/** + * @brief Initializes each slot in the flat `slot` storage. + * + * @tparam block_size The size of the thread block + * @tparam atomic_slot_type Type of the slot's atomic container + * @param slots Pointer to flat `slot` storage + * @param num_slots Size of the storage pointed to by `slots` + */ +template +__global__ void __launch_bounds__(block_size) + initialize(atomic_slot_type* const slots, std::size_t num_slots) +{ + for (std::size_t tid = block_size * blockIdx.x + threadIdx.x; tid < num_slots; + tid += gridDim.x * block_size) { + new (&slots[tid]) atomic_slot_type{0}; + } +} + +/** + * @brief Inserts all keys in the range `[first, last)`. + * + * @tparam block_size The size of the thread block + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the filter's `key_type` + * @tparam View Type of device view allowing access of filter storage + * @tparam Hash Unary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param view Mutable device view used to access the filter's slot storage + * @param hash1 First hash function; used to determine a filter slot + * @param hash2 Second hash function; used to generate a signature of the key + * @param hash3 Third hash function; used to generate a signature of the key + */ +template +__global__ void __launch_bounds__(block_size) + insert(InputIt first, InputIt last, View view, Hash1 hash1, Hash2 hash2, Hash3 hash3) +{ + std::size_t tid = block_size * blockIdx.x + threadIdx.x; + auto it = first + tid; + + while (it < last) { + typename View::key_type const key{*it}; + view.insert(key, hash1, hash2, hash3); + it += gridDim.x * block_size; + } +} + +/** + * @brief Indicates whether the keys in the range `[first, last)` are contained + * in the filter. + * + * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists + * in the filter. + * + * @tparam block_size The size of the thread block + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the filter's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to `bool`. + * @tparam View Type of device view allowing access of filter storage + * @tparam Hash Unary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param view Mutable device view used to access the filter's slot storage + * @param hash1 First hash function; used to determine a filter slot + * @param hash2 Second hash function; used to generate a signature of the key + * @param hash3 Third hash function; used to generate a signature of the key + */ +template +__global__ void __launch_bounds__(block_size) contains(InputIt first, + InputIt last, + OutputIt output_begin, + View view, + Hash1 hash1, + Hash2 hash2, + Hash3 hash3) +{ + std::size_t tid = block_size * blockIdx.x + threadIdx.x; + auto it = first + tid; + + while ((first + tid) < last) { + typename View::key_type const key{*(first + tid)}; + *(output_begin + tid) = view.contains(key, hash1, hash2, hash3); + tid += gridDim.x * block_size; + } +} + +} // namespace detail +} // namespace cuco diff --git a/include/cuco/detail/cache_residency_control.cuh b/include/cuco/detail/cache_residency_control.cuh new file mode 100644 index 000000000..f5711e631 --- /dev/null +++ b/include/cuco/detail/cache_residency_control.cuh @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + */ + +#pragma once + +#include +#include + +namespace cuco { + +/** + * @brief Registers the global memory region `[begin, end)` to + * be permanently resident in L2 cache. + * + * @tparam Iterator Accessor of the memory region + * @param[in, out] stream The CUDA stream this region is accessed through + * @param[in] begin Start of the memory region to be mapped + * @param[in] end End of the memory region + * @param[in] hit_rate Probability for a sub-segment to be mapped in L2 + * @param[in] carve_out Fraction of total L2 space to be blocked for resident memory segments + * + * @note Only has effect on Ampere and above. + * @note Assumes the memory region to be contiguous. + */ +template +void register_l2_persistence( + cudaStream_t& stream, Iterator begin, Iterator end, float hit_rate = 0.6f, float carve_out = 1.0f) +{ + using value_type = typename std::iterator_traits::value_type; + + int device_id; + cudaGetDevice(&device_id); + cudaDeviceProp prop; + cudaGetDeviceProperties(&prop, device_id); + + hit_rate = std::clamp(hit_rate, 0.0f, 1.0f); + carve_out = std::clamp(carve_out, 0.0f, 1.0f); + // Must be less than cudaDeviceProp::accessPolicyMaxWindowSize + auto const num_bytes = std::min(std::distance(begin, end) * sizeof(value_type), + std::size_t(prop.accessPolicyMaxWindowSize)); + + cudaDeviceSetLimit(cudaLimitPersistingL2CacheSize, carve_out * prop.persistingL2CacheMaxSize); + + // Stream level attributes data structure + cudaStreamAttrValue stream_attribute; + // Global Memory data pointer + stream_attribute.accessPolicyWindow.base_ptr = reinterpret_cast(&begin[0]); + // Number of bytes for persistence access. + stream_attribute.accessPolicyWindow.num_bytes = num_bytes; + // Hint for cache hit ratio + stream_attribute.accessPolicyWindow.hitRatio = hit_rate; + // Type of access property on cache hit + stream_attribute.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; + // Type of access property on cache miss. + stream_attribute.accessPolicyWindow.missProp = cudaAccessPropertyStreaming; + // Set the attributes to a CUDA stream of type cudaStream_t + cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_attribute); +} + +/** + * @brief Globally removes all persistent cache lines from L2. + * + * @param[in, out] stream The CUDA stream the resident region has been accessed through + * + * @note Only has effect on Ampere and above. + */ +void unregister_l2_persistence(cudaStream_t& stream) +{ + cudaStreamAttrValue stream_attribute; + // Setting the window size to 0 to disable it + stream_attribute.accessPolicyWindow.num_bytes = 0; + // Overwrite the access policy attribute of CUDA Stream + cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_attribute); + // Remove any persistent lines in L2$ + cudaCtxResetPersistingL2Cache(); +} + +} // namespace cuco \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 92dd5a34d..19ce6a429 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -64,6 +64,7 @@ ConfigureTest(STATIC_MAP_TEST static_map/stream_test.cu static_map/unique_sequence_test.cu) +#################################################################################################### foreach(arch IN LISTS CMAKE_CUDA_ARCHITECTURES) if("${arch}" MATCHES "^6") target_compile_definitions(STATIC_MAP_TEST PRIVATE CUCO_NO_INDEPENDENT_THREADS) @@ -86,3 +87,8 @@ ConfigureTest(STATIC_MULTIMAP_TEST static_multimap/multiplicity_test.cu static_multimap/non_match_test.cu static_multimap/pair_function_test.cu) + +################################################################################################### +# - bloom_filter tests ------------------------------------------------------------------------- +ConfigureTest(BLOOM_FILTER_TEST +bloom_filter/bloom_filter_test.cu) \ No newline at end of file diff --git a/tests/bloom_filter/bloom_filter_test.cu b/tests/bloom_filter/bloom_filter_test.cu new file mode 100644 index 000000000..1c7e4f608 --- /dev/null +++ b/tests/bloom_filter/bloom_filter_test.cu @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include + +#include + +template +__global__ void shared_memory_filter_kernel(bool* key_found) +{ + namespace cg = cooperative_groups; + + using filter_type = + cuco::bloom_filter, Slot>; + using mutable_view_type = typename filter_type::device_mutable_view; + using view_type = typename filter_type::device_view; + + __shared__ typename mutable_view_type::slot_type slots[NumSlots]; + + auto mutable_view = mutable_view_type::make_from_uninitialized_slots( + cg::this_thread_block(), &slots[0], NumSlots * CHAR_BIT, 4); + + auto g = cg::this_thread_block(); + std::size_t index = threadIdx.x + blockIdx.x * blockDim.x; + int rank = g.thread_rank(); + + mutable_view.insert(rank); + g.sync(); + + auto view = view_type(mutable_view); + key_found[index] = view.contains(rank); +} + +TEMPLATE_TEST_CASE_SIG("Unit tests for cuco::bloom_filter.", + "", + ((typename Key, typename Slot), Key, Slot), + (int32_t, int32_t), + (int64_t, int64_t)) +{ + using filter_type = + cuco::bloom_filter, Slot>; + + SECTION("Edge cases during object construction.") + { + SECTION( + "The ctor should allocate at least a single slot independent of the value given by num_bits.") + { + filter_type filter{0, 1}; + + REQUIRE(filter.get_num_slots() == 1); + REQUIRE(filter.get_num_bits() == sizeof(Slot) * CHAR_BIT); + } + + SECTION("The number of hash function to apply should always be in range [1, slot bits].") + { + filter_type filter_a{1, 0}; + + REQUIRE(filter_a.get_num_hashes() == 1); + + filter_type filter_b{1, 1000}; + REQUIRE(filter_b.get_num_hashes() == sizeof(Slot) * CHAR_BIT); + } + } + + SECTION("Core functionality.") + { + std::size_t constexpr num_keys{10'000'000}; + std::size_t constexpr num_bits{250'000'000}; + std::size_t constexpr num_hashes{4}; + + // generate test data + thrust::device_vector keys(num_keys * 2); + thrust::sequence(keys.begin(), keys.end(), 1); + thrust::device_vector contained(num_keys, false); + + // true-positives + auto tp_begin = keys.begin(); + auto tp_end = tp_begin + num_keys; + + filter_type filter{num_bits, num_hashes}; + + SECTION("There should be no keys present in an empty filter.") + { + filter.contains(tp_begin, tp_end, contained.begin()); + + REQUIRE(cuco::test::none_of( + contained.begin(), contained.end(), [] __device__(bool const& b) { return b; })); + } + + SECTION("Host-side bulk API.") + { + filter.insert(tp_begin, tp_end); + + SECTION("All inserted keys should be present in the filter after insertion.") + { + filter.contains(tp_begin, tp_end, contained.begin()); + + REQUIRE(cuco::test::all_of( + contained.begin(), contained.end(), [] __device__(bool const& b) { return b; })); + } + + SECTION( + "Only a fraction of foreign keys (false positives) should be contained in the filter.") + { + // true negatives + auto tn_begin = tp_end; + auto tn_end = keys.end(); + + filter.contains(tn_begin, tn_end, contained.begin()); + + float fp = thrust::count(thrust::device, contained.begin(), contained.end(), true); + float fpr = fp / num_keys; + REQUIRE(fpr < 0.05); + } + + SECTION("Re-initializing the filter should delete all keys.") + { + filter.initialize(); + + filter.contains(tp_begin, tp_end, contained.begin()); + + REQUIRE(cuco::test::none_of( + contained.begin(), contained.end(), [] __device__(bool const& b) { return b; })); + } + } + + SECTION("Device-side API.") + { + SECTION("Insert keys using the filter's mutable view.") + { + auto view = filter.get_device_mutable_view(); + + thrust::for_each( + thrust::device, tp_begin, tp_end, [view] __device__(Key const& key) mutable { + view.insert(key); + }); + + filter.contains(tp_begin, tp_end, contained.begin()); + + REQUIRE(cuco::test::all_of( + contained.begin(), contained.end(), [] __device__(bool const& b) { return b; })); + } + + SECTION("Check if all inserted keys can be found using the filter's device view.") + { + filter.insert(tp_begin, tp_end); + + auto view = filter.get_device_view(); + + REQUIRE(cuco::test::all_of( + tp_begin, tp_end, [view] __device__(Key const& key) { return view.contains(key); })); + } + } + } + + SECTION("Filter in shared memory.") + { + thrust::device_vector contained(1024, false); + + shared_memory_filter_kernel<<<1, 1024>>>(contained.data().get()); + + REQUIRE(cuco::test::all_of( + contained.begin(), contained.end(), [] __device__(bool const& b) { return b; })); + } +} \ No newline at end of file