diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 7753718d1..1a2fe59f2 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -80,6 +80,7 @@ - [concrete.ml.quantization.md](developer-guide/api/concrete.ml.quantization.md) - [concrete.ml.quantization.post_training.md](developer-guide/api/concrete.ml.quantization.post_training.md) - [concrete.ml.quantization.quantized_module.md](developer-guide/api/concrete.ml.quantization.quantized_module.md) + - [concrete.ml.quantization.quantized_module_passes.md](developer-guide/api/concrete.ml.quantization.quantized_module_passes.md) - [concrete.ml.quantization.quantized_ops.md](developer-guide/api/concrete.ml.quantization.quantized_ops.md) - [concrete.ml.quantization.quantizers.md](developer-guide/api/concrete.ml.quantization.quantizers.md) - [concrete.ml.search_parameters.md](developer-guide/api/concrete.ml.search_parameters.md) diff --git a/docs/advanced_examples/FullyConnectedNeuralNetwork.ipynb b/docs/advanced_examples/FullyConnectedNeuralNetwork.ipynb index 574810462..4df7be8ff 100644 --- a/docs/advanced_examples/FullyConnectedNeuralNetwork.ipynb +++ b/docs/advanced_examples/FullyConnectedNeuralNetwork.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -62,16 +62,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "params = {\n", " \"module__n_layers\": 3,\n", - " \"module__n_w_bits\": 3,\n", - " \"module__n_a_bits\": 4,\n", - " \"module__n_accum_bits\": 9,\n", - " \"module__activation_function\": nn.Sigmoid,\n", + " \"module__activation_function\": nn.ReLU,\n", " \"max_epochs\": 1000,\n", " \"verbose\": 0,\n", "}\n", @@ -80,7 +77,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -89,7 +86,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -110,14 +107,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "The test accuracy of the trained Concrete ML simulated model is 86.84%\n" + "The test accuracy of the trained Concrete ML simulated model is 97.37%\n" ] } ], @@ -138,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -155,14 +152,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Generating a key for a 8-bit circuit\n" + "Generating a key for a 9-bit circuit\n" ] } ], @@ -172,14 +169,14 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Key generation time: 135.09 seconds\n" + "Key generation time: 46.67 seconds\n" ] } ], @@ -198,21 +195,21 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 38/38 [02:38<00:00, 4.16s/it]" + "100%|██████████| 38/38 [00:46<00:00, 1.23s/it]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "Execution time: 4.17 seconds per sample\n" + "Execution time: 1.23 seconds per sample\n" ] }, { @@ -242,7 +239,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -250,8 +247,8 @@ "output_type": "stream", "text": [ "Test accuracy using the sklearn model: 100.00%\n", - "Test accuracy using the Concrete ML simulated model: 86.84%\n", - "Test accuracy using the Concrete ML FHE model: 86.84%\n" + "Test accuracy using the Concrete ML simulated model: 97.37%\n", + "Test accuracy using the Concrete ML FHE model: 97.37%\n" ] } ], @@ -280,7 +277,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ @@ -314,12 +311,12 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 26, "metadata": {}, "outputs": [ { "data": { - "image/png": "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sGEDn376abK9jwEDBtjFHu3atWsMHTo0znOKFy8OGMXo+6dXb9myJcEdsh9F9erVady4Mb/99hsDBw4kKCgoVp8DBw6wZs2amO+//fbbOAuMq1evxmazUaBAgWSNUURERCQpNO1aREREnE5AQABgFN+CgoI4evQohw8fxmaz8cQTTzB//nzKlStnd06WLFlYsWIFrVu3pn///rz//vuULVuWrFmzcunSJfbt28fNmzdZtmwZZcuWBYxi08yZM+nRowfdunXj3XffpXz58gQGBnLo0CHOnj3LjRs3cHd3jzfWTZs2MWnSJPLly0elSpXw8fHhwoULbNmyhaioKN55551Yu1E/qGDBgnz99dcEBATQunVrateuTYECBdi3bx9Hjhwhd+7czJ079/F+qEk0YMAA+vbty9dff03RokU5ePAgf/75Jz4+PsyaNcuub6dOnXj++eeZOnUqZcuWpUGDBnh5ebFp0yauXr1KjRo1ePfdd+3O6d69O+PGjWPbtm2UKlWKqlWrcu7cOXbv3s3gwYMZP358sryP7t27s2zZMhYvXkzp0qVp2LAhGTJkYMOGDRQpUoQaNWrEKhq/9NJLzJo1iy+//JJNmzZRvnx5zp8/z9atW3nttdeSLbZoc+fOpVmzZnz55ZfMnz+fihUr4ufnR2BgIAcPHuTs2bO8/PLLNGvWDIClS5fSo0cPihYtSrly5fD09OTkyZPs3LkTFxcX3n///WSNT0RERCQpNPJRREREnM7s2bOZPXs23333HVu2bMHV1ZUePXrw/fff8/fff1OlSpU4z6tRowZ//PEHw4YNw8fHh82bN7N8+XJOnz5NvXr1mDVrVsxU62jdunVjz549PPvsswQGBrJ06VL27t1LwYIF+eSTT8iUKVOCsQYEBPDaa6/h5+fHrl27WLp0KSdPnqRFixb88ssvDB48OEnv+bnnnmPLli20atWKv//+myVLlnDnzh0GDBjA3r17HbJRTly6dOnCDz/8gKurKytWrODcuXO0bduW7du3U6lSpVj9v/76a+bMmUOlSpXYvHkzK1euJFeuXHzwwQds2LAh1m7Tnp6erF+/nu7du3Pr1i1++uknIiMjWbhwIQMHDkzW9zJ//nzGjh1Lvnz5WLNmDTt27ODpp59mw4YNcRaUixcvzp49e2jdujXXrl3jhx9+IDg4mK+//jrZRz4C5MqVi23btvHZZ59RunRpfv/9d5YsWcLBgwcpUqQIH3/8MUOGDInpP3jwYAYOHEjmzJnZsmULy5Yt48qVK3Tt2pWdO3fSuXPnZI9RREREJDEWW3xb8omIiIiIiIiIiIg8Bo18FBEREREREREREYdQ8VFEREREREREREQcQsVHERERERERERERcQgVH0VERERERERERMQhVHwUERERERERERERh1DxUURERERERERERBxCxUcRERERERERERFxCBUfRURERERERERExCFUfBQRERERERERERGHUPFRREREREREREREHELFRxEREREREREREXEIFR9FRERERERERETEIVR8FBEREREREREREYdQ8VFEREREREREREQcQsVHERERERERERERcQgVH0VERERERERERMQhVHwUERERERERERERh1DxUURERERERERERBxCxUcRERERERERERFxCBUfRURERERERERExCFUfBQRERERERERERGHUPFRJJ2xWq0EBASYHUay2r17N7Vq1cLb2xuLxcL+/fvNDsl048aNo2TJkkRFRT30uQEBAWTKlCnRfv7+/vj7+z9CdCmrW7dudOnSxewwRERExATKE2N7nDxRzFGjRg2GDRtmdhgij0zFR5E04sSJE/Tv358iRYrg4eGBj48PtWvXZtKkSdy5c8fs8BLk7+9P2bJlH+nc8PBwOnfuzL///svEiRP59ttvKVSoUDJH6FyCgoIYO3Ysr7/+Oi4u9/43HxwczKhRoyhbtize3t5kz56dihUr8vLLL3PhwgUTI3as119/naVLl3LgwAGzQxEREUm1Zs2ahcViifM1fPjwmH5Wq5VWrVrFeY1NmzZhsVhYsmRJkq5rsVjYsWNHgnEpT0xe8eWJAHfv3mXixIlUr14dX19fPDw8KF68OIMGDeLo0aMmRZx8bt++zejRo9m0aVOyX/v+z/nWrVtjHbfZbBQoUACLxRLrvx+LxcKgQYMSvP7rr7/O5MmTuXTpUrLGLZJSMpgdgIg8vlWrVtG5c2fc3d3p0aMHZcuWJSwsjK1btzJ06FD+/PNPpk6danaYDnHixAlOnz7NtGnT6Nu3r9nhpAozZswgIiKC7t27x7SFh4fz1FNPcfjwYXr27MmLL75IcHAwf/75J/Pnz6d9+/b4+fmZGLXjVKpUiSpVqvDJJ58wZ84cs8MRERFJ1d59910KFy5s1/aoxb/ErgtQrFixx752fJQnxhZXnghw7do1mjVrxt69e2nVqhVPP/00mTJl4siRIyxYsICpU6cSFhZmUtTJ4/bt27zzzjsADpu94+Hhwfz586lTp45d++bNmzl37hzu7u6PdN22bdvi4+PDl19+ybvvvpscoYqkKBUfRZzcyZMn6datG4UKFWLDhg3kzZs35tjAgQM5fvw4q1atMjFCiIqKIiwsDA8Pj2S/9pUrVwDIkiVLsl0zJCQEb2/vZLteSps5cyZt2rSx+3kvX76c33//nXnz5vH000/b9b97967TJJM2m427d+/i6en5UOd16dKFUaNG8eWXXyZpSrmIiEh61bx5c6pUqeI0102I8sTY4soTwVh25/fff2fJkiV07NjR7th7773Hm2++mZJhJsmj5oWO1KJFCxYvXsxnn31Ghgz3yi3z58+ncuXKXLt27ZGu6+LiQqdOnZgzZw7vvPMOFosluUIWSRGadi3i5MaNG0dwcDDTp0+3KzxGK1asGC+//HKC17h58yavvPIKBQoUwN3dnWLFijF27NhY68CMHz+eWrVqkT17djw9PalcubLdtJpo0VMH5s2bR5kyZXB3d2fNmjUP9b6ir7F8+XLKli2Lu7s7ZcqUsbtOQEAA9erVA6Bz585YLBa7p5iHDx+mU6dOZMuWDQ8PD6pUqcIPP/xgd5/oKRKbN2/mhRdeIFeuXOTPnz/m+OrVq6lbty7e3t5kzpyZli1b8ueff9pdI3qNxPPnz9OuXTsyZcpEzpw5GTJkCJGRkXZ9o6KimDRpEuXKlcPDw4OcOXPSrFkz9uzZY9dv7ty5VK5cGU9PT7Jly0a3bt04e/Zsoj+3kydPcvDgQRo1amTXfuLECQBq164d65zoafoJ2b9/Pzlz5sTf35/g4OB4+4WGhjJq1CiKFSuGu7s7BQoUYNiwYYSGhtr1mzlzJg0aNCBXrly4u7tTunRppkyZEut60VO71q5dS5UqVfD09OTrr7+Omda1aNEiPvjgA/Lnz4+HhwcNGzbk+PHjsa7TuHFjQkJCWLduXYLvU0RERFI/5YnJmyfu3LmTVatW0adPn1iFRwB3d3fGjx9v17Zhw4aY954lSxbatm3L33//bddn9OjRWCwWjh8/TkBAAFmyZMHX15devXpx+/btWPeZO3cu1apVw8vLi6xZs/LUU0/x888/xxyPLy+ExH+fOXXqFDlz5gSIKd5ZLBZGjx4dc/2kfCYS0717d65fv26Xc4aFhbFkyZJYAwAeVuPGjTl9+rTWLRWnpJGPIk5u5cqVFClShFq1aj3S+bdv36ZevXqcP3+e/v37U7BgQbZt28aIESO4ePEin376aUzfSZMm0aZNG5555hnCwsJYsGABnTt35scff6Rly5Z2192wYQOLFi1i0KBB5MiRA6vV+tCxbd26le+//54XXniBzJkz89lnn9GxY0fOnDlD9uzZ6d+/P/ny5ePDDz/kpZdeomrVquTOnRuAP//8k9q1a5MvXz6GDx+Ot7c3ixYtol27dixdupT27dvb3euFF14gZ86cjBw5kpCQEAC+/fZbevbsSdOmTRk7diy3b99mypQp1KlTh99//93uPUVGRtK0aVOqV6/O+PHj+eWXX/jkk08oWrQoAwYMiOnXp08fZs2aRfPmzenbty8RERFs2bKFHTt2xIwG+OCDD3j77bfp0qULffv25erVq3z++ec89dRT/P777wk+vd+2bRsATz75pF179PpGc+bM4a233nqop6W7d++madOmVKlShRUrVsT7dDkqKoo2bdqwdetWnn/+eUqVKsUff/zBxIkTOXr0KMuXL4/pO2XKFMqUKUObNm3IkCEDK1eu5IUXXiAqKoqBAwfaXffIkSN0796d/v37069fP0qUKBFz7KOPPsLFxYUhQ4YQGBjIuHHjeOaZZ9i5c6fdNUqXLo2npye//fZbrL97ERERuScwMDDW6KwcOXLYfR8eHh7nCK7AwMCHuq7FYiF79uyPFKfyxOTLE6MLbM8991ySfva//PILzZs3p0iRIowePZo7d+7w+eefU7t2bfbt2xcr7+/SpQuFCxdmzJgx7Nu3j2+++YZcuXIxduzYmD7vvPMOo0ePplatWrz77ru4ubmxc+dONmzYQJMmTWL6xZUXJuX3mZw5czJlyhQGDBhA+/bt6dChAwDly5cHHv4zER+r1UrNmjX57rvvaN68OWAUqQMDA+nWrRufffZZkq4Tl8qVKwPw22+/UalSpUe+jogpbCLitAIDA22ArW3btkk+p1ChQraePXvGfP/ee+/ZvL29bUePHrXrN3z4cJurq6vtzJkzMW23b9+26xMWFmYrW7asrUGDBnbtgM3FxcX2559/JimmevXq2cqUKRPrGm5ubrbjx4/HtB04cMAG2D7//POYto0bN9oA2+LFi+3Ob9iwoa1cuXK2u3fvxrRFRUXZatWqZXviiSdi2mbOnGkDbHXq1LFFRETEtN+6dcuWJUsWW79+/eyue+nSJZuvr69de8+ePW2A7d1337XrW6lSJVvlypVjvt+wYYMNsL300kuxfgZRUVE2m81mO3XqlM3V1dX2wQcf2B3/448/bBkyZIjV/qC33nrLBthu3bpl13779m1biRIlbICtUKFCtoCAANv06dNtly9fjnWNnj172ry9vW02m822detWm4+Pj61ly5Z2P0ubzfh7q1evXsz33377rc3FxcW2ZcsWu35fffWVDbD99ttvdvE8qGnTprYiRYrYtRUqVMgG2NasWWPXHv33XqpUKVtoaGhM+6RJk2yA7Y8//oh1/eLFi9uaN28eq11ERETu5URxve4X/W9zQq/787KEruvu7p5oXMoTHZ8ntm/f3gbYbty4keD50SpWrGjLlSuX7fr16zFtBw4csLm4uNh69OgR0zZq1CgbYOvdu3es+2XPnj3m+2PHjtlcXFxs7du3t0VGRtr1jX7vNlv8eWFSf5+5evWqDbCNGjUq1ntK6mciPtGfld27d9u++OILW+bMmWPy3c6dO9vq168f8x5atmxpdy5gGzhwYKL3sNlsNjc3N9uAAQOS1FckNdG0axEnFhQUBEDmzJkf+RqLFy+mbt26ZM2alWvXrsW8GjVqRGRkJL/++mtM3/tHvN24cYPAwEDq1q3Lvn37Yl23Xr16lC5d+pHjAmjUqBFFixaN+b58+fL4+Pjwzz//JHjev//+y4YNG+jSpQu3bt2KeU/Xr1+nadOmHDt2jPPnz9ud069fP1xdXWO+X7duHTdv3qR79+52PxdXV1eqV6/Oxo0bY933f//7n933devWtYt16dKlWCwWRo0aFevc6JGI33//PVFRUXTp0sXuvnny5OGJJ56I8773u379OhkyZIi1rqGnpyc7d+5k6NChgDGNqE+fPuTNm5cXX3wx1rRogI0bN9K0aVMaNmzI999/n+gC2YsXL6ZUqVKULFnSLvYGDRrEXO/+eKJFj4SoV68e//zzT6xRE4ULF6Zp06Zx3rNXr164ubnFfF+3bl2AOD8j0Z9xERERid/kyZNZt26d3etB1atXj9Vn3bp1sabmJnbd1atXP3KcyhOTL098mN8pLl68yP79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9mTSpElUr16dS5cuAeDp6Ymvry8AQ4YMoXXr1hQqVIgLFy4watQoXF1d6d69uyPfSppR2BUWZ4KLUbA7EiJtUNYVnnA1OzIREXloVrMDEEn9lI+mPhks8LYHvO4OWyLghg1yuUBtV63zKCIikhY5dOTjnj17qFSpEpUqVQJg8ODBVKpUiZEjRwJw8eJFu50Bp06dSkREBAMHDiRv3rwxr5dffjmmz7lz5+jevTslSpSgS5cuZM+enR07dpAz54OrGEpC8rpAm4zQ3k2FR0kHrGYHIOIA/uizLZIEykdTLzcLNMwIndzgKW0wIyIikmal2IYzqUlQUBC+vr7pboFvkXTJijbkkLTJH3220zltOOPclI+KyEPxRw8dJeVYgQCTYxCnkNR8NNWt+SgiIiIiIiIiIiJpg4qPIiIiIiIiIiIi4hAqPoqIiIiIiIiIiIhDqPgoImmXFa2JJ2mTP/psi4iIiIiIU1DxUUTSLqvZAYiIiIiIiIikbyo+ioiIiIiIiIiIiEOo+CgiIiIiIiIiIiIOoeKjiIiIiIiIiIiIOISKjyKSdlnNDkDEQfzNDkBERERERCRpVHwUkbTJ3+wARBwkwOwAREREREREkk7FRxEREREREREREXEIFR9FRERERERERMTgb3YAktao+CgiIiIiIiIiIsa6+VaTY5A0R8VHERERERERERERcQgVH0Uk7fFHT+skbQpAn20REREREXEqKj6KSNpjNTsAEREREREREQEVH0VERERERERERMRBVHwUERERERERERERh1DxUURERERERERERBxCxUcRSVusZgcg4iD+6PMtIiIiIiJOR8VHEUlb/M0OQMRB/M0OQERERERE5OGp+CgiIiIiIiKSWlnR7AcRcWoqPoqIiIiIiIikVlazAxAReTwqPoqIiIiIiIiIiIhDqPgoImlHgNkBiIiIiIiIiMj9VHwUERFJ7UabHYCIiIiIiMijUfFRREREREREREREHELFRxERERERERER0QZH4hAqPoqIiIiIiIiIpHdWwN/kGCRNUvFRRNIGf7MDEBEREREREZEHqfgoImmD1ewARBwkwOwAREREREREHp2KjyIiIqmVFRXWRURERETEqan4KCIiIiIiIiIiIg6h4qOIiIiIiIiIiIg4hIqPIuL8rGYHICIiIiIiIiJxUfFRRJyfv9kBiDiI1ewARERE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ioiImEf5qEAKbjiTmgQFBeHr66sFvkWckT8a+Shplz8qsEuSacMZ56Z8VESSzB/lv+J4/igPlYeW1Hw0Va35KCIikm5ZUcInIiIiIiJpjoqPIiIiqYG/2QGIiIiIiIgkPxUfRcR5+KMpJyIiIiIiIiJORMVHEREREREREZH0yh/NwhGHUvFRREREREREREREHELFRxEREREREREREXEIFR9FRETMFoDWMxURERERkTRJxUcRcR5WswMQERERERERkYeh4qOIOIcAswMQERERERERkYel4qOIiIiIiIiIiIg4hIqPIiIiIiIiIiIi4hAqPoqIiJgpAK1nKiIiIiIiaZaKjyIiIiIiIiIiIuIQKj6KSOoXYHYAIiIiIiIiIvIoVHwUERERERERSa2sZgcgIvJ4VHwUERERERERSY38zQ5AROTxqfgoIiJiFisazSAiIiLxs5odgIjI41PxUURSN6vZAYg4UIDZAYiIiIiIiDiWio8ikrr5mx2AiIiIiIiIiDwqFR9FRERERERERNIjKxrwIQ6n4qOIiIiIiIiISHpkNTsASQ9UfBQRERERERERERGHUPFRRETEDKPNDkBERERERMTxVHwUkdQrwOwARERERERERORxqPgoIiIiIiIiIiIiDqHio4iIiIiIiIiIiDiEio8iIiIiIiIiIiLiECo+ioiIpLQAswMQERERERFJGSo+ikjqFGB2ACIOYv3vJSIiIiIikg6o+CgiqY/V7ABEREREREREJDmo+CgiIiIiIiIiIiIOoeKjiIiIiIiIiIiIOISKjyIiIiIiIiKpTYDZAYiIJA8VH0VERFKKFf0iISIiIiIi6YqKjyKSulgBf5NjEBEREREREZFkoeKjiIiIiIiIiIiIOISKjyIiIiIiIiIi6Y0VzTqTFKHio4iIiIiIiIhIeuNvdgCSXqj4KCKpi7/ZAYg4UIDZAYiIiIiIiKQsFR9FJPWwmh2AiAP5mx2AiIiIiIhIylPxUURERERERERERBxCxUcRERERERERERFxCBUfRURERERERERExCEymB2AOKerUXDVBr4WyKcStiQXq9kBiIiIiLO4Y4MzUcZoikIu4GYxOyIRERGJi8pG8lA2hkPzYMgVBGVuQf4gqHELFoSBzWZ2dOLU/FHxUdIuf7ThjIhIMrkYBa/chryBUPIWFL8FBYLgrTtwM8rs6ERE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pUiY0xN2O2DBLqPxGF8GetaCMLd1G0TE17ht5qM305lm8Sp2VHxKYLGhmY8ipUAzH32b6tEAY0f1nsiZEtDfCxEfZ/rMRxERERERkYCWYHYAERER86n5KCIinmNDH8RERMS/2dCyOiIiIqdR81FERERERKQkEswOICIi4r3UfBQxU4LZAURERETknNjQDEcREZFiUPNRRERERESkOGyn3URERKRY1HwUEREREREpiC333m5iBhERER+m5qOIiHiO3ewAIiIixWBDMxxF3MmG/n6JBBA1H0XMYjM7gIiH2dDPvYiIeDd77r3NxAwigcBudgAR8SQ1H0XMYjc7gIiIiIgARl1mMzmDiIiIn1LzUUREREREAo8NnQwWERHxADUfRUREREQkMCSYHUBERCTwqPkoIiIiIiL+LcHsACIiIoFLzUcRMySYHUDEw+zo0jYREfE8G/r/R0RExGRWswOIiIiIiIiIiIiIf1LzUURERERERERERNxCzUcRERERERERERFxCzUfRURERERERERExC3UfBTxNLvZAUQ8zIZ+7kVERERERAKUmo8inmYzO4CIh9nMDiAiIiIiIiJmUfNRRERERERERERE3ELNRxEREREREREREXELNR9FRERERERERETELYLNDiASMGxo0w0JTDazA4iIiIiIiIhZNPNRRETcJwE1H0VERETkFBuqD0UCjJqPIiIiIiIiIuIZCWYHEBFP02XXIiJSpE1H4bvdkJEDdWKhRy0IDTI7lYiISBFsZgcQkdKSlATzvoaDhyE2Bq7qClXizE4lIsWh5qOIiBRo+zEYshi+32M0G8ODwZEBcVEwvhXccaHZCUVERApgQ81HET+QlQUPPwlvToXUdIgJh+QMuGsU3NQbJr0IMTFmpxSRwqj5KOIpdrMDiJydnUnQdhaUCYcZveG6hhAWDOsPwksrYOj3cDQdxrY0O6mIiIiI+COnE/reDnPnw5i2MLQ5VIuBY2nw/h8w/n+weQv88BVER5udVkQKojUfRTzBZnYAkbN371KICIXlg+DmJkbjEaBxZZjWCx6+DB5ebsyOzJcd/eyLiIiIyDn7/H/w2TyYdT08eYXReAQoGwH3XArf94cNm+CVN83NKSKFU/NRRET+Y5cD5u+Ese2gYlT+Yx66DMqGw5Q/C3gRm7vSiYiIiEggeHMqtKsJ1zbM//jFVeDW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Zs0aOnXqdPIxq9VKp06dWLFihYnJJBAlJSUB6N+7c5STk8PMmTNJSUmhdevWZscR8SqqSUtO9ai4i+pR8TaqSc+d6tHSpeZjER588EGioqI477zz2L17N/PmzTM7ks/atm0bEydO5I477jA7ivihI0eOkJOTQ+XKlfM8XrlyZRITE01KJYHI6XRy77330rZtWxo3bmx2HJ/y559/Eh0dTVhYGEOHDmXu3Lk0atTI7FgiXkE1aelQPSrupHpUvIlq0nOjetQ9Aq75OGbMGCwWS6G3zZs3nxz/wAMP8Pvvv7Nw4UKCgoLo378/gX6l+tm+hwD79u2ja9eu3HDDDQwePNik5N7hXN4/EfEdd911F+vXr2fmzJlmR/E59evXZ+3ataxcuZJhw4YxYMAANm7caHYsEbdQTVoyqkdLRvWoiP9TTXpuVI+6R8Ct+Xj48GH++eefQsfUrl2b0NDQ/zy+d+9e4uPjWb58eUBPuz3b93D//v3Y7XYuvfRSpk+fjtUacD3vPM7lZ1Br7BQtMzOTyMhIZs+enWddmAEDBnDs2DHNEDkLWl/n3A0fPpx58+axbNkyatWqZXYcn9epUyfq1KnDW2+9ZXYUkVKnmrRkVI+WjOpR91A9WrpUk5471aSlR/Vo6Qg2O4CnVaxYkYoVK57Tc51OJwAZGRmlGcnnnM17uG/fPjp06ECzZs2YNm1awBd6ULKfQSlYaGgozZo1Y/HixScLFKfTyeLFixk+fLi54cTvuVwuRowYwdy5c1myZImKvFLidDoD/v9c8V+qSUtG9WjJqB51D9WjYjbVpKVP9WjpCLjmY3GtXLmS1atX065dO8qVK8f27dt59NFHqVOnTsCeYT5b+/btw263U7NmTV588UUOHz588lhcXJyJyXzH7t27OXr0KLt37yYnJ4e1a9cCULduXaKjo80N54VGjhzJgAEDaN68OS1btuTVV18lJSWFgQMHmh3N6yUnJ7Nt27aTv9+5cydr166lfPny1KhRw8RkvuGuu+5ixowZzJs3jzJlypxc1yk2NpaIiAiT0/mGsWPH0q1bN2rUqMHx48eZMWMGS5Ys4dtvvzU7moipVJOWjOrRklM9enZUj5aMatKSUU1aMqpH3cgl+Vq3bp2rQ4cOrvLly7vCwsJcNpvNNXToUNfevXvNjuYzpk2b5gLyvUnxDBgwIN/374cffjA7mteaOHGiq0aNGq7Q0FBXy5YtXb/88ovZkXzCDz/8kO/P2oABA8yO5hMK+rdu2rRpZkfzGbfddpurZs2artDQUFfFihVdHTt2dC1cuNDsWCKmU01aMqpHS0716NlTPXruVJOWjGrSklE96j4Bt+ajiIiIiIiIiIiIeIYWPBERERERERERERG3UPNRRERERERERERE3ELNRxEREREREREREXELNR9FRERERERERETELdR8FBEREREREREREbdQ81FERERERERERETcQs1HERERERERERERcQs1H0VERERERERERMQt1HwUERERERERERERt1DzUURERERERERERNxCzUcRERERERERERFxi/8DyZF5CAEjizQAAAAASUVORK5CYII=", + "image/png": "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LHA/Labicr58+eLzFiJFfF7fe+893nvvvRj7PVlg9PX15eeff2bp0qW0atUKV1dXKleuTNOmTenVqxc5c+Z8phhEREREEoOKjyIiIpLq/P777wCULFkysi08PByAjh07xlgMAyhevHiixzNp0iT69evHmjVr+OWXX/j11185cOAA48eP54cffqBDhw7Pdf2YdscGoh01l1zEFnd0Iv4MY2Lr97pkyRImTZpE3rx5mTx5MtWrVydbtmykS5eO4OBgXFxcYixKurq6PtO9It5rrVq14l38dnR0ZMmSJYwcOZI1a9awdetW9u/fz65du/j888/ZuHEjNWrUeKY4RERERJ6Xio8iIiKSqvj5+bFp0yYA6tWrF9meJ08eTp48yciRIyOnp8Ylb968nD59mrNnz1K+fPnniqtYsWKMGDGCESNG8OjRI77++muGDx9O//794yw+ent7c/LkSS5cuECpUqWiHI8YJZc7d+7nijE+Lly4EGu7t7c3YEx3dnZ25vbt2wQGBkZb8I0u7ogp4tHtIh4xEvV5RcR269YtHj58GGUNR4CLFy9GaVu1ahVgrLPYokULq2P//PNPosQWIWIUZ9u2bXnrrbee6dwKFSpQoUIFxowZg7+/P2PGjGHy5MkMGTLEapq2iIiISFJIvo/CRURERBLgrbfeIjAwkMqVK1O9evXI9kaNGgGPC0jxEbFxR8TmHonF1dWVYcOGkStXLm7dusXNmzdj7R+xHuUPP/wQ5VhwcDDLli2z6mdLS5cujdIWGhrKihUrAGOkHhjrQFarVg2AxYsXRznn+PHjHD16lPTp01sVdnPlygXAqVOnopyzefPm544/IraqVasC0b+fn3/+2WqadoSIadjRTe+O7jrPIyGf1+h4enoyduxYTCYTx48fT4zQRERERJ6Jio8iIiKSKvzzzz906dKFWbNm4eHhwaxZs6yO9+vXj+zZszN+/HhmzJgRZQpvaGgomzZtsirQDBkyBFdXV2bOnMmSJUus+lssFjZv3hznmomrV6+O3NzkSYcOHeLGjRukT5+ejBkzxnqN3r174+bmxuLFi1m/fn1ke3h4OO+++y5XrlyhYsWKca5nmRh2797N999/b9U2evRoLl68SNmyZa0KoIMGDQKMHaWfHBl4//59Bg4ciMVioV+/flZTkuvWrQvAF198wYMHDyLbt27dypdffplo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NqHio4iISGLzRRs+iYiIiIiIoOKjiIiIiIiIiIiI2IiKjyKpldneAYiIiIiIiIhIWqfio0hq5GPvAEREREREREREVHwUERERERERERERG1HxUUREJDH5oGUPRERERERE/qPio4iISGLysXcAIiIiIiIiyYeKjyIiIiIiIiIiImITKj6KiIiIiIjIs/G1dwAiIpJSqPgokhqZ7R2AiIiIiKRaZnsHICIiKYmKjyKpja+9AxBJw8z2DkBERERERCR5UfFRREQkMZhR8V9EREREROQpKj6KiIiIiIiIiIiITaj4KCIiIiIiIiIiIjah4qOIiIiIiIiIiIjYhIqPIiIiIiIiIiIiYhMqPoqIiCQGX3sHICIiIiIikvyo+CiSmvjaOwCRNMps7wBERERERESSJxUfRURERERERERExCZUfBQRERERERERERGbUPFRREREREREREREbELFRxERkedltncAIiIiIiIiyZOKjyIiIs/DDPjYOQYREREREZFkSsVHERERERERERERsQkVH0VSCx97ByAiIiIiIiIiYk3FR5HUwmzvAEREREQkTfCxdwAiKZSvvQMQsQ8VH0VERBLKjJJIERFJW8z2DkAkhTKjvz+SZqn4KCIiklBmewcgIiIiIiKSvKn4KCIiIiIiIiIiIjah4qOIiIiIiIiIiIjYhIqPIiIiIiIiIiIiYhMqPoqkBj72DkAkjTLbOwAREREREZHkTcVHkdTAbO8ARNIgH/R3T0REREREJA4qPoqIiIiIiIiIiIhNqPgoIiIiIiIiIiIiNqHio4iIiIiIiIiIiNhEkhQfp06ditlsxtXVlapVq3LgwIEY+86ZMweTyWT1cnV1tepjsVgYNWoUuXLlws3NjYYNG3L69Glbvw0RERERSaGUj4qIiIjYh82Lj0uWLGHo0KGMHj2aw4cPU65cOZo0acLNmzdjPMfT05Nr165Fvi5cuGB1fPz48fzvf/9j+vTp7N+/Hw8PD5o0acKjR49s/XZERESMzWZ87ByDiMSb8lERERER+7F58XHSpEn07duXnj17UrJkSaZPn467uzvff/99jOeYTCZy5swZ+cqRI0fkMYvFwpdffsn7779PmzZtKFu2LPPmzePq1ausXr3a1m9HJPkx2zsAERGR5E35qIiIiIj92LT4GBwczKFDh2jYsOHjGzo40LBhQ/bu3RvjeQEB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mzdvBqBChQrnX/fFF18kLi7u/C00S5cuJSwsjHr16hXmSxERcVuBvvDRTdD7O+j8GTzZ1lhvx2aHH3bDc6tgaxwsu83YXU9ExNOoJhURMd/zrWDZIWjzMTwbDf0aQGgA7DoJ7/wKU3+D0ddB84Jf6xERD2Wx2+12R7xw165dOX78ONOmTSMjI4OBAwfStGlTZs2aBcCRI0fo0KEDn376Kc2bN2fv3r3MmjWLbt26cdVVV7FlyxYeeeQRKleuzKpVqwDIysqicePGVKxYkVdffZXY2Fjuvfde7r//fl566aUCZ0tISCA8PJz4cAjTL+ZSWNHotmtxCT/+A4+sNjaVCfQ1mo/pWdC0PEyOhhYVzE6Yixj0PSRSDBISILwKxMfHZ9uJWbJz1ZpU9ag4XYzZAcQTxSXD8OXw7dlVKQJ9jXUeywTBuKZG89FrL4ZHnz1EPFhB61GHbDgDxg6BI0eOpEOHDlitVnr37s277757/nxGRga7du06v3Ogv78/P/30E2+//TZJSUlERkbSu3dvnnrqqfPP8fHx4bvvvmP48OG0bNmSkJAQBgwYwHPPPeeoL0NExGV1joJtVWH1EfjzBFgt0CICmrny1eUo1HgUEadSTSoi4jjlgmFeDziYAIsPGI3HyBLQoxoEOKzbICLuxmEzH12ZrjRLkUSj5onIlYpCMy9EiolmPro31aPidDFmBxDxMtFo5qN4vILWo9r0XkRERERERERERBxCzUeRwohGsx5FRERERERERApIzUcRERERERERERFxCDUfRUTEOaLQelMiIiJmiDE7gIiIeDM1H0VERERERERERMQh1HwUERERERERERERh1DzUURERERERERERBxCzUcRERERERERERFxCDUfRQoq6uwhIlcmxuwAIiIiIiIi4mxqPooUVLTZAUTcWLTZAURERERERMQMaj6KiIiIiIiIiIiIQ6j5KCIiIiIiIiIiIg6h5qOIiIiIiIiIiIg4hJqPIiLieFFmBxAREREREREzqPkoIiKOFY2ajyIiIiIiIl5KzUcRERERERERERFxCDUfRQoixuwAIiIiIiIiIiLuR81HERERERERERERcQg1H0VERERERERERMQh1HwUERHHiT57iIiIiIh4i2hUA4tcRM1HERFxnCizA4iIiIiIiIiZ1HwUERERERERERERh1DzUURERERERERE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TVi2bNn5AsVms7Fs2TJGjhxpbjjxeHa7nVGjRjF//nxWrlypIq+Y2Gw2r/83VzyXatKiUT1aNKpHHUP1qJhNNWnxUz1aPLyu+VhQ69evZ+PGjbRp04ZSpUqxd+9enn76aWrUqOG1V5gL68iRI0RHR1O1alVef/11Tpw4cf5cRESEicncx8GDBzl16hQHDx4kKyuLzZs3A1CzZk1CQ0PNDeeCRo8ezYABA2jatCnNmzfn7bffJikpiYEDB5odzeUlJiayZ8+e83/ev38/mzdvpnTp0lSpUsXEZO5hxIgRzJo1iwULFlCiRInz6zqFh4cTFBRkcjr3MH78eLp27UqVKlU4c+YMs2bNYuXKlfz4449mRxMxlWrSolE9WnSqRwtH9WjRqCYtGtWkRaN61IHskqMtW7bY27dvby9durQ9ICDAHhUVZR82bJj98OHDZkdzGzNmzLADOR5SMAMGDMjx/VuxYoXZ0VzWpEmT7FWqVLH7+/vbmzdvbv/111/NjuQWVqxYkeP/awMGDDA7mlvI7WfdjBkzzI7mNu677z571apV7f7+/vayZcvaO3ToYF+yZInZsURMp5q0aFSPFp3q0cJTPXrlVJMWjWrSolE96jhet+ajiIiIiIiIiIiIOIcWPBERERERERERERGHUPNRREREREREREREHELNRxEREREREREREXEINR9FRERERERERETEIdR8FBEREREREREREYdQ81FEREREREREREQcQs1HERERERERERERcQg1H0VERERERERERMQh1HwUERERERERERERh1DzUURERERERERERBxCzUcRERERERERERFxiP8H5XKYNvNKcUMAAAAASUVORK5CYII=", "text/plain": [ "
" ] diff --git a/docs/developer-guide/api/README.md b/docs/developer-guide/api/README.md index c2f6cb245..cb8750f4b 100644 --- a/docs/developer-guide/api/README.md +++ b/docs/developer-guide/api/README.md @@ -33,6 +33,7 @@ - [`concrete.ml.quantization.base_quantized_op`](./concrete.ml.quantization.base_quantized_op.md#module-concretemlquantizationbase_quantized_op): Base Quantized Op class that implements quantization for a float numpy op. - [`concrete.ml.quantization.post_training`](./concrete.ml.quantization.post_training.md#module-concretemlquantizationpost_training): Post Training Quantization methods. - [`concrete.ml.quantization.quantized_module`](./concrete.ml.quantization.quantized_module.md#module-concretemlquantizationquantized_module): QuantizedModule API. +- [`concrete.ml.quantization.quantized_module_passes`](./concrete.ml.quantization.quantized_module_passes.md#module-concretemlquantizationquantized_module_passes): Optimization passes for QuantizedModules. - [`concrete.ml.quantization.quantized_ops`](./concrete.ml.quantization.quantized_ops.md#module-concretemlquantizationquantized_ops): Quantized versions of the ONNX operators for post training quantization. - [`concrete.ml.quantization.quantizers`](./concrete.ml.quantization.quantizers.md#module-concretemlquantizationquantizers): Quantization utilities for a numpy array/tensor. - [`concrete.ml.search_parameters`](./concrete.ml.search_parameters.md#module-concretemlsearch_parameters): Modules for `p_error` search. @@ -106,6 +107,7 @@ - [`post_training.PostTrainingAffineQuantization`](./concrete.ml.quantization.post_training.md#class-posttrainingaffinequantization): Post-training Affine Quantization. - [`post_training.PostTrainingQATImporter`](./concrete.ml.quantization.post_training.md#class-posttrainingqatimporter): Converter of Quantization Aware Training networks. - [`quantized_module.QuantizedModule`](./concrete.ml.quantization.quantized_module.md#class-quantizedmodule): Inference for a quantized model. +- [`quantized_module_passes.PowerOfTwoScalingRoundPBSAdapter`](./concrete.ml.quantization.quantized_module_passes.md#class-poweroftwoscalingroundpbsadapter): Detect neural network patterns that can be optimized with round PBS. - [`quantized_ops.ONNXConstantOfShape`](./concrete.ml.quantization.quantized_ops.md#class-onnxconstantofshape): ConstantOfShape operator. - [`quantized_ops.ONNXGather`](./concrete.ml.quantization.quantized_ops.md#class-onnxgather): Gather operator. - [`quantized_ops.ONNXShape`](./concrete.ml.quantization.quantized_ops.md#class-onnxshape): Shape operator. @@ -280,6 +282,7 @@ - [`ops_impl.numpy_celu`](./concrete.ml.onnx.ops_impl.md#function-numpy_celu): Compute celu in numpy according to ONNX spec. - [`ops_impl.numpy_concatenate`](./concrete.ml.onnx.ops_impl.md#function-numpy_concatenate): Apply concatenate in numpy according to ONNX spec. - [`ops_impl.numpy_constant`](./concrete.ml.onnx.ops_impl.md#function-numpy_constant): Return the constant passed as a kwarg. +- [`ops_impl.numpy_conv`](./concrete.ml.onnx.ops_impl.md#function-numpy_conv): Compute N-D convolution using Torch. - [`ops_impl.numpy_cos`](./concrete.ml.onnx.ops_impl.md#function-numpy_cos): Compute cos in numpy according to ONNX spec. - [`ops_impl.numpy_cosh`](./concrete.ml.onnx.ops_impl.md#function-numpy_cosh): Compute cosh in numpy according to ONNX spec. - [`ops_impl.numpy_div`](./concrete.ml.onnx.ops_impl.md#function-numpy_div): Compute div in numpy according to ONNX spec. @@ -289,6 +292,7 @@ - [`ops_impl.numpy_exp`](./concrete.ml.onnx.ops_impl.md#function-numpy_exp): Compute exponential in numpy according to ONNX spec. - [`ops_impl.numpy_flatten`](./concrete.ml.onnx.ops_impl.md#function-numpy_flatten): Flatten a tensor into a 2d array. - [`ops_impl.numpy_floor`](./concrete.ml.onnx.ops_impl.md#function-numpy_floor): Compute Floor in numpy according to ONNX spec. +- [`ops_impl.numpy_gemm`](./concrete.ml.onnx.ops_impl.md#function-numpy_gemm): Compute Gemm in numpy according to ONNX spec. - [`ops_impl.numpy_greater`](./concrete.ml.onnx.ops_impl.md#function-numpy_greater): Compute greater in numpy according to ONNX spec. - [`ops_impl.numpy_greater_float`](./concrete.ml.onnx.ops_impl.md#function-numpy_greater_float): Compute greater in numpy according to ONNX spec and cast outputs to floats. - [`ops_impl.numpy_greater_or_equal`](./concrete.ml.onnx.ops_impl.md#function-numpy_greater_or_equal): Compute greater or equal in numpy according to ONNX spec. diff --git a/docs/developer-guide/api/concrete.ml.onnx.ops_impl.md b/docs/developer-guide/api/concrete.ml.onnx.ops_impl.md index f0079e5f8..2e8511ed0 100644 --- a/docs/developer-guide/api/concrete.ml.onnx.ops_impl.md +++ b/docs/developer-guide/api/concrete.ml.onnx.ops_impl.md @@ -140,7 +140,43 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Constant-13 ______________________________________________________________________ - + + +## function `numpy_gemm` + +```python +numpy_gemm( + a: ndarray, + b: ndarray, + c: Optional[ndarray] = None, + alpha: float = 1, + beta: float = 1, + transA: int = 0, + transB: int = 0 +) → Tuple[ndarray] +``` + +Compute Gemm in numpy according to ONNX spec. + +See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Gemm-13 + +**Args:** + +- `a` (numpy.ndarray): Input tensor A. The shape of A should be (M, K) if transA is 0, or (K, M) if transA is non-zero. +- `b` (numpy.ndarray): Input tensor B. The shape of B should be (K, N) if transB is 0, or (N, K) if transB is non-zero. +- `c` (Optional\[numpy.ndarray\]): Optional input tensor C. If not specified, the computation is done as if C is a scalar 0. The shape of C should be unidirectional broadcastable to (M, N). Defaults to None. +- `alpha` (float): Scalar multiplier for the product of input tensors A * B. Defaults to 1. +- `beta` (float): Scalar multiplier for input tensor C. Defaults to 1. +- `transA` (int): Whether A should be transposed. The type is kept as int as it is the type used by ONNX and it can easily be interpreted by Python as a boolean. Defaults to 0. +- `transB` (int): Whether B should be transposed. The type is kept as int as it is the type used by ONNX and it can easily be interpreted by Python as a boolean. Defaults to 0. + +**Returns:** + +- `Tuple[numpy.ndarray]`: The tuple containing the result tensor + +______________________________________________________________________ + + ## function `numpy_matmul` @@ -163,7 +199,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#MatMul-13 ______________________________________________________________________ - + ## function `numpy_relu` @@ -185,7 +221,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Relu-14 ______________________________________________________________________ - + ## function `numpy_sigmoid` @@ -207,7 +243,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Sigmoid-13 ______________________________________________________________________ - + ## function `numpy_softmax` @@ -233,7 +269,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#softmax-13 ______________________________________________________________________ - + ## function `numpy_cos` @@ -255,7 +291,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Cos-7 ______________________________________________________________________ - + ## function `numpy_cosh` @@ -277,7 +313,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Cosh-9 ______________________________________________________________________ - + ## function `numpy_sin` @@ -299,7 +335,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Sin-7 ______________________________________________________________________ - + ## function `numpy_sinh` @@ -321,7 +357,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Sinh-9 ______________________________________________________________________ - + ## function `numpy_tan` @@ -343,7 +379,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Tan-7 ______________________________________________________________________ - + ## function `numpy_tanh` @@ -365,7 +401,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Tanh-13 ______________________________________________________________________ - + ## function `numpy_acos` @@ -387,7 +423,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Acos-7 ______________________________________________________________________ - + ## function `numpy_acosh` @@ -409,7 +445,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Acosh-9 ______________________________________________________________________ - + ## function `numpy_asin` @@ -431,7 +467,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Asin-7 ______________________________________________________________________ - + ## function `numpy_asinh` @@ -453,7 +489,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Asinh-9 ______________________________________________________________________ - + ## function `numpy_atan` @@ -475,7 +511,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Atan-7 ______________________________________________________________________ - + ## function `numpy_atanh` @@ -497,7 +533,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Atanh-9 ______________________________________________________________________ - + ## function `numpy_elu` @@ -520,7 +556,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Elu-6 ______________________________________________________________________ - + ## function `numpy_selu` @@ -548,7 +584,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Selu-6 ______________________________________________________________________ - + ## function `numpy_celu` @@ -571,7 +607,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Celu-12 ______________________________________________________________________ - + ## function `numpy_leakyrelu` @@ -594,7 +630,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#LeakyRelu-6 ______________________________________________________________________ - + ## function `numpy_thresholdedrelu` @@ -617,7 +653,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#ThresholdedRelu-10 ______________________________________________________________________ - + ## function `numpy_hardsigmoid` @@ -645,7 +681,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#HardSigmoid-6 ______________________________________________________________________ - + ## function `numpy_softplus` @@ -667,7 +703,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Softplus-1 ______________________________________________________________________ - + ## function `numpy_abs` @@ -689,7 +725,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Abs-13 ______________________________________________________________________ - + ## function `numpy_div` @@ -712,7 +748,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Div-14 ______________________________________________________________________ - + ## function `numpy_mul` @@ -735,7 +771,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Mul-14 ______________________________________________________________________ - + ## function `numpy_sub` @@ -758,7 +794,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Sub-14 ______________________________________________________________________ - + ## function `numpy_log` @@ -780,7 +816,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Log-13 ______________________________________________________________________ - + ## function `numpy_erf` @@ -802,7 +838,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Erf-13 ______________________________________________________________________ - + ## function `numpy_hardswish` @@ -824,7 +860,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#hardswish-14 ______________________________________________________________________ - + ## function `numpy_exp` @@ -846,7 +882,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Exp-13 ______________________________________________________________________ - + ## function `numpy_equal` @@ -869,7 +905,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Equal-11 ______________________________________________________________________ - + ## function `numpy_not` @@ -891,7 +927,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Not-1 ______________________________________________________________________ - + ## function `numpy_not_float` @@ -913,7 +949,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Not-1 ______________________________________________________________________ - + ## function `numpy_greater` @@ -936,7 +972,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Greater-13 ______________________________________________________________________ - + ## function `numpy_greater_float` @@ -959,7 +995,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Greater-13 ______________________________________________________________________ - + ## function `numpy_greater_or_equal` @@ -982,7 +1018,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#GreaterOrEqual-12 ______________________________________________________________________ - + ## function `numpy_greater_or_equal_float` @@ -1005,7 +1041,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#GreaterOrEqual-12 ______________________________________________________________________ - + ## function `numpy_less` @@ -1028,7 +1064,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Less-13 ______________________________________________________________________ - + ## function `numpy_less_float` @@ -1051,7 +1087,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Less-13 ______________________________________________________________________ - + ## function `numpy_less_or_equal` @@ -1074,7 +1110,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#LessOrEqual-12 ______________________________________________________________________ - + ## function `numpy_less_or_equal_float` @@ -1097,7 +1133,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#LessOrEqual-12 ______________________________________________________________________ - + ## function `numpy_identity` @@ -1119,7 +1155,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Identity-14 ______________________________________________________________________ - + ## function `numpy_transpose` @@ -1142,7 +1178,48 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Transpose-13 ______________________________________________________________________ - + + +## function `numpy_conv` + +```python +numpy_conv( + x: ndarray, + w: ndarray, + b: Optional[ndarray] = None, + dilations: Tuple[int, ], + group: int = 1, + kernel_shape: Tuple[int, ], + pads: Tuple[int, ], + strides: Tuple[int, ] +) → Tuple[ndarray] +``` + +Compute N-D convolution using Torch. + +Currently supports 2d convolution with torch semantics. This function is also ONNX compatible. + +See: https://github.com/onnx/onnx/blob/main/docs/Operators.md#Conv + +**Args:** + +- `x` (numpy.ndarray): input data (many dtypes are supported). Shape is N x C x H x W for 2d +- `w` (numpy.ndarray): weights tensor. Shape is (O x I x Kh x Kw) for 2d +- `b` (Optional\[numpy.ndarray\]): bias tensor, Shape is (O,). Default to None. +- `dilations` (Tuple\[int, ...\]): dilation of the kernel, default 1 on all dimensions. +- `group` (int): number of convolution groups, can be 1 or a multiple of both (C,) and (O,), so that I = C / group. Default to 1. +- `kernel_shape` (Tuple\[int, ...\]): shape of the kernel. Should have 2 elements for 2d conv +- `pads` (Tuple\[int, ...\]): padding in ONNX format (begin, end) on each axis +- `strides` (Tuple\[int, ...\]): stride of the convolution on each axis + +**Returns:** + +- `res` (numpy.ndarray): a tensor of size (N x OutChannels x OutHeight x OutWidth). +- `See https`: //pytorch.org/docs/stable/generated/torch.nn.Conv2d.html + +______________________________________________________________________ + + ## function `numpy_avgpool` @@ -1181,7 +1258,7 @@ See: https://github.com/onnx/onnx/blob/main/docs/Operators.md#AveragePool ______________________________________________________________________ - + ## function `numpy_maxpool` @@ -1222,7 +1299,7 @@ See: https://github.com/onnx/onnx/blob/main/docs/Operators.md#MaxPool ______________________________________________________________________ - + ## function `numpy_cast` @@ -1247,7 +1324,7 @@ See: https://github.com/onnx/onnx/blob/main/docs/Operators.md#Cast ______________________________________________________________________ - + ## function `numpy_batchnorm` @@ -1289,7 +1366,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#BatchNormalization- ______________________________________________________________________ - + ## function `numpy_flatten` @@ -1312,7 +1389,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Flatten-13. ______________________________________________________________________ - + ## function `numpy_or` @@ -1335,7 +1412,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Or-7 ______________________________________________________________________ - + ## function `numpy_or_float` @@ -1358,7 +1435,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Or-7 ______________________________________________________________________ - + ## function `numpy_round` @@ -1380,7 +1457,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Round-11 Remark tha ______________________________________________________________________ - + ## function `numpy_pow` @@ -1403,7 +1480,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Pow-13 ______________________________________________________________________ - + ## function `numpy_floor` @@ -1425,7 +1502,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Floor-1 ______________________________________________________________________ - + ## function `numpy_max` @@ -1450,7 +1527,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Max-1 ______________________________________________________________________ - + ## function `numpy_min` @@ -1475,7 +1552,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Max-1 ______________________________________________________________________ - + ## function `numpy_sign` @@ -1497,7 +1574,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Sign-9 ______________________________________________________________________ - + ## function `numpy_neg` @@ -1519,7 +1596,7 @@ See https://github.com/onnx/onnx/blob/main/docs/Changelog.md#Sign-9 ______________________________________________________________________ - + ## function `numpy_concatenate` diff --git a/docs/developer-guide/api/concrete.ml.pytest.torch_models.md b/docs/developer-guide/api/concrete.ml.pytest.torch_models.md index 6fca69e23..88d054e33 100644 --- a/docs/developer-guide/api/concrete.ml.pytest.torch_models.md +++ b/docs/developer-guide/api/concrete.ml.pytest.torch_models.md @@ -8,13 +8,13 @@ Torch modules for our pytests. ______________________________________________________________________ - + ## class `SimpleNet` Fake torch model used to generate some onnx. - + ### method `__init__` @@ -24,7 +24,7 @@ __init__() → None ______________________________________________________________________ - + ### method `forward` @@ -44,13 +44,13 @@ Forward function. ______________________________________________________________________ - + ## class `FCSmall` Torch model for the tests. - + ### method `__init__` @@ -60,7 +60,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -79,13 +79,13 @@ the output of the NN ______________________________________________________________________ - + ## class `FC` Torch model for the tests. - + ### method `__init__` @@ -95,7 +95,7 @@ __init__(activation_function, input_output=3072) ______________________________________________________________________ - + ### method `forward` @@ -114,13 +114,13 @@ the output of the NN ______________________________________________________________________ - + ## class `CNN` Torch CNN model for the tests. - + ### method `__init__` @@ -130,7 +130,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -149,13 +149,13 @@ the output of the NN ______________________________________________________________________ - + ## class `CNNMaxPool` Torch CNN model for the tests with a max pool. - + ### method `__init__` @@ -165,7 +165,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -184,13 +184,13 @@ the output of the NN ______________________________________________________________________ - + ## class `CNNOther` Torch CNN model for the tests. - + ### method `__init__` @@ -200,7 +200,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -219,13 +219,13 @@ the output of the NN ______________________________________________________________________ - + ## class `CNNInvalid` Torch CNN model for the tests. - + ### method `__init__` @@ -235,7 +235,7 @@ __init__(activation_function, groups) ______________________________________________________________________ - + ### method `forward` @@ -254,13 +254,13 @@ the output of the NN ______________________________________________________________________ - + ## class `CNNGrouped` Torch CNN model with grouped convolution for compile torch tests. - + ### method `__init__` @@ -270,7 +270,7 @@ __init__(input_output, activation_function, groups) ______________________________________________________________________ - + ### method `forward` @@ -289,7 +289,7 @@ the output of the NN ______________________________________________________________________ - + ## class `NetWithLoops` @@ -297,7 +297,7 @@ Torch model, where we reuse some elements in a loop. Torch model, where we reuse some elements in a loop in the forward and don't expect the user to define these elements in a particular order. - + ### method `__init__` @@ -307,7 +307,7 @@ __init__(activation_function, input_output, n_fc_layers) ______________________________________________________________________ - + ### method `forward` @@ -326,13 +326,13 @@ the output of the NN ______________________________________________________________________ - + ## class `MultiInputNN` Torch model to test multiple inputs forward. - + ### method `__init__` @@ -342,7 +342,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -362,13 +362,13 @@ the output of the NN ______________________________________________________________________ - + ## class `MultiInputNNConfigurable` Torch model to test multiple inputs forward. - + ### method `__init__` @@ -378,7 +378,7 @@ __init__(use_conv, use_qat, input_output, n_bits) ______________________________________________________________________ - + ### method `forward` @@ -398,13 +398,13 @@ the output of the NN ______________________________________________________________________ - + ## class `MultiInputNNDifferentSize` Torch model to test multiple inputs with different shape in the forward pass. - + ### method `__init__` @@ -419,7 +419,7 @@ __init__( ______________________________________________________________________ - + ### method `forward` @@ -439,13 +439,13 @@ The output of the NN. ______________________________________________________________________ - + ## class `BranchingModule` Torch model with some branching and skip connections. - + ### method `__init__` @@ -455,7 +455,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -474,13 +474,13 @@ the output of the NN ______________________________________________________________________ - + ## class `BranchingGemmModule` Torch model with some branching and skip connections. - + ### method `__init__` @@ -490,7 +490,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -509,13 +509,13 @@ the output of the NN ______________________________________________________________________ - + ## class `UnivariateModule` Torch model that calls univariate and shape functions of torch. - + ### method `__init__` @@ -525,7 +525,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -544,13 +544,13 @@ the output of the NN ______________________________________________________________________ - + ## class `StepActivationModule` Torch model implements a step function that needs Greater, Cast and Where. - + ### method `__init__` @@ -560,7 +560,7 @@ __init__(input_output, activation_function) ______________________________________________________________________ - + ### method `forward` @@ -579,13 +579,13 @@ the output of the NN ______________________________________________________________________ - + ## class `NetWithConcatUnsqueeze` Torch model to test the concat and unsqueeze operators. - + ### method `__init__` @@ -595,7 +595,7 @@ __init__(activation_function, input_output, n_fc_layers) ______________________________________________________________________ - + ### method `forward` @@ -614,13 +614,13 @@ the output of the NN ______________________________________________________________________ - + ## class `MultiOpOnSingleInputConvNN` Network that applies two quantized operations on a single input. - + ### method `__init__` @@ -630,7 +630,7 @@ __init__(can_remove_input_tlu: bool) ______________________________________________________________________ - + ### method `forward` @@ -649,7 +649,7 @@ the output of the NN ______________________________________________________________________ - + ## class `FCSeq` @@ -657,7 +657,7 @@ Torch model that should generate MatMul->Add ONNX patterns. This network generates additions with a constant scalar - + ### method `__init__` @@ -667,7 +667,7 @@ __init__(input_output, act) ______________________________________________________________________ - + ### method `forward` @@ -686,7 +686,7 @@ the output of the NN ______________________________________________________________________ - + ## class `FCSeqAddBiasVec` @@ -694,7 +694,7 @@ Torch model that should generate MatMul->Add ONNX patterns. This network tests the addition with a constant vector - + ### method `__init__` @@ -704,7 +704,7 @@ __init__(input_output, act) ______________________________________________________________________ - + ### method `forward` @@ -723,13 +723,13 @@ the output of the NN ______________________________________________________________________ - + ## class `TinyCNN` A very small CNN. - + ### method `__init__` @@ -746,7 +746,7 @@ Create the tiny CNN with two conv layers. ______________________________________________________________________ - + ### method `forward` @@ -765,7 +765,7 @@ the output of the NN ______________________________________________________________________ - + ## class `TinyQATCNN` @@ -773,12 +773,19 @@ A very small QAT CNN to classify the sklearn digits data-set. This class also allows pruning to a maximum of 10 active neurons, which should help keep the accumulator bit-width low. - + ### method `__init__` ```python -__init__(n_classes, n_bits, n_active, signed, narrow) → None +__init__( + n_classes, + n_bits, + n_active, + signed, + narrow, + power_of_two_scaling +) → None ``` Construct the CNN with a configurable number of classes. @@ -790,10 +797,11 @@ Construct the CNN with a configurable number of classes. - `n_active` (int): number of active (non-zero weight) neurons to keep - `signed` (bool): whether quantized integer values are signed - `narrow` (bool): whether the range of quantized integer values is narrow/symmetric +- `power_of_two_scaling` (bool): whether to use power-of-two scaling quantizers ______________________________________________________________________ - + ### method `forward` @@ -812,27 +820,7 @@ the output of the NN ______________________________________________________________________ - - -### method `test_torch` - -```python -test_torch(test_loader) -``` - -Test the network: measure accuracy on the test set. - -**Args:** - -- `test_loader`: the test loader - -**Returns:** - -- `res`: the number of correctly classified test examples - -______________________________________________________________________ - - + ### method `toggle_pruning` @@ -848,13 +836,13 @@ Enable or remove pruning. ______________________________________________________________________ - + ## class `SimpleQAT` Torch model implements a step function that needs Greater, Cast and Where. - + ### method `__init__` @@ -864,7 +852,7 @@ __init__(input_output, activation_function, n_bits=2, disable_bit_check=False) ______________________________________________________________________ - + ### method `forward` @@ -883,13 +871,13 @@ the output of the NN ______________________________________________________________________ - + ## class `QATTestModule` Torch model that implements a simple non-uniform quantizer. - + ### method `__init__` @@ -899,7 +887,7 @@ __init__(activation_function) ______________________________________________________________________ - + ### method `forward` @@ -918,13 +906,13 @@ the output of the NN ______________________________________________________________________ - + ## class `SingleMixNet` Torch model that with a single conv layer that produces the output, e.g., a blur filter. - + ### method `__init__` @@ -934,7 +922,7 @@ __init__(use_conv, use_qat, inp_size, n_bits) ______________________________________________________________________ - + ### method `forward` @@ -953,7 +941,7 @@ the output of the NN ______________________________________________________________________ - + ## class `DoubleQuantQATMixNet` @@ -961,7 +949,7 @@ Torch model that with two different quantizers on the input. Used to test that it keeps the input TLU. - + ### method `__init__` @@ -971,7 +959,7 @@ __init__(use_conv, use_qat, inp_size, n_bits) ______________________________________________________________________ - + ### method `forward` @@ -990,13 +978,13 @@ the output of the NN ______________________________________________________________________ - + ## class `TorchSum` Torch model to test the ReduceSum ONNX operator in a leveled circuit. - + ### method `__init__` @@ -1013,7 +1001,7 @@ Initialize the module. ______________________________________________________________________ - + ### method `forward` @@ -1033,13 +1021,13 @@ Forward pass. ______________________________________________________________________ - + ## class `TorchSumMod` Torch model to test the ReduceSum ONNX operator in a circuit containing a PBS. - + ### method `__init__` @@ -1056,7 +1044,7 @@ Initialize the module. ______________________________________________________________________ - + ### method `forward` @@ -1076,13 +1064,13 @@ Forward pass. ______________________________________________________________________ - + ## class `NetWithConstantsFoldedBeforeOps` Torch QAT model that does not quantize the inputs. - + ### method `__init__` @@ -1097,7 +1085,7 @@ __init__( ______________________________________________________________________ - + ### method `forward` @@ -1117,13 +1105,13 @@ Forward pass. ______________________________________________________________________ - + ## class `ShapeOperationsNet` Torch QAT model that reshapes the input. - + ### method `__init__` @@ -1133,7 +1121,7 @@ __init__(is_qat) ______________________________________________________________________ - + ### method `forward` @@ -1153,13 +1141,13 @@ Forward pass. ______________________________________________________________________ - + ## class `PaddingNet` Torch QAT model that applies various padding patterns. - + ### method `__init__` @@ -1169,7 +1157,7 @@ __init__() ______________________________________________________________________ - + ### method `forward` @@ -1189,13 +1177,13 @@ Forward pass. ______________________________________________________________________ - + ## class `QuantCustomModel` A small quantized network with Brevitas, trained on make_classification. - + ### method `__init__` @@ -1206,7 +1194,8 @@ __init__( hidden_shape: int = 100, n_bits: int = 5, act_quant=, - weight_quant= + weight_quant=, + bias_quant=None ) ``` @@ -1220,10 +1209,11 @@ Quantized Torch Model with Brevitas. - `n_bits` (int): Bit of quantization - `weight_quant` (brevitas.quant): Quantization protocol of weights - `act_quant` (brevitas.quant): Quantization protocol of activations. +- `bias_quant` (brevitas.quant): Quantizer for the linear layer bias ______________________________________________________________________ - + ### method `forward` @@ -1243,13 +1233,13 @@ Forward pass. ______________________________________________________________________ - + ## class `TorchCustomModel` A small network with Brevitas, trained on make_classification. - + ### method `__init__` @@ -1267,7 +1257,7 @@ Torch Model. ______________________________________________________________________ - + ### method `forward` @@ -1287,13 +1277,13 @@ Forward pass. ______________________________________________________________________ - + ## class `ConcatFancyIndexing` Concat with fancy indexing. - + ### method `__init__` @@ -1319,7 +1309,7 @@ Torch Model. ______________________________________________________________________ - + ### method `forward` diff --git a/docs/developer-guide/api/concrete.ml.quantization.md b/docs/developer-guide/api/concrete.ml.quantization.md index 33a2d5359..8fb4af6c4 100644 --- a/docs/developer-guide/api/concrete.ml.quantization.md +++ b/docs/developer-guide/api/concrete.ml.quantization.md @@ -12,4 +12,6 @@ Modules for quantization. - **base_quantized_op** - **quantized_module** - **quantized_ops** +- **quantized_module_passes** - **post_training** +- **qat_quantizers** diff --git a/docs/developer-guide/api/concrete.ml.quantization.post_training.md b/docs/developer-guide/api/concrete.ml.quantization.post_training.md index 510187483..0ac4b03c9 100644 --- a/docs/developer-guide/api/concrete.ml.quantization.post_training.md +++ b/docs/developer-guide/api/concrete.ml.quantization.post_training.md @@ -14,7 +14,7 @@ Post Training Quantization methods. ______________________________________________________________________ - + ## function `get_n_bits_dict` @@ -36,7 +36,7 @@ Convert the n_bits parameter into a proper dictionary. ______________________________________________________________________ - + ## class `ONNXConverter` @@ -54,7 +54,7 @@ This class should be sub-classed to provide specific calibration and quantizatio - `numpy_model` (NumpyModule): Model in numpy. - `rounding_threshold_bits` (int): if not None, every accumulators in the model are rounded down to the given bits of precision - + ### method `__init__` @@ -108,7 +108,7 @@ Get the number of bits to use for the quantization of any constants (usually wei ______________________________________________________________________ - + ### method `quantize_module` @@ -130,7 +130,7 @@ Following https://arxiv.org/abs/1712.05877 guidelines. ______________________________________________________________________ - + ## class `PostTrainingAffineQuantization` @@ -153,7 +153,7 @@ Create the quantized version of the passed numpy module. - `QuantizedModule`: A quantized version of the numpy model. - + ### method `__init__` @@ -207,7 +207,7 @@ Get the number of bits to use for the quantization of any constants (usually wei ______________________________________________________________________ - + ### method `quantize_module` @@ -229,7 +229,7 @@ Following https://arxiv.org/abs/1712.05877 guidelines. ______________________________________________________________________ - + ## class `PostTrainingQATImporter` @@ -237,7 +237,7 @@ Converter of Quantization Aware Training networks. This class provides specific configuration for QAT networks during ONNX network conversion to Concrete ML computation graphs. - + ### method `__init__` @@ -291,7 +291,7 @@ Get the number of bits to use for the quantization of any constants (usually wei ______________________________________________________________________ - + ### method `quantize_module` diff --git a/docs/developer-guide/api/concrete.ml.quantization.quantized_module_passes.md b/docs/developer-guide/api/concrete.ml.quantization.quantized_module_passes.md new file mode 100644 index 000000000..56c78602a --- /dev/null +++ b/docs/developer-guide/api/concrete.ml.quantization.quantized_module_passes.md @@ -0,0 +1,143 @@ + + + + +# module `concrete.ml.quantization.quantized_module_passes` + +Optimization passes for QuantizedModules. + +______________________________________________________________________ + + + +## class `PowerOfTwoScalingRoundPBSAdapter` + +Detect neural network patterns that can be optimized with round PBS. + + + +### method `__init__` + +```python +__init__(qmodule: QuantizedModule) → None +``` + +______________________________________________________________________ + +#### property num_ignored_valid_patterns + +Get the number of optimizable patterns that were ignored. + +Patterns could be ignored since a number of rounding bits was set manually through the compilation function. + +**Returns:** + +- `result` (int): number of patterns that could be optimized but were not + +______________________________________________________________________ + + + +### method `compute_op_predecessors` + +```python +compute_op_predecessors() → DefaultDict[Union[QuantizedOp, NoneType], List[Tuple[Union[QuantizedOp, NoneType], str]]] +``` + +Compute the predecessors for each QuantizedOp in a QuantizedModule. + +Stores, for each quantized op, a list of quantized ops that produce its inputs. Currently only the first input of the operations is considered as it is, usually, the encrypted input. + +**Returns:** + +- `result` (PredecessorsType): a dictionary containing a hierarchy of op predecessors + +______________________________________________________________________ + + + +### method `detect_patterns` + +```python +detect_patterns( + predecessors: DefaultDict[Optional[QuantizedOp], List[Tuple[Optional[QuantizedOp], str]]] +) → Dict[QuantizedMixingOp, Tuple[List[Union[QuantizedOp, NoneType]], Union[QuantizedOp, NoneType]]] +``` + +Detect the patterns that can be optimized with roundPBS in the QuantizedModule. + +**Args:** + +- `predecessors` (PredecessorsType): Module predecessor operation list + +**Returns:** + +- `result` (PatternDict): list of optimizable patterns + +______________________________________________________________________ + + + +### method `match_path_pattern` + +```python +match_path_pattern( + predecessors: DefaultDict[Optional[QuantizedOp], List[Tuple[Optional[QuantizedOp], str]]], + nodes_in_path: List[Optional[QuantizedOp]], + input_producer_of_path: Optional[QuantizedOp] +) → bool +``` + +Determine if a pattern has the structure that makes it viable for roundPBS. + +**Args:** + +- `predecessors` (PredecessorsType): Module predecessor operation list +- `nodes_in_path` (List\[QuantizedOp\]): list of quantized ops in the pattern +- `input_producer_of_path` (Optional\[QuantizedOp\]): operation that produces the input + +**Returns:** + +- `result` (bool): whether the pattern can be optimized + +______________________________________________________________________ + + + +### method `process` + +```python +process() → Dict[QuantizedMixingOp, Tuple[List[Union[QuantizedOp, NoneType]], Union[QuantizedOp, NoneType]]] +``` + +Analyze an ONNX graph and detect Gemm/Conv patterns that can use RoundPBS. + +We want to detect a gemm/conv node whose weights/bias are Brevitas QAT, and whose input is produced by a Brevitas QAT node that is applied on the output of another Gemm/conv node. Optionally a Relu can be placed before this input quantization node. + +Nothing will be done if rounding is already specified. + +**Returns:** + +- `result` (PatternDict): a dictionary containing for each Conv/Gemm node for which round PBS can be applied based on power-of-two scaling factors + +______________________________________________________________________ + + + +### method `process_patterns` + +```python +process_patterns( + valid_paths: Dict[QuantizedMixingOp, Tuple[List[Optional[QuantizedOp]], Optional[QuantizedOp]]] +) → Dict[QuantizedMixingOp, Tuple[List[Union[QuantizedOp, NoneType]], Union[QuantizedOp, NoneType]]] +``` + +Configure the rounding bits of roundPBS for the optimizable operations. + +**Args:** + +- `valid_paths` (PatternDict): list of optimizable patterns + +**Returns:** + +- `result` (PatternDict): list of patterns actually optimized with roundPBS diff --git a/docs/developer-guide/api/concrete.ml.quantization.quantized_ops.md b/docs/developer-guide/api/concrete.ml.quantization.quantized_ops.md index 288836e0c..e1b6d7166 100644 --- a/docs/developer-guide/api/concrete.ml.quantization.quantized_ops.md +++ b/docs/developer-guide/api/concrete.ml.quantization.quantized_ops.md @@ -259,7 +259,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `q_impl` @@ -273,7 +273,7 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedMatMul` @@ -306,7 +306,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `q_impl` @@ -320,7 +320,7 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedAdd` @@ -340,7 +340,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -358,7 +358,7 @@ Add operation can be computed in float and fused if it operates over inputs prod ______________________________________________________________________ - + ### method `q_impl` @@ -371,7 +371,7 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedTanh` @@ -389,7 +389,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedSoftplus` @@ -407,7 +407,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedExp` @@ -425,7 +425,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedLog` @@ -443,7 +443,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedAbs` @@ -461,7 +461,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedIdentity` @@ -479,7 +479,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `q_impl` @@ -492,7 +492,7 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedReshape` @@ -510,7 +510,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -528,7 +528,7 @@ Max Pooling operation can not be fused since it must be performed over integer t ______________________________________________________________________ - + ### method `q_impl` @@ -552,13 +552,13 @@ Reshape the input integer encrypted tensor. ______________________________________________________________________ - + ## class `QuantizedConv` Quantized Conv op. - + ### method `__init__` @@ -601,7 +601,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `q_impl` @@ -632,13 +632,13 @@ Allows an optional quantized bias. ______________________________________________________________________ - + ## class `QuantizedAvgPool` Quantized Average Pooling op. - + ### method `__init__` @@ -665,7 +665,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `q_impl` @@ -679,13 +679,13 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedMaxPool` Quantized Max Pooling op. - + ### method `__init__` @@ -712,7 +712,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -730,7 +730,7 @@ Max Pooling operation can not be fused since it must be performed over integer t ______________________________________________________________________ - + ### method `q_impl` @@ -743,13 +743,13 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedPad` Quantized Padding op. - + ### method `__init__` @@ -776,7 +776,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -794,7 +794,7 @@ Pad operation cannot be fused since it must be performed over integer tensors. ______________________________________________________________________ - + ### method `q_impl` @@ -808,7 +808,7 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedWhere` @@ -816,7 +816,7 @@ Where operator on quantized arrays. Supports only constants for the results produced on the True/False branches. - + ### method `__init__` @@ -843,7 +843,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedCast` @@ -863,7 +863,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedGreater` @@ -871,7 +871,7 @@ Comparison operator >. Only supports comparison with a constant. - + ### method `__init__` @@ -898,7 +898,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedGreaterOrEqual` @@ -906,7 +906,7 @@ Comparison operator >=. Only supports comparison with a constant. - + ### method `__init__` @@ -933,7 +933,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedLess` @@ -941,7 +941,7 @@ Comparison operator \<. Only supports comparison with a constant. - + ### method `__init__` @@ -968,7 +968,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedLessOrEqual` @@ -976,7 +976,7 @@ Comparison operator \<=. Only supports comparison with a constant. - + ### method `__init__` @@ -1003,7 +1003,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedOr` @@ -1023,7 +1023,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedDiv` @@ -1043,7 +1043,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedMul` @@ -1063,7 +1063,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedSub` @@ -1083,7 +1083,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1101,7 +1101,7 @@ Add operation can be computed in float and fused if it operates over inputs prod ______________________________________________________________________ - + ### method `q_impl` @@ -1114,7 +1114,7 @@ q_impl( ______________________________________________________________________ - + ## class `QuantizedBatchNormalization` @@ -1132,7 +1132,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedFlatten` @@ -1150,7 +1150,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1168,7 +1168,7 @@ Flatten operation cannot be fused since it must be performed over integer tensor ______________________________________________________________________ - + ### method `q_impl` @@ -1192,13 +1192,13 @@ Flatten the input integer encrypted tensor. ______________________________________________________________________ - + ## class `QuantizedReduceSum` ReduceSum with encrypted input. - + ### method `__init__` @@ -1239,7 +1239,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `calibrate` @@ -1259,7 +1259,7 @@ Create corresponding QuantizedArray for the output of the activation function. ______________________________________________________________________ - + ### method `q_impl` @@ -1283,7 +1283,7 @@ Sum the encrypted tensor's values along the given axes. ______________________________________________________________________ - + ## class `QuantizedErf` @@ -1301,7 +1301,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedNot` @@ -1319,13 +1319,13 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedBrevitasQuant` Brevitas uniform quantization with encrypted input. - + ### method `__init__` @@ -1368,7 +1368,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `calibrate` @@ -1388,7 +1388,7 @@ Create corresponding QuantizedArray for the output of Quantization function. ______________________________________________________________________ - + ### method `q_impl` @@ -1412,7 +1412,7 @@ Quantize values. ______________________________________________________________________ - + ## class `QuantizedTranspose` @@ -1432,7 +1432,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1450,7 +1450,7 @@ Transpose can not be fused since it must be performed over integer tensors as it ______________________________________________________________________ - + ### method `q_impl` @@ -1474,7 +1474,7 @@ Transpose the input integer encrypted tensor. ______________________________________________________________________ - + ## class `QuantizedFloor` @@ -1492,7 +1492,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedMax` @@ -1510,7 +1510,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedMin` @@ -1528,7 +1528,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedNeg` @@ -1546,7 +1546,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedSign` @@ -1564,7 +1564,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ## class `QuantizedUnsqueeze` @@ -1582,7 +1582,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1600,7 +1600,7 @@ Unsqueeze can not be fused since it must be performed over integer tensors as it ______________________________________________________________________ - + ### method `q_impl` @@ -1624,7 +1624,7 @@ Unsqueeze the input tensors on a given axis. ______________________________________________________________________ - + ## class `QuantizedConcat` @@ -1642,7 +1642,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1660,7 +1660,7 @@ Concatenation can not be fused since it must be performed over integer tensors a ______________________________________________________________________ - + ### method `q_impl` @@ -1684,7 +1684,7 @@ Concatenate the input tensors on a given axis. ______________________________________________________________________ - + ## class `QuantizedSqueeze` @@ -1702,7 +1702,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1720,7 +1720,7 @@ Squeeze can not be fused since it must be performed over integer tensors as it r ______________________________________________________________________ - + ### method `q_impl` @@ -1744,7 +1744,7 @@ Squeeze the input tensors on a given axis. ______________________________________________________________________ - + ## class `ONNXShape` @@ -1762,7 +1762,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1780,7 +1780,7 @@ This operation returns the shape of the tensor and thus can not be fused into a ______________________________________________________________________ - + ### method `q_impl` @@ -1793,7 +1793,7 @@ q_impl( ______________________________________________________________________ - + ## class `ONNXConstantOfShape` @@ -1811,7 +1811,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1829,7 +1829,7 @@ This operation returns a new encrypted tensor and thus can not be fused. ______________________________________________________________________ - + ## class `ONNXGather` @@ -1849,7 +1849,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1867,7 +1867,7 @@ This operation returns values from a tensor and thus can not be fused into a uni ______________________________________________________________________ - + ### method `q_impl` @@ -1880,7 +1880,7 @@ q_impl( ______________________________________________________________________ - + ## class `ONNXSlice` @@ -1898,7 +1898,7 @@ Get the names of encrypted integer tensors that are used by this op. ______________________________________________________________________ - + ### method `can_fuse` @@ -1916,7 +1916,7 @@ This operation returns values from a tensor and thus can not be fused into a uni ______________________________________________________________________ - + ### method `q_impl` diff --git a/docs/developer-guide/api/concrete.ml.sklearn.qnn_module.md b/docs/developer-guide/api/concrete.ml.sklearn.qnn_module.md index b4ffca6ff..efbe95b98 100644 --- a/docs/developer-guide/api/concrete.ml.sklearn.qnn_module.md +++ b/docs/developer-guide/api/concrete.ml.sklearn.qnn_module.md @@ -12,7 +12,7 @@ Sparse Quantized Neural Network torch module. ______________________________________________________________________ - + ## class `SparseQuantNeuralNetwork` @@ -20,7 +20,7 @@ Sparse Quantized Neural Network. This class implements an MLP that is compatible with FHE constraints. The weights and activations are quantized to low bit-width and pruning is used to ensure accumulators do not surpass an user-provided accumulator bit-width. The number of classes and number of layers are specified by the user, as well as the breadth of the network - + ### method `__init__` @@ -36,7 +36,8 @@ __init__( n_prune_neurons_percentage: float = 0.0, activation_function: Type = , quant_narrow: bool = False, - quant_signed: bool = True + quant_signed: bool = True, + power_of_two_scaling: bool = True ) ``` @@ -55,6 +56,7 @@ Sparse Quantized Neural Network constructor. - `activation_function` (Type): The activation function to use in the network (e.g., torch.ReLU, torch.SELU, torch.Sigmoid, ...). - `quant_narrow` (bool): Whether this network should quantize the values using narrow range (e.g a 2-bits signed quantization uses \[-1, 0, 1\] instead of \[-2, -1, 0, 1\]). - `quant_signed` (bool): Whether this network should quantize the values using signed integers. +- `power_of_two_scaling` (bool): Force quantization scales to be a power of two to enable inference speed optimizations. Defaults to True **Raises:** @@ -62,7 +64,7 @@ Sparse Quantized Neural Network constructor. ______________________________________________________________________ - + ### method `enable_pruning` @@ -78,7 +80,7 @@ Enable pruning in the network. Pruning must be made permanent to recover pruned ______________________________________________________________________ - + ### method `forward` @@ -98,7 +100,7 @@ Forward pass. ______________________________________________________________________ - + ### method `make_pruning_permanent` @@ -110,7 +112,7 @@ Make the learned pruning permanent in the network. ______________________________________________________________________ - + ### method `max_active_neurons` diff --git a/script/doc_utils/update_apidocs.sh b/script/doc_utils/update_apidocs.sh index 42d207a37..056ff7f2d 100755 --- a/script/doc_utils/update_apidocs.sh +++ b/script/doc_utils/update_apidocs.sh @@ -7,9 +7,12 @@ APIDOCS_OUTPUT="$1" # Clean rm -rf "$APIDOCS_OUTPUT" +# Ignore concrete.ml.quantization.qat_quantizers since +# brevitas has some issues with lazydocs poetry run lazydocs --output-path="$APIDOCS_OUTPUT" \ --overview-file="README.md" \ --src-base-url="../../../" \ --no-watermark \ + --ignored-modules concrete.ml.quantization.qat_quantizers \ concrete.ml diff --git a/src/concrete/ml/common/utils.py b/src/concrete/ml/common/utils.py index 3af1800b5..4414056fb 100644 --- a/src/concrete/ml/common/utils.py +++ b/src/concrete/ml/common/utils.py @@ -42,6 +42,14 @@ # Indicate if the old simulation method should be used when simulating FHE executions USE_OLD_VL = True +# Debug option for testing round PBS optimization +# Setting this option to true will make quantizers "round half up" +# For example: 0.5 -> 1, 1.5 -> 2 instead of "round half to even" +# When the option is set to false, Concrete ML uses numpy.rint +# which has the same behavior as torch.round -> Brevitas nets +# should be exact compared to their Concrete ML QuantizedModule +QUANT_ROUND_LIKE_ROUND_PBS = False + class FheMode(str, enum.Enum): """Enum representing the execution mode. diff --git a/src/concrete/ml/onnx/ops_impl.py b/src/concrete/ml/onnx/ops_impl.py index 4b08818ce..d06489a49 100644 --- a/src/concrete/ml/onnx/ops_impl.py +++ b/src/concrete/ml/onnx/ops_impl.py @@ -14,6 +14,7 @@ from scipy import special from typing_extensions import SupportsIndex +from ..common import utils from ..common.debugging import assert_false, assert_true from .onnx_impl_utils import ( compute_onnx_pool_padding, @@ -240,7 +241,6 @@ def numpy_constant(**kwargs): # pylint: disable=invalid-name # 1 is technically an int but is accepted by mypy as a float (and it simplifies our life for # compilation) so instead of passing 1.0 by default 1 is passed -@onnx_func_raw_args("c") def numpy_gemm( a: numpy.ndarray, b: numpy.ndarray, @@ -1140,7 +1140,6 @@ def numpy_transpose(x: numpy.ndarray, *, perm=None) -> Tuple[numpy.ndarray]: return (numpy.transpose(x, axes=perm),) -@onnx_func_raw_args("b") def numpy_conv( x: numpy.ndarray, w: numpy.ndarray, @@ -1655,7 +1654,10 @@ def numpy_brevitas_quant( y = numpy.clip(y, min_int_val, max_int_val) # Quantize to produce integers representing the float quantized values - y = numpy.rint(y) + if utils.QUANT_ROUND_LIKE_ROUND_PBS: + y = numpy.floor(y + 0.5) + else: + y = numpy.rint(y) # Compute quantized floating point values y = (y - zero_point) * scale diff --git a/src/concrete/ml/pytest/torch_models.py b/src/concrete/ml/pytest/torch_models.py index b1a79720e..bf371c7fd 100644 --- a/src/concrete/ml/pytest/torch_models.py +++ b/src/concrete/ml/pytest/torch_models.py @@ -6,10 +6,12 @@ import brevitas.nn as qnn import numpy import torch -from brevitas.quant import Int8ActPerTensorFloat, Int8WeightPerTensorFloat +from brevitas.quant import Int8ActPerTensorFloat, Int8WeightPerTensorFloat, IntBias from torch import nn from torch.nn.utils import prune +from concrete.ml.quantization.qat_quantizers import Int8ActPerTensorPoT, Int8WeightPerTensorPoT + # pylint: disable=too-many-lines @@ -686,7 +688,7 @@ class TinyQATCNN(nn.Module): should help keep the accumulator bit-width low. """ - def __init__(self, n_classes, n_bits, n_active, signed, narrow) -> None: + def __init__(self, n_classes, n_bits, n_active, signed, narrow, power_of_two_scaling) -> None: """Construct the CNN with a configurable number of classes. Args: @@ -695,6 +697,8 @@ def __init__(self, n_classes, n_bits, n_active, signed, narrow) -> None: n_active (int): number of active (non-zero weight) neurons to keep signed (bool): whether quantized integer values are signed narrow (bool): whether the range of quantized integer values is narrow/symmetric + power_of_two_scaling (bool): whether to use power-of-two scaling quantizers which + allows to test the round PBS optimization when the scales are power-of-two """ super().__init__() @@ -705,17 +709,56 @@ def __init__(self, n_classes, n_bits, n_active, signed, narrow) -> None: q_args = {"signed": signed, "narrow_range": narrow} - self.quant1 = qnn.QuantIdentity(bit_width=a_bits, return_quant_tensor=True, **q_args) + if power_of_two_scaling: + act_quant = Int8ActPerTensorPoT + weight_quant = Int8WeightPerTensorPoT + bias_quant = IntBias + else: + act_quant = Int8ActPerTensorFloat + weight_quant = Int8WeightPerTensorFloat + bias_quant = None + + self.quant1 = qnn.QuantIdentity( + bit_width=a_bits, return_quant_tensor=True, **q_args, act_quant=act_quant + ) self.conv1 = qnn.QuantConv2d( - 1, 2, 3, stride=1, padding=0, weight_bit_width=w_bits, **q_args + 1, + 2, + 3, + stride=1, + padding=0, + weight_bit_width=w_bits, + **q_args, + weight_quant=weight_quant, + bias_quant=bias_quant, + ) + self.quant2 = qnn.QuantIdentity( + bit_width=a_bits, return_quant_tensor=True, **q_args, act_quant=act_quant ) - self.quant2 = qnn.QuantIdentity(bit_width=a_bits, return_quant_tensor=True, **q_args) self.conv2 = qnn.QuantConv2d( - 2, 3, 3, stride=2, padding=0, weight_bit_width=w_bits, **q_args + 2, + 3, + 3, + stride=2, + padding=0, + weight_bit_width=w_bits, + **q_args, + weight_quant=weight_quant, + bias_quant=bias_quant, + ) + self.quant3 = qnn.QuantIdentity( + bit_width=a_bits, return_quant_tensor=True, **q_args, act_quant=act_quant ) - self.quant3 = qnn.QuantIdentity(bit_width=a_bits, return_quant_tensor=True, **q_args) self.conv3 = qnn.QuantConv2d( - 3, 16, 2, stride=1, padding=0, weight_bit_width=w_bits, **q_args + 3, + 16, + 2, + stride=1, + padding=0, + weight_bit_width=w_bits, + **q_args, + weight_quant=weight_quant, + bias_quant=bias_quant, ) self.quant4 = qnn.QuantIdentity(bit_width=a_bits, return_quant_tensor=True, **q_args) @@ -781,47 +824,6 @@ def forward(self, x): x = self.fc1(x) return x - def test_torch(self, test_loader): - """Test the network: measure accuracy on the test set. - - Args: - test_loader: the test loader - - Returns: - res: the number of correctly classified test examples - - """ - - # Freeze normalization layers - self.eval() - - all_y_pred = numpy.zeros((len(test_loader)), dtype=numpy.int64) - all_targets = numpy.zeros((len(test_loader)), dtype=numpy.int64) - - # Iterate over the batches - idx = 0 - for data, target in test_loader: - # Accumulate the ground truth labels - endidx = idx + target.shape[0] - all_targets[idx:endidx] = target.numpy() - - # Run forward and get the raw predictions first - raw_pred = self(data).detach().numpy() - - # Get the predicted class id, handle NaNs - if numpy.any(numpy.isnan(raw_pred)): - output = -1 # pragma: no cover - else: - output = raw_pred.argmax(1) - - all_y_pred[idx:endidx] = output - - idx += target.shape[0] - - # Print out the accuracy as a percentage - n_correct = numpy.sum(all_targets == all_y_pred) - return n_correct - class SimpleQAT(nn.Module): """Torch model implements a step function that needs Greater, Cast and Where.""" @@ -1230,6 +1232,7 @@ def __init__( n_bits: int = 5, act_quant=Int8ActPerTensorFloat, weight_quant=Int8WeightPerTensorFloat, + bias_quant=None, ): """Quantized Torch Model with Brevitas. @@ -1240,7 +1243,7 @@ def __init__( n_bits (int): Bit of quantization weight_quant (brevitas.quant): Quantization protocol of weights act_quant (brevitas.quant): Quantization protocol of activations. - + bias_quant (brevitas.quant): Quantizer for the linear layer bias """ super().__init__() @@ -1253,6 +1256,7 @@ def __init__( weight_bit_width=n_bits, weight_quant=weight_quant, bias=True, + bias_quant=bias_quant, return_quant_tensor=True, ) @@ -1263,6 +1267,7 @@ def __init__( weight_bit_width=n_bits, weight_quant=weight_quant, bias=True, + bias_quant=bias_quant, return_quant_tensor=True, ) @@ -1274,6 +1279,7 @@ def __init__( weight_bit_width=n_bits, weight_quant=weight_quant, bias=True, + bias_quant=bias_quant, return_quant_tensor=True, ) diff --git a/src/concrete/ml/pytest/utils.py b/src/concrete/ml/pytest/utils.py index a66bb448e..ce130991c 100644 --- a/src/concrete/ml/pytest/utils.py +++ b/src/concrete/ml/pytest/utils.py @@ -173,6 +173,9 @@ def instantiate_model_generic(model_class, n_bits, **parameters): extra_kwargs["module__n_a_bits"] = 2 extra_kwargs["module__n_accum_bits"] = 7 + # Disable power-of-two since it sets the input bitwidth to 8 + # and thus increases bitwidth too much for a test + extra_kwargs["module__power_of_two_scaling"] = False extra_kwargs.update(parameters) model = model_class(**extra_kwargs) diff --git a/src/concrete/ml/quantization/__init__.py b/src/concrete/ml/quantization/__init__.py index 4e6dc0594..845b5dc11 100644 --- a/src/concrete/ml/quantization/__init__.py +++ b/src/concrete/ml/quantization/__init__.py @@ -7,6 +7,7 @@ QuantizedAdd, QuantizedAvgPool, QuantizedBatchNormalization, + QuantizedBrevitasQuant, QuantizedCelu, QuantizedClip, QuantizedConv, diff --git a/src/concrete/ml/quantization/post_training.py b/src/concrete/ml/quantization/post_training.py index 2a5edda20..fcbe6abd5 100644 --- a/src/concrete/ml/quantization/post_training.py +++ b/src/concrete/ml/quantization/post_training.py @@ -19,6 +19,7 @@ QuantizedOp, ) from .quantized_module import QuantizedModule +from .quantized_module_passes import PowerOfTwoScalingRoundPBSAdapter from .quantized_ops import QuantizedBrevitasQuant from .quantizers import QuantizationOptions, QuantizedArray, UniformQuantizer @@ -427,13 +428,14 @@ def _quantize_layers(self, *input_calibration_data: numpy.ndarray): attributes.update({"rounding_threshold_bits": self.rounding_threshold_bits}) # All inputs, allow optional constants (they become None) - curr_inputs = { - input_name: node_results.get(input_name, None) for input_name in node.input - } + # Note that input of a node can be duplicated, e.g., (%a, %a, %b) + curr_inputs = [ + (input_name, node_results.get(input_name, None)) for input_name in node.input + ] # Constant inputs curr_cst_inputs: Dict[int, ONNXOpInputOutputType] = {} - for input_idx, (input_name, value) in enumerate(curr_inputs.items()): + for input_idx, (input_name, value) in enumerate(curr_inputs): if not (input_name in self.quant_params or input_name in constants): continue @@ -455,10 +457,12 @@ def _quantize_layers(self, *input_calibration_data: numpy.ndarray): has_variable_inputs = (len(curr_inputs) - len(curr_cst_inputs)) > 0 variable_input_names = [ - input_name for input_name in curr_inputs if input_name not in constants + input_name for input_name, _ in curr_inputs if input_name not in constants ] curr_calibration_data = tuple( - curr_inputs[input_name] for input_name in variable_input_names + input_data + for input_name, input_data in curr_inputs + if input_name in variable_input_names ) # For mypy @@ -604,6 +608,10 @@ def quantize_module(self, *calibration_data: numpy.ndarray) -> QuantizedModule: onnx_model=self.numpy_model.onnx_model, ) + adapter = PowerOfTwoScalingRoundPBSAdapter(quantized_module) + # Apply the round PBS optimization if possible + adapter.process() + self._process_input_quantizers(quantized_module, calibration_data) return quantized_module diff --git a/src/concrete/ml/quantization/qat_quantizers.py b/src/concrete/ml/quantization/qat_quantizers.py new file mode 100644 index 000000000..36a916546 --- /dev/null +++ b/src/concrete/ml/quantization/qat_quantizers.py @@ -0,0 +1,30 @@ +"""Custom Quantization Aware Training Brevitas quantizers.""" +from brevitas.quant.scaled_int import ( + IntQuant, + MaxStatsScaling, + ParamFromRuntimePercentileScaling, + PerTensorPoTScaling8bit, + WeightQuantSolver, +) +from brevitas.quant.solver.act import ActQuantSolver + +# Note these classes are added here in order to isolate them from +# the other modules, since the API doc generator has +# an error when parsing them. Putting them in a separate +# file allows us to ignore them during API doc generation + + +# pylint: disable-next=too-many-ancestors +class Int8ActPerTensorPoT( + IntQuant, ParamFromRuntimePercentileScaling, PerTensorPoTScaling8bit, ActQuantSolver +): + """Quantization options for power-of-two scaling activations.""" + + _partialmethod = None + + +# pylint: disable-next=too-many-ancestors +class Int8WeightPerTensorPoT(IntQuant, MaxStatsScaling, PerTensorPoTScaling8bit, WeightQuantSolver): + """Quantization options for power-of-two scaling weights.""" + + _partialmethod = None diff --git a/src/concrete/ml/quantization/quantized_module_passes.py b/src/concrete/ml/quantization/quantized_module_passes.py new file mode 100644 index 000000000..517e2b15d --- /dev/null +++ b/src/concrete/ml/quantization/quantized_module_passes.py @@ -0,0 +1,304 @@ +"""Optimization passes for QuantizedModules.""" +from collections import defaultdict +from typing import DefaultDict, Dict, List, Optional, Tuple + +import numpy + +from ..common.debugging import assert_true +from .base_quantized_op import QuantizedMixingOp, QuantizedOp +from .quantized_module import QuantizedModule +from .quantized_ops import ( + QuantizedBrevitasQuant, + QuantizedConv, + QuantizedGemm, + QuantizedMatMul, + QuantizedRelu, +) + +# A dictionary that contains for a quantized op a list of predecessor ops +# Each predecessor op is stored along with its output tensor name +PredecessorsType = DefaultDict[Optional[QuantizedOp], List[Tuple[Optional[QuantizedOp], str]]] + +# A list of optimizable patterns. For a "Mixing" op that supports rounding accumulators +# we store a list of ops which contain information that allows us to +# compute the integer scaling factor for the Mixing op. +# The quantizer op of the input to the the Mixing op is stored in the second member of the tuple +PatternDict = Dict[QuantizedMixingOp, Tuple[List[Optional[QuantizedOp]], Optional[QuantizedOp]]] + + +class PowerOfTwoScalingRoundPBSAdapter: + """Detect neural network patterns that can be optimized with round PBS.""" + + SUPPORTED_ROUND_PBS_OPS = (QuantizedGemm, QuantizedMatMul, QuantizedConv) + SUPPORTED_ROUND_PBS_OP_PREDECESSOR = { + QuantizedBrevitasQuant: QuantizedRelu, + QuantizedRelu: QuantizedMixingOp, + QuantizedMixingOp: QuantizedBrevitasQuant, + } + + def __init__(self, qmodule: QuantizedModule) -> None: + self._qmodule = qmodule + self._num_ignored_valid_patterns = 0 + + @property + def num_ignored_valid_patterns(self): + """Get the number of optimizable patterns that were ignored. + + Patterns could be ignored since a number of rounding bits was + set manually through the compilation function. + + Returns: + result (int): number of patterns that could be optimized but were not + """ + return self._num_ignored_valid_patterns + + def process(self) -> PatternDict: + """Analyze an ONNX graph and detect Gemm/Conv patterns that can use RoundPBS. + + We want to detect a gemm/conv node whose weights/bias are Brevitas QAT, and whose + input is produced by a Brevitas QAT node that is applied on the output of + another Gemm/conv node. Optionally a Relu can be placed before this input + quantization node. + + Nothing will be done if rounding is already specified. + + Returns: + result (PatternDict): a dictionary containing for each Conv/Gemm node for which + round PBS can be applied based on power-of-two scaling factors + """ + + # The Pattern can be described as follows + # x = Quant(x) -> stored separately in the second member of the tuple in PatternDict + # .... the following ops are stored in the List of the PatternDict + # y = Gemm(x, w, b), with w, b produced by a Brevitas quant node + # +---> This is the node for which roundPBS can be adjusted + # y = Relu(y) + # y = Quant(y) + # z = Gemm(y, w2, b2) -> the output node of the pattern + + self._num_ignored_valid_patterns = 0 + + predecessors = self.compute_op_predecessors() + + valid_paths = self.detect_patterns(predecessors) + + valid_paths = self.process_patterns(valid_paths) + + return valid_paths + + def compute_op_predecessors(self) -> PredecessorsType: + """Compute the predecessors for each QuantizedOp in a QuantizedModule. + + Stores, for each quantized op, a list of quantized ops that produce its + inputs. Currently only the first input of the operations is considered + as it is, usually, the encrypted input. + + Returns: + result (PredecessorsType): a dictionary containing a hierarchy of op + predecessors + """ + + # Initialize the list of predecessors with tensors that are graph inputs + predecessors: PredecessorsType = defaultdict(list) + + for (node_inputs, node_op) in self._qmodule.quant_layers_dict.values(): + # The first input node contains the encrypted data + enc_input_node = node_inputs[0] + + assert_true( + enc_input_node in self._qmodule.quant_layers_dict + or enc_input_node in self._qmodule.ordered_module_input_names + ) + pred = self._qmodule.quant_layers_dict.get(enc_input_node, (None, None)) + # Get the quantized op that produces the current op's input + pred_with_output = (pred[1], enc_input_node) + predecessors[node_op].append(pred_with_output) + return predecessors + + def match_path_pattern( + self, + predecessors: PredecessorsType, + nodes_in_path: List[Optional[QuantizedOp]], + input_producer_of_path: Optional[QuantizedOp], + ) -> bool: + """Determine if a pattern has the structure that makes it viable for roundPBS. + + Args: + predecessors (PredecessorsType): Module predecessor operation list + nodes_in_path (List[QuantizedOp]): list of quantized ops in the pattern + input_producer_of_path (Optional[QuantizedOp]): operation that produces the input + + Returns: + result (bool): whether the pattern can be optimized + """ + + # Test if the list of operations in this pattern has not the right length + if len(nodes_in_path) != 3: + return False + + # If the input of this pattern is produced by a graph input then ignore it + # as graph inputs are not always quantized with QAT. QAT networks + # will have the input to the first gemm/conv op produced by a BrevitasQuant + # op and it will be valid pattern + if input_producer_of_path is None: + return False + + for test_node in nodes_in_path: + # Check the operations in the pattern are chained properly + # for example if the Gemm op is preceded by a quantizer op, etc.. + for pattern_first, pattern_second in self.SUPPORTED_ROUND_PBS_OP_PREDECESSOR.items(): + pred_type = predecessors[test_node][0][0] + if isinstance(test_node, pattern_first) and not isinstance( + pred_type, pattern_second + ): + return False + + return True + + def detect_patterns(self, predecessors: PredecessorsType) -> PatternDict: + """Detect the patterns that can be optimized with roundPBS in the QuantizedModule. + + Args: + predecessors (PredecessorsType): Module predecessor operation list + + Returns: + result (PatternDict): list of optimizable patterns + """ + + valid_paths: PatternDict = {} + + # pylint: disable-next=too-many-nested-blocks + for (_, node_op) in self._qmodule.quant_layers_dict.values(): + # Only work with supported nodes that have a single + # encrypted input (not supporting enc x enc matmul) + if ( + isinstance(node_op, self.SUPPORTED_ROUND_PBS_OPS) + and len(node_op.int_input_names) == 1 + ): + prev_compatible_node_output = list(node_op.int_input_names)[0] + if len(predecessors[node_op]) == 1: + back_node, back_node_output = predecessors[node_op][0] + + # A pattern is a sequence of Gemm/Conv -> Relu -> Quant + # but we also need to store the Quant that quantizes + # the Gemm/Conv's input + nodes_in_path: List[Optional[QuantizedOp]] = [] + integer_node_input_quant: Optional[QuantizedOp] = None + + while back_node_output != prev_compatible_node_output: + assert back_node is not None + nodes_in_path.append(back_node) + assert_true( + back_node in predecessors, + "Power of Two adapter: Error during graph traversal", + ) + # If multiple ops produced this node, the pattern is not matched + + if len(predecessors[back_node]) == 1: + back_node, back_node_output = predecessors[back_node][0] + + # Reached the previous integer node + if back_node_output == prev_compatible_node_output: + # The Gemm/Conv op that produces this integer node is the one + # onto which we apply the roundPBS optimization + nodes_in_path.append(back_node) + list_pred_of_path = predecessors[back_node] + if len(list_pred_of_path) == 1: + integer_node_input_quant = list_pred_of_path[0][0] + + assert isinstance(node_op, QuantizedMixingOp) + if self.match_path_pattern(predecessors, nodes_in_path, integer_node_input_quant): + # If rounding was manually set (usually globally for all layers) + # the do not override the requested number of rounding bits + # but keep statistics for testing purposes + path_start_node = nodes_in_path[-1] + assert isinstance(path_start_node, QuantizedMixingOp) + if path_start_node.rounding_threshold_bits is not None: + self._num_ignored_valid_patterns += 1 + else: + valid_paths[path_start_node] = (nodes_in_path, integer_node_input_quant) + return valid_paths + + def process_patterns(self, valid_paths: PatternDict) -> PatternDict: + """Configure the rounding bits of roundPBS for the optimizable operations. + + Args: + valid_paths (PatternDict): list of optimizable patterns + + Returns: + result (PatternDict): list of patterns actually optimized with roundPBS + """ + + def integer_log2(value: float) -> Tuple[int, bool]: + """Compute the log2 of the value and tests if its an integer. + + Args: + value (float): the value for which to take the log2 + + Returns: + result: The integer log2 and a bool indicating whether + the input value was an integer power of two + """ + log2_value = int(numpy.rint(numpy.log2(value))) + # Check that the integer power of two is close to the original value + # with a small percentage tolerance + if numpy.isclose(numpy.power(2.0, log2_value), value, rtol=0.01): + return log2_value, True + return 0, False + + invalid_paths: List[QuantizedMixingOp] = [] + for path_start_node, (path, path_input_quant) in valid_paths.items(): + # Placeholders + scale_input, scale_output, scale_weights = None, None, None + # Populate placeholders + for node in path: + if isinstance(node, self.SUPPORTED_ROUND_PBS_OPS): + # Get the scale of the input of the Gemm/Conv node + # and of its weights + assert path_input_quant is not None + scale_input = path_input_quant.constant_inputs[1] + scale_weights = node.constant_inputs[1].quantizer.scale + elif isinstance(node, QuantizedBrevitasQuant): + # Get the output scale that will be used to + # compute the compounded scale factor of the + # node that will apply roundPBS + scale_output = node.constant_inputs[1] + + # Check placeholders + assert scale_input is not None, ( + "Power of two adapter: Can not determine input scale of pattern", + ) + assert scale_weights is not None, ( + "Power of two adapter: Can not determine weight scale of pattern", + ) + assert scale_output is not None, ( + "Power of two adapter: Can not determine output scale of pattern", + ) + + # Check if power of two + log2_input, ok_input = integer_log2(scale_input) + log2_weights, ok_weights = integer_log2(scale_weights) + log2_output, ok_output = integer_log2(scale_output) + + # Modify rounding + if ok_input and ok_weights and ok_output: + assert_true( + path_start_node.rounding_threshold_bits is None, + "Power of two adapter: a global rounding configuration was unexpected here", + ) + # The total scale factor is multiplied with the accumulator + # but we want to use a division with a power-of-two (shift right) + # operation to perform the scaling. Thus the + # number of lsbs to round is the negative of the sum of log2 + # of the scale factors + lsbs_to_round = -(log2_input + log2_weights - log2_output) + if lsbs_to_round > 0: + path_start_node.rounding_threshold_bits = lsbs_to_round + path_start_node.lsbs_to_remove = lsbs_to_round + else: + invalid_paths.append(path_start_node) + + for node in invalid_paths: + valid_paths.pop(node) + + return valid_paths diff --git a/src/concrete/ml/quantization/quantized_ops.py b/src/concrete/ml/quantization/quantized_ops.py index 85d33293f..d7397c8bf 100644 --- a/src/concrete/ml/quantization/quantized_ops.py +++ b/src/concrete/ml/quantization/quantized_ops.py @@ -163,6 +163,7 @@ def __init__( f"{self._impl_for_op_named} if weights are provided as the 'b' constant input.", ) + # pylint: disable-next=too-many-statements def q_impl( self, *q_inputs: ONNXOpInputOutputType, @@ -183,7 +184,7 @@ def q_impl( q_input: QuantizedArray = prepared_inputs[0] q_weights: QuantizedArray = prepared_inputs[1] - q_bias: Optional[numpy.ndarray] = ( + q_bias: Optional[QuantizedArray] = ( None if len(prepared_inputs) == 2 or beta == 0 else prepared_inputs[2] ) @@ -263,13 +264,23 @@ def q_impl( # Make mypy happy assert q_bias is not None # Reshape the biases to broadcast them to each neuron - out_zp = out_zp + q_bias / (-m_matmul) + bias_out = q_bias.values if isinstance(q_bias, QuantizedArray) else q_bias + out_zp = out_zp + bias_out / (-m_matmul) # We identify terms in the above equation to determine what # the scale/zero-point of the in-the-clear quantizer should be # to properly de-quantize numpy_q_out return self.make_output_quant_parameters(numpy_q_out, m_matmul, out_zp) + # Integer biases are only supported for Brevitas QAT which sets is_precomputed_qat to true + # These biases are produced by QuantizedBrevitasQuant ops + if q_bias is not None and q_bias.quantizer.is_precomputed_qat: + # Make sure the scale was correctly matching during training + # The bias scale should be the same scale as the one of the weights * inputs + assert q_bias.quantizer.scale is not None + assert numpy.isclose(q_bias.quantizer.scale, m_matmul) + numpy_q_out += q_bias.qvalues + with tag(self.op_instance_name + ".matmul_rounding"): # Apply Concrete rounding (if relevant) numpy_q_out = self.cnp_round(numpy_q_out, calibrate_rounding) @@ -280,9 +291,9 @@ def q_impl( numpy_q_out = m_matmul * numpy_q_out - if q_bias is not None: + if q_bias is not None and not q_bias.quantizer.is_precomputed_qat: # The bias is handled as a float and will be fused - numpy_q_out = numpy_q_out + q_bias + numpy_q_out = numpy_q_out + q_bias.values # Return the float values, so that Concrete can fuse any following float operations # We also keep track of the scaling factor and zero-point, since these will be @@ -595,6 +606,7 @@ def __init__( " standard", ) + # pylint: disable-next=too-many-statements def q_impl( self, *q_inputs: ONNXOpInputOutputType, @@ -628,7 +640,7 @@ def q_impl( ) q_input: QuantizedArray = prepared_inputs[0] q_weights: QuantizedArray = prepared_inputs[1] - q_bias: Optional[numpy.ndarray] = None if len(prepared_inputs) == 2 else prepared_inputs[2] + q_bias: Optional[QuantizedArray] = None if len(prepared_inputs) == 2 else prepared_inputs[2] in_channels = q_input.values.shape[1] weight_channels = q_weights.values.shape[1] @@ -742,13 +754,20 @@ def q_impl( out_zp: Union[int, numpy.ndarray] = sum_weights - final_term if q_bias is not None: # Reshape the biases to broadcast them to each channel - out_zp = out_zp - q_bias.reshape((1, -1, 1, 1)) / m_matmul + out_zp = out_zp - q_bias.values.reshape((1, -1, 1, 1)) / m_matmul # We identify terms in the above equation to determine what # the scale/zero-point of the in-the-clear quantizer should be # to properly de-quantize numpy_q_out return self.make_output_quant_parameters(numpy_q_out, m_matmul, out_zp) + if q_bias is not None and q_bias.quantizer.is_precomputed_qat: + # Make sure the scale was correctly matching during training + # The bias scale should be the same scale as the one of the weights * inputs + assert q_bias.quantizer.scale is not None + assert numpy.isclose(q_bias.quantizer.scale, m_matmul) + numpy_q_out += q_bias.qvalues.reshape((1, -1, 1, 1)) + with tag(self.op_instance_name + ".conv_rounding"): # Apply Concrete rounding (if relevant) numpy_q_out = self.cnp_round(numpy_q_out, calibrate_rounding) @@ -759,10 +778,10 @@ def q_impl( # Rescale from scale=scale_inputs x scale_outputs to output scale numpy_q_out = m_matmul * numpy_q_out - if q_bias is not None: + if q_bias is not None and not q_bias.quantizer.is_precomputed_qat: # The bias addition is handled in float and will be fused into a TLU # Reshape the biases to broadcast them to each channel - numpy_q_out = numpy_q_out + q_bias.reshape((1, -1, 1, 1)) # bias_part + numpy_q_out = numpy_q_out + q_bias.values.reshape((1, -1, 1, 1)) # bias_part # And return as a QuantizedArray initialized from the float data, keeping # track of the quantization parameters diff --git a/src/concrete/ml/quantization/quantizers.py b/src/concrete/ml/quantization/quantizers.py index 73e9d145d..c3317cea4 100644 --- a/src/concrete/ml/quantization/quantizers.py +++ b/src/concrete/ml/quantization/quantizers.py @@ -7,6 +7,7 @@ import numpy +from ..common import utils from ..common.debugging import assert_true from ..common.serialization.dumpers import dump, dumps @@ -745,7 +746,10 @@ def quant(self, values: numpy.ndarray) -> numpy.ndarray: assert self.offset is not None assert self.scale is not None - qvalues = numpy.rint(values / self.scale + self.zero_point) + if utils.QUANT_ROUND_LIKE_ROUND_PBS: + qvalues = numpy.floor(values / self.scale + self.zero_point + 0.5) + else: + qvalues = numpy.rint(values / self.scale + self.zero_point) # Clipping can be performed for PTQ and for precomputed (for now only Brevitas) QAT # (where quantizer parameters are available in ONNX layers). diff --git a/src/concrete/ml/sklearn/qnn.py b/src/concrete/ml/sklearn/qnn.py index b74dab5bb..7e4d94823 100644 --- a/src/concrete/ml/sklearn/qnn.py +++ b/src/concrete/ml/sklearn/qnn.py @@ -31,6 +31,7 @@ "activation_function", "quant_narrow", "quant_signed", + "power_of_two_scaling", ] # skorch's special attribute prefixes, which can be found in: diff --git a/src/concrete/ml/sklearn/qnn_module.py b/src/concrete/ml/sklearn/qnn_module.py index 8710bcaaa..bd65831cc 100644 --- a/src/concrete/ml/sklearn/qnn_module.py +++ b/src/concrete/ml/sklearn/qnn_module.py @@ -5,10 +5,11 @@ import numpy import torch import torch.nn.utils.prune as pruning +from brevitas.quant.scaled_int import Int8ActPerTensorFloat, Int8WeightPerTensorFloat, IntBias from torch import nn from ..common.debugging import assert_true -from ..common.utils import MAX_BITWIDTH_BACKWARD_COMPATIBLE +from ..quantization.qat_quantizers import Int8ActPerTensorPoT, Int8WeightPerTensorPoT class SparseQuantNeuralNetwork(nn.Module): @@ -27,13 +28,15 @@ def __init__( n_layers: int, n_outputs: int, n_hidden_neurons_multiplier: int = 4, - n_w_bits: int = 3, - n_a_bits: int = 3, - n_accum_bits: int = MAX_BITWIDTH_BACKWARD_COMPATIBLE, + n_w_bits: int = 4, + n_a_bits: int = 4, + # No pruning by default as roundPBS keeps the PBS precision low + n_accum_bits: int = 32, n_prune_neurons_percentage: float = 0.0, activation_function: Type = nn.ReLU, quant_narrow: bool = False, quant_signed: bool = True, + power_of_two_scaling: bool = True, # Default to true: use roundPBS to speed up the NNs ): """Sparse Quantized Neural Network constructor. @@ -60,6 +63,8 @@ def __init__( (e.g a 2-bits signed quantization uses [-1, 0, 1] instead of [-2, -1, 0, 1]). quant_signed (bool): Whether this network should quantize the values using signed integers. + power_of_two_scaling (bool): Force quantization scales to be a power of two + to enable inference speed optimizations. Defaults to True Raises: ValueError: If the parameters have invalid values or the computed accumulator bit-width @@ -81,6 +86,8 @@ def __init__( if n_w_bits <= 0 or n_a_bits <= 0: raise ValueError("The weight & activation quantization bit-width cannot be less than 1") + high_input_bitwidth = False # power_of_two_scaling and activation_function is nn.ReLU + for idx in range(n_layers): out_features = ( n_outputs if idx == n_layers - 1 else int(input_dim * n_hidden_neurons_multiplier) @@ -88,10 +95,11 @@ def __init__( quant_name = f"quant{idx}" quantizer = qnn.QuantIdentity( - bit_width=n_a_bits, + bit_width=8 if high_input_bitwidth else n_a_bits, return_quant_tensor=True, narrow_range=quant_narrow, signed=quant_signed, + act_quant=Int8ActPerTensorPoT if power_of_two_scaling else Int8ActPerTensorFloat, ) layer_name = f"fc{idx}" @@ -100,10 +108,13 @@ def __init__( out_features, True, weight_bit_width=n_w_bits, - bias_quant=None, + bias_quant=IntBias if power_of_two_scaling else None, weight_narrow_range=quant_narrow, narrow_range=quant_narrow, signed=quant_signed, + weight_quant=Int8WeightPerTensorPoT + if power_of_two_scaling + else Int8WeightPerTensorFloat, ) self.features.add_module(quant_name, quantizer) diff --git a/tests/quantization/test_quantized_ops.py b/tests/quantization/test_quantized_ops.py index 7431436ef..d3e7a15ee 100644 --- a/tests/quantization/test_quantized_ops.py +++ b/tests/quantization/test_quantized_ops.py @@ -468,13 +468,14 @@ def test_all_gemm_ops( # Quantize the inputs and weights q_inputs = QuantizedArray(n_bits, inputs) q_weights = QuantizedArray(n_bits, weights, is_signed=is_signed) + q_bias = QuantizedArray(n_bits, bias) # 1- Test our QuantizedGemm layer q_gemm = QuantizedGemm( n_bits, OP_DEBUG_NAME + "QuantizedGemm", int_input_names={"0"}, - constant_inputs={"b": q_weights, "c": bias}, + constant_inputs={"b": q_weights, "c": q_bias}, ) q_gemm.produces_graph_output = produces_output @@ -532,7 +533,7 @@ def test_all_gemm_ops( n_bits, OP_DEBUG_NAME + "QuantizedGemm", int_input_names={"0"}, - constant_inputs={"b": q_weights, "c": bias}, + constant_inputs={"b": q_weights, "c": q_bias}, alpha=1, beta=0, ) @@ -670,6 +671,7 @@ def test_identity_op(x, n_bits): ], ) @pytest.mark.parametrize("produces_output", [True, False]) +# pylint: disable-next=too-many-locals def test_quantized_conv(params, n_bits, produces_output, check_r2_score, check_float_arrays_equal): """Test the quantized convolution operator.""" @@ -694,13 +696,14 @@ def test_quantized_conv(params, n_bits, produces_output, check_r2_score, check_f # Create quantized data q_input = QuantizedArray(n_bits, net_input, is_signed=False) q_weights = QuantizedArray(n_bits, weights, is_signed=True) + q_bias = QuantizedArray(n_bits, biases) # Create the operator, specifying weights & biases as constants q_op = QuantizedConv( n_bits, OP_DEBUG_NAME + "QuantizedConv", int_input_names={"0"}, - constant_inputs={1: q_weights, 2: biases}, + constant_inputs={1: q_weights, 2: q_bias}, strides=strides, pads=pads, kernel_shape=(weights.shape[2], weights.shape[3]), diff --git a/tests/sklearn/test_dump_onnx.py b/tests/sklearn/test_dump_onnx.py index 2d51a6dab..00b22c4a9 100644 --- a/tests/sklearn/test_dump_onnx.py +++ b/tests/sklearn/test_dump_onnx.py @@ -2,6 +2,7 @@ import warnings +from functools import partial import numpy import onnx @@ -11,6 +12,7 @@ from concrete.ml.common.utils import is_model_class_in_a_list from concrete.ml.pytest.utils import get_model_name, sklearn_models_and_datasets from concrete.ml.sklearn import get_sklearn_tree_models +from concrete.ml.sklearn.qnn import NeuralNetClassifier, NeuralNetRegressor # Remark that the dump tests for torch module is directly done in test_compile_torch.py @@ -91,6 +93,29 @@ def test_dump( if parameters.get("n_classes", 2) != 2 and model_name in ["LinearSVC", "LogisticRegression"]: return + if model_name == "NeuralNetClassifier": + model_class = partial( + NeuralNetClassifier, + module__n_layers=3, + module__power_of_two_scaling=False, + max_epochs=1, + verbose=0, + callbacks="disable", + ) + elif model_name == "NeuralNetRegressor": + model_class = partial( + NeuralNetRegressor, + module__n_layers=3, + module__n_w_bits=2, + module__n_a_bits=2, + module__n_accum_bits=7, # Stay with 7 bits for test exec time + module__n_hidden_neurons_multiplier=1, + module__power_of_two_scaling=False, + max_epochs=1, + verbose=0, + callbacks="disable", + ) + n_classes = parameters.get("n_classes", 2) # Ignore long lines here diff --git a/tests/sklearn/test_qnn.py b/tests/sklearn/test_qnn.py index 1404dedb1..c0495db84 100644 --- a/tests/sklearn/test_qnn.py +++ b/tests/sklearn/test_qnn.py @@ -11,11 +11,14 @@ from sklearn.preprocessing import StandardScaler from torch import nn +from concrete.ml.common import utils from concrete.ml.common.utils import ( MAX_BITWIDTH_BACKWARD_COMPATIBLE, is_classifier_or_partial_classifier, is_regressor_or_partial_regressor, ) +from concrete.ml.quantization.base_quantized_op import QuantizedMixingOp +from concrete.ml.quantization.post_training import PowerOfTwoScalingRoundPBSAdapter from concrete.ml.sklearn import get_sklearn_neural_net_models from concrete.ml.sklearn.qnn import NeuralNetClassifier, NeuralNetRegressor from concrete.ml.sklearn.qnn_module import SparseQuantNeuralNetwork @@ -186,6 +189,7 @@ def test_compile_and_calib( "module__n_a_bits": 2, "module__n_accum_bits": 5, "module__activation_function": activation_function, + "module__power_of_two_scaling": False, "max_epochs": 10, "verbose": 0, } @@ -485,3 +489,139 @@ def test_serialization_unsupported_parameters( with pytest.raises(expected_error, match=expected_message): model.dumps() + + +@pytest.mark.parametrize( + "activation_function", + [ + pytest.param(nn.ReLU), + pytest.param(nn.Sigmoid), + ], +) +@pytest.mark.parametrize("num_layers", [2, 4]) +@pytest.mark.parametrize("model_class", [NeuralNetClassifier]) +@pytest.mark.parametrize("use_power_of_two_scaling", [True, False]) +def test_power_of_two_scaling( + activation_function, + model_class, + num_layers, + load_data, + use_power_of_two_scaling, + default_configuration, +): + """Check that built-in neural networks can use roundPBS optimization.""" + + n_features = 10 + + # Get the data-set. The data generation is seeded in load_data. + x, y = load_data( + model_class, + n_samples=1000, + n_features=n_features, + n_redundant=0, + n_repeated=0, + n_informative=n_features, + n_classes=2, + class_sep=2, + ) + + # Perform a classic test-train split (deterministic by fixing the seed) + x_train, x_test, y_train, _ = train_test_split( + x, + y, + test_size=0.25, + random_state=numpy.random.randint(0, 2**15), + ) + + # Compute mean/stdev on training set and normalize both train and test sets with them + # Optimization algorithms for Neural networks work well on 0-centered inputs + normalizer = StandardScaler() + x_train = normalizer.fit_transform(x_train) + x_test = normalizer.transform(x_test) + + # Configure a minimal neural network and train it quickly + params = { + "module__n_layers": num_layers, + "module__n_w_bits": 4, + "module__n_a_bits": 4, + "module__n_accum_bits": 32, + "module__activation_function": activation_function, + "module__power_of_two_scaling": use_power_of_two_scaling, + "max_epochs": 2, + "verbose": 0, + } + + model = model_class(**params) + + utils.QUANT_ROUND_LIKE_ROUND_PBS = True + + # Train normally. This also converts the torch NN to a QuantizedModule + # and thus applies the PowerOfTwoScalingRoundPBSAdapter that + # detects and applies round PBS optimization + model.fit(x_train, y_train) + + # Count the number of patterns that were optimized with roundPBS + num_round_pbs_layers = 0 + for (_, node_op) in model.quantized_module_.quant_layers_dict.values(): + if isinstance(node_op, QuantizedMixingOp): + num_round_pbs_layers += 1 if node_op.rounding_threshold_bits is not None else 0 + assert node_op.rounding_threshold_bits == node_op.lsbs_to_remove + + # Apply the PowerOfTwoScalingRoundPBSAdapter again. The second time + # the adapter will ignore already optimized patterns but report them + # as ignored. + adapter = PowerOfTwoScalingRoundPBSAdapter(model.quantized_module_) + round_pbs_patterns = adapter.process() + + # The power-of-two optimization will only work + # when Relu activations are used and scaling factors are forced to be 2**s + if activation_function is nn.ReLU and use_power_of_two_scaling: + assert ( + len(round_pbs_patterns) == 0 + ), "Expected number of round PBS optimized patterns was not matched" + assert ( + adapter.num_ignored_valid_patterns == num_layers - 1 + ), "Expected number of ignored round PBS optimizable patterns was not matched" + + y_pred_clear_round = model.predict(x_test, fhe="disable") + + # Compile the model to ensure rounding is taken into account + # in compilation + model.compile( + x_train, + configuration=default_configuration, + ) + + # Compute the results with simulation, which uses the actual + # lookup tables. + y_pred_sim_round = model.predict(x_test, fhe="simulate") + + # Ensure rounding was compiled in the circuit + # the number of rounding nodes should be equal + num_rounding_mlir = model.fhe_circuit.mlir.count(".round") + + assert ( + num_rounding_mlir == num_layers - 1 + ), "Power-of-to adapter: Rounding nodes not found in MLIR" + + # Remove rounding in the network to perform inference without the optimization. + # We expect a network that was optimized with the power-of-two adapter + # to be exactly correct to the non-optimized one + for (_, node_op) in model.quantized_module_.quant_layers_dict.values(): + if isinstance(node_op, QuantizedMixingOp): + node_op.rounding_threshold_bits = None + node_op.lsbs_to_remove = None + + # Predict with the unoptimized network + y_pred_clear_no_round = model.predict(x_test, fhe="disable") + + # Compare the result with the optimized network with and without + # rounding. Tolerate at most 1 error + assert numpy.sum(y_pred_clear_round != y_pred_clear_no_round) <= 1 + assert numpy.sum(y_pred_sim_round != y_pred_clear_no_round) <= 1 + else: + # If the optimization is not expected to work, check that no patterns were + # detected + assert ( + adapter.num_ignored_valid_patterns == 0 + ), "Optimization performed but not expected for round PBS optimizable patterns" diff --git a/tests/torch/test_brevitas_qat.py b/tests/torch/test_brevitas_qat.py index ba60d9273..72e46d09a 100644 --- a/tests/torch/test_brevitas_qat.py +++ b/tests/torch/test_brevitas_qat.py @@ -1,48 +1,100 @@ """Tests with brevitas quantization aware training.""" +from typing import Optional + import brevitas.nn as qnn import numpy import pytest import torch import torch.utils +from brevitas.quant import Int8ActPerTensorFloat, Int8WeightPerTensorFloat +from brevitas.quant.scaled_int import IntBias from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from torch import nn from torch.utils.data import DataLoader, TensorDataset +from concrete.ml.common import utils from concrete.ml.common.utils import ( is_classifier_or_partial_classifier, is_regressor_or_partial_regressor, ) -from concrete.ml.pytest.torch_models import NetWithConstantsFoldedBeforeOps, TinyQATCNN +from concrete.ml.pytest.torch_models import ( + NetWithConstantsFoldedBeforeOps, + QuantCustomModel, + TinyQATCNN, +) +from concrete.ml.quantization.base_quantized_op import QuantizedMixingOp +from concrete.ml.quantization.post_training import PowerOfTwoScalingRoundPBSAdapter +from concrete.ml.quantization.qat_quantizers import Int8ActPerTensorPoT, Int8WeightPerTensorPoT from concrete.ml.sklearn import get_sklearn_neural_net_models from concrete.ml.sklearn.qnn_module import SparseQuantNeuralNetwork from concrete.ml.torch.compile import compile_brevitas_qat_model -# This test is a known flaky -# FIXME: https://github.com/zama-ai/concrete-ml-internal/issues/3933 -@pytest.mark.flaky -@pytest.mark.parametrize("qat_bits", [3]) -@pytest.mark.parametrize("signed, narrow", [(True, False), (True, True), (False, False)]) -def test_brevitas_tinymnist_cnn( - qat_bits, - signed, - narrow, - default_configuration, - check_graph_input_has_no_tlu, - check_graph_output_has_no_tlu, - check_is_good_execution_for_cml_vs_circuit, -): # pylint: disable=too-many-statements, too-many-locals - """Train, execute and test a QAT CNN on a small version of MNIST.""" +def forward_test_torch(net, test_loader): + """Test the network: measure accuracy on the test set. + + Args: + test_loader: the test loader + + Returns: + res: the number of correctly classified test examples + + """ + + # Freeze normalization layers + net.eval() + + all_y_pred = numpy.zeros((len(test_loader)), dtype=numpy.int64) + all_targets = numpy.zeros((len(test_loader)), dtype=numpy.int64) + + # Iterate over the batches + idx = 0 + for data, target in test_loader: + # Accumulate the ground truth labels + endidx = idx + target.shape[0] + all_targets[idx:endidx] = target.numpy() + + # Run forward and get the raw predictions first + raw_pred = net(data).detach().numpy() + + # Get the predicted class id, handle NaNs + if numpy.any(numpy.isnan(raw_pred)): + output = -1 # pragma: no cover + else: + output = raw_pred.argmax(1) + + all_y_pred[idx:endidx] = output + + idx += target.shape[0] + + # Print out the accuracy as a percentage + n_correct = numpy.sum(all_targets == all_y_pred) + return n_correct + +def train_brevitas_network_tinymnist(is_cnn, qat_bits, signed, narrow, pot_scaling): + """Train a QAT network on tiny mnist. + + Args: + is_cnn (bool): whether to train a CNN or a FC network + qat_bits (int): quantization bits + signed (bool): use signed quantization + narrow (bool): use brevitas narrow range quantization + pot_scaling (int): use power of two scaling quantization + + Returns: + result (Tuple): the network, the dataset and the test data loader + """ # And some helpers for visualization. x_all, y_all = load_digits(return_X_y=True) # The sklearn Digits data-set, though it contains digit images, keeps these images in vectors # so we need to reshape them to 2D first. The images are 8x8 px in size and monochrome - x_all = numpy.expand_dims(x_all.reshape((-1, 8, 8)), 1) + if is_cnn: + x_all = numpy.expand_dims(x_all.reshape((-1, 8, 8)), 1) x_train, x_test, y_train, y_test = train_test_split( x_all, y_all, test_size=0.25, shuffle=True, random_state=numpy.random.randint(0, 2**15) @@ -77,7 +129,19 @@ def train_one_epoch(net, optimizer, train_loader): while not trained_ok: # Create the tiny CNN module with 10 output classes - net = TinyQATCNN(10, qat_bits, 4 if qat_bits <= 3 else 20, signed, narrow) + if is_cnn: + net = TinyQATCNN(10, qat_bits, 4 if qat_bits <= 3 else 20, signed, narrow, pot_scaling) + else: + if pot_scaling: + act_quant = Int8ActPerTensorPoT + weight_quant = Int8WeightPerTensorPoT + bias_quant = IntBias + else: + act_quant = Int8ActPerTensorFloat + weight_quant = Int8WeightPerTensorFloat + bias_quant = None + + net = QuantCustomModel(64, 10, 100, qat_bits, act_quant, weight_quant, bias_quant) # Train a single epoch to have a fast test, accuracy should still be the same for both # FHE simulation and torch @@ -90,14 +154,38 @@ def train_one_epoch(net, optimizer, train_loader): train_one_epoch(net, optimizer, train_dataloader) # Finally, disable pruning (sets the pruned weights to 0) - net.toggle_pruning(False) + if hasattr(net, "toggle_pruning"): + net.toggle_pruning(False) - torch_correct = net.test_torch(test_dataloader) + torch_correct = forward_test_torch(net, test_dataloader) # If number of correct results was zero, training failed and there were NaNs in the weights # Retrain while training is bad trained_ok = torch_correct > 0 + return net, x_all, test_dataloader + + +# This test is a known flaky +# FIXME: https://github.com/zama-ai/concrete-ml-internal/issues/3933 +@pytest.mark.flaky +@pytest.mark.parametrize("qat_bits", [3]) +@pytest.mark.parametrize("signed, narrow", [(True, False), (True, True), (False, False)]) +def test_brevitas_tinymnist_cnn( + qat_bits, + signed, + narrow, + default_configuration, + check_graph_input_has_no_tlu, + check_graph_output_has_no_tlu, + check_is_good_execution_for_cml_vs_circuit, +): # pylint: disable=too-many-statements, too-many-locals + """Train, execute and test a QAT CNN on a small version of MNIST.""" + + net, x_all, test_dataloader = train_brevitas_network_tinymnist( + True, qat_bits, signed, narrow, False + ) + def test_with_concrete(quantized_module, test_loader, use_fhe_simulation): """Test a neural network that is quantized and compiled with Concrete ML.""" @@ -149,7 +237,7 @@ def test_with_concrete(quantized_module, test_loader, use_fhe_simulation): # FIXME: https://github.com/zama-ai/concrete-ml-internal/issues/2550 # assert abs(fhe_simulation_correct - torch_correct) <= numpy.ceil(0.01 * len(y_test)) - assert fhe_s_correct.shape == torch_correct.shape + assert fhe_s_correct >= 0 check_graph_input_has_no_tlu(q_module_simulated.fhe_circuit.graph) check_graph_output_has_no_tlu(q_module_simulated.fhe_circuit.graph) @@ -405,3 +493,107 @@ def test_brevitas_constant_folding(default_configuration): torch_inputset=data, configuration=default_configuration, ) + + +@pytest.mark.parametrize("manual_rounding", [None, 3]) +@pytest.mark.parametrize("power_of_two", [True, False]) +@pytest.mark.parametrize("n_bits", [4]) +@pytest.mark.parametrize("is_cnn", [True, False]) +def test_brevitas_power_of_two( + default_configuration, + manual_rounding: Optional[int], + power_of_two: bool, + n_bits: int, + is_cnn: bool, +): + """Test a custom QAT network that uses power-of-two scaling. + + Test whether a network using power-of-two scaling quantization is imported + correctly and roundPBS is used. Test that the Concrete ML does not override + the user's round PBS configuration. + """ + + net, x_all, _ = train_brevitas_network_tinymnist(is_cnn, n_bits, True, False, power_of_two) + + utils.QUANT_ROUND_LIKE_ROUND_PBS = True + + # If rounding threshold is set -> nothing happens + # If Quantizer is not setup -> nothing happens + quantized_module = compile_brevitas_qat_model( + net.to("cpu"), + torch_inputset=x_all, + configuration=default_configuration, + rounding_threshold_bits=manual_rounding, + ) + + pot_should_be_applied = not manual_rounding and power_of_two + # Count the number of patterns that were optimized with roundPBS + num_round_pbs_layers = 0 + for (_, node_op) in quantized_module.quant_layers_dict.values(): + if isinstance(node_op, QuantizedMixingOp): + num_round_pbs_layers += 1 if node_op.rounding_threshold_bits is not None else 0 + if pot_should_be_applied: + assert node_op.rounding_threshold_bits == node_op.lsbs_to_remove + elif manual_rounding: + # If manual rounding was set, LSBs_to_remove must be equal + # to the accumulator size minus the requested rounding_threshold_bits + assert node_op.rounding_threshold_bits == manual_rounding + assert node_op.produces_graph_output or node_op.lsbs_to_remove is not None + + # The power-of-two optimization will only work + # when Relu activations are used and scaling factors are forced to be 2**s + if not pot_should_be_applied: + return + + # Apply the PowerOfTwoScalingRoundPBSAdapter again. The second time + # the adapter will ignore already optimized patterns but report them + # as ignored. + adapter = PowerOfTwoScalingRoundPBSAdapter(quantized_module) + round_pbs_patterns = adapter.process() + + assert ( + len(round_pbs_patterns) == 0 + ), "Expected number of round PBS optimized patterns was not matched" + # 3 layers + assert ( + adapter.num_ignored_valid_patterns == 3 - 1 + ), "Expected number of ignored round PBS optimizable patterns was not matched" + + x_test = x_all[numpy.random.choice(len(x_all), 100), ::] + + x_test_q = quantized_module.quantize_input(x_test) + + y_pred_clear_round = numpy.argmax( + quantized_module.quantized_forward(x_test_q, fhe="disable"), axis=1 + ) + + # Compute the results with simulation, which uses the actual + # lookup tables. + y_pred_sim_round = numpy.argmax( + quantized_module.quantized_forward(x_test_q, fhe="simulate"), axis=1 + ) + + # Ensure rounding was compiled in the circuit + # the number of rounding nodes should be equal + num_rounding_mlir = quantized_module.fhe_circuit.mlir.count(".round") + + assert num_rounding_mlir == 2, "Power-of-to adapter: Rounding nodes not found in MLIR" + + # Remove rounding in the network to perform inference without the optimization. + # We expect a network that was optimized with the power-of-two adapter + # to be exactly correct to the non-optimized one + for (_, node_op) in quantized_module.quant_layers_dict.values(): + if isinstance(node_op, QuantizedMixingOp): + node_op.rounding_threshold_bits = None + node_op.lsbs_to_remove = None + + # Predict with the unoptimized network + y_pred_clear_no_round = numpy.argmax( + quantized_module.quantized_forward(x_test_q, fhe="disable"), axis=1 + ) + + # # Compare the result with the optimized network and without + # # they should be equal + + assert numpy.sum(y_pred_sim_round != y_pred_clear_round) == 0 + assert numpy.sum(y_pred_clear_round != y_pred_clear_no_round) == 0 diff --git a/tests/torch/test_compile_torch.py b/tests/torch/test_compile_torch.py index 7db365c05..be5f50af6 100644 --- a/tests/torch/test_compile_torch.py +++ b/tests/torch/test_compile_torch.py @@ -1224,7 +1224,7 @@ def test_compilation_functions_check_model_types(default_configuration): configuration=default_configuration, ) - torch_model_qat = TinyQATCNN(5, 4, 10, True, False) + torch_model_qat = TinyQATCNN(5, 4, 10, True, False, False) with pytest.raises( AssertionError, match=".*must be imported using compile_brevitas_qat_model.*" ): diff --git a/use_case_examples/llm/utility_functions.py b/use_case_examples/llm/utility_functions.py index 0d9ccadce..46d479f15 100644 --- a/use_case_examples/llm/utility_functions.py +++ b/use_case_examples/llm/utility_functions.py @@ -32,7 +32,7 @@ def max_fhe_relu(q_x, axis=-1, keepdims=True): if keepdims: shape = list(result.shape) shape.insert(axis, 1) - result = result.reshape(shape) + result = result.reshape(tuple(shape)) return result