From bbbf8efc0be6894a3dddbd4901d3c9ca8b117dcf Mon Sep 17 00:00:00 2001 From: Bourgerie Quentin Date: Thu, 3 Oct 2024 11:17:12 +0200 Subject: [PATCH 1/4] test(frontend-python): Remove sha256 example before refactoring --- docs/tutorials/see-all-tutorials.md | 1 - .../examples/sha256/sha256.ipynb | 840 ------------------ 2 files changed, 841 deletions(-) delete mode 100644 frontends/concrete-python/examples/sha256/sha256.ipynb diff --git a/docs/tutorials/see-all-tutorials.md b/docs/tutorials/see-all-tutorials.md index 934f00cdd..13c300abc 100644 --- a/docs/tutorials/see-all-tutorials.md +++ b/docs/tutorials/see-all-tutorials.md @@ -11,7 +11,6 @@ * [Floating points](../../frontends/concrete-python/examples/floating_point/floating_point.ipynb) * [Key value database](../../frontends/concrete-python/examples/key_value_database/key_value_database.ipynb) -* [SHA-256 ](../../frontends/concrete-python/examples/sha256/sha256.ipynb) * [Game of Life](../../frontends/concrete-python/examples/game_of_life/README.md) * [XOR distance](../../frontends/concrete-python/examples/xor_distance/README.md) * [SHA1 with Modules](../../frontends/concrete-python/examples/sha1/README.md) diff --git a/frontends/concrete-python/examples/sha256/sha256.ipynb b/frontends/concrete-python/examples/sha256/sha256.ipynb deleted file mode 100644 index d373b04b2..000000000 --- a/frontends/concrete-python/examples/sha256/sha256.ipynb +++ /dev/null @@ -1,840 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "_FTzVxUkjQno" - }, - "source": [ - "# SHA-256 Implementation Using Concrete\n", - "\n", - "In this tutorial, we will explore the implementation of SHA-256, a widely used hashing algorithm, using concrete-python. Details about the algorithm can be found [here](https://en.wikipedia.org/wiki/SHA-2).\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "zXozpJvmcBH1", - "outputId": "79dfc00b-10cc-4ffd-d4b9-a10f18d8d01e" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", - "Requirement already satisfied: concrete-python in /usr/local/lib/python3.10/dist-packages (1.0.0)\n", - "Requirement already satisfied: numpy>=1.23 in /usr/local/lib/python3.10/dist-packages (from concrete-python) (1.24.3)\n", - "Requirement already satisfied: scipy>=1.10 in /usr/local/lib/python3.10/dist-packages (from concrete-python) (1.10.1)\n", - "Requirement already satisfied: torch>=1.13 in /usr/local/lib/python3.10/dist-packages (from concrete-python) (2.0.0+cu118)\n", - "Requirement already satisfied: networkx>=2.6 in /usr/local/lib/python3.10/dist-packages (from concrete-python) (3.1)\n", - "Requirement already satisfied: typing-extensions in /usr/local/lib/python3.10/dist-packages (from torch>=1.13->concrete-python) (4.5.0)\n", - "Requirement already satisfied: triton==2.0.0 in /usr/local/lib/python3.10/dist-packages (from torch>=1.13->concrete-python) (2.0.0)\n", - "Requirement already satisfied: sympy in /usr/local/lib/python3.10/dist-packages (from torch>=1.13->concrete-python) (1.11.1)\n", - "Requirement already satisfied: jinja2 in /usr/local/lib/python3.10/dist-packages (from torch>=1.13->concrete-python) (3.1.2)\n", - "Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from torch>=1.13->concrete-python) (3.12.0)\n", - "Requirement already satisfied: cmake in /usr/local/lib/python3.10/dist-packages (from triton==2.0.0->torch>=1.13->concrete-python) (3.25.2)\n", - "Requirement already satisfied: lit in /usr/local/lib/python3.10/dist-packages (from triton==2.0.0->torch>=1.13->concrete-python) (16.0.2)\n", - "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from jinja2->torch>=1.13->concrete-python) (2.1.2)\n", - "Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.10/dist-packages (from sympy->torch>=1.13->concrete-python) (1.3.0)\n" - ] - } - ], - "source": [ - "# Uncomment this line to install dependency\n", - "# ! pip install concrete-python\n", - "\n", - "# Required libraries\n", - "from concrete import fhe\n", - "import platform\n", - "import numpy as np" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "oCfjYazikbm_" - }, - "source": [ - "## Data Representation\n", - "As mentioned in the wiki page, all variables are $32$-bit unsigned integers. Additions should be calculated modulo $2^{32}$.\n", - "\n", - "While addition of 32-bit numbers are possible in the library, any other operations such modulizing, rotations, and bitwise operations are currently not possible. These operations require a lookup table with 32-bit inputs, but as of writing this tutorial, concrete-python supports up to 16-bit lookup tables. Higher precision lookup tables is still a research challenge in the homomorphic world and such a table would be dificult to compile and store at this moment.\n", - "\n", - "Thus, we need to break all the variables to **chunks** and work at the chunk level. Throughtout the code, *WIDTH* refers to the bitwidth of a chunk, and *NUM_CHUNKS* shows the number of chunks we need to represent a 32-bit data. These parameters are set at the begining of the code. We vary these parameters to see the impact of the *WIDTH* on the performance of the compiler and the circuit.\n", - "\n", - "![chunks.jpg](data:image/jpeg;base64,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25tTiVNsyIx2k9cYPYmi4+MHw+tfi1a/A6fUivjO80ltcisvIlKtp6zNAZfOCeSD5iMNpfdxnGMGgD02iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//Q/fyiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAGbcnPHNfjZ4U+I3/DJdp+2d4FEv2IeFrtvFugLnaufEtuFgWLvsiuPIjOOhPrmv2Wr8rP2xf2QfiJ8Zv2m/hz428HWom8GavDaaV44/fQRJ/Zul6jFqEW+KR1eVpTlVMauVKLuwp5APGv2NfDA/Yj+LFz4C8XzSWuk+PfhvY+MLkykkQ6to6OdRgQt0KRySSN22hfQAeT+K/DOp2f/AASP8efErxHHs174qeIY/FV9u+8Xvtat0i5IyVaKJXH++T3r7m/4KP8A7N3xY+OnhDwjrXwJtvtPjHQrm/06VftENrnRtcs3tb8M9w6IQVCKVBztZtozXY/th/s9+KvFn7D99+z38F9KGr6np9podjp9p50Frvg0y5tsnzLh44lxFEW5cZxgZOAQDjPHAA/4KTfBb0/4QbVsf+ReazP+CoPwx8Kw/AC++P8AotvDovj74d32m6jpesW4EF0He9ggaNpFwXGHDKGzhlGMAnOh+0L4I/aL0D9qj4cfHv4O/DMfEWy8M+GbvSru2Os2OkbZrl3GPMumLHarbsqjA9MiuO8efCr9sr9tLUdH8D/HXwtpvwe+FFjfw32qafbapFquqaqtuwdLfzrYmJVyDzhNpIfDlVWgD3v9svWJvEX7BPj3xDdReTNqnhiK6dP7jTiJyvPoTivoX9n4H/hQ/wANz/1LWjf+kUVcT+1v8PPE3xH/AGYvH3w1+Humrfa1q+lG0sLNZIrdXcMm1A8rRxoMDjcwFen/AAe0PVPC3wl8E+GNdg+zalpGh6bZ3UW5X8ue3tY45F3oWVsMCMqSD1BI5oA/Hj9iL9pnxP8ACr4XeIvCmlfBnxp45t18U61P/aWhWKXFmTJMMx72dTvXHzDHevS/hh8VtW+L3/BS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X7aH/P18P/8AwH1X/wCO0eV+2h/z9fD/AP8AAfVf/jtAH0/RXzB5X7aH/P18P/8AwH1X/wCO0eV+2h/z9fD/AP8AAfVf/jtAH0/Sc18w+V+2h/z9fD//AMB9V/8AjtHlftof8/Xw/wD/AAH1X/47QB9Pc0tfMHlftof8/Xw//wDAfVf/AI7R5X7aH/P18P8A/wAB9V/+O0AfTuKdXzB5X7aH/P18P/8AwH1X/wCO0eV+2h/z9fD/AP8AAfVf/jtAH0/RXzB5X7aH/P18P/8AwH1X/wCO0eV+2h/z9fD/AP8AAfVf/jtAH0/RXzB5X7aH/P18P/8AwH1X/wCO0eV+2h/z9fD/AP8AAfVf/jtAH0/RXzB5X7aH/P18P/8AwH1X/wCO0eV+2h/z9fD/AP8AAfVf/jtAH0/RXzB5X7aH/P18P/8AwH1X/wCO0eV+2h/z9fD/AP8AAfVf/jtAH0/SYr5h8r9tD/n6+H//AID6r/8AHaPK/bQ/5+vh/wD+A+q//HaAPp/NGa+X/J/bP/5+fh//AN+NV/8AjtHk/tn/APPz8P8A/vxqv/x2gD//1/38ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9D9/KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBM+tRxzwygtC4kAJBKkHkdRTipzX88H7D/wAXPGX7M3iCTxZ44uDL8Fvil4q1TR5rkkiPQ9et5sQzSk/KsVzGQrtxwpY4EPzgH9DvnxeYId43kbgueceuKI54pgWhcSAHBKkEZH0r859fOf8Agql4aOeP+FWTf+nWfH9aZ/wS9wP2d9b7H/hL9e/9HLQB+jfnxeb5O8eZjdtzzjpn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)\n", - "\n", - "Figure 1: Shows a break down of 32 bit of data into 4 chunks of 8 bit. This is not the only way to chunk the input." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "id": "yaz8cNzjQ1UW" - }, - "outputs": [], - "source": [ - "# Bitwidth of each chunk and number of chunks in each 32-bit number.\n", - "WIDTH, NUM_CHUNKS = 4, 8\n", - "\n", - "## Some other valid parameter sets\n", - "# WIDTH, NUM_CHUNKS= 8, 4\n", - "# WIDTH, NUM_CHUNKS= 2, 16\n", - "\n", - "assert WIDTH * NUM_CHUNKS == 32\n", - "\n", - "\n", - "def break_down_data(data, data_size):\n", - " all_chunks = [\n", - " [(x >> i * WIDTH) % (2**WIDTH) for i in range(data_size // WIDTH)[::-1]] for x in data\n", - " ]\n", - " return all_chunks\n", - "\n", - "\n", - "def reshape_data(data):\n", - " return np.array(data).reshape(-1, NUM_CHUNKS)\n", - "\n", - "\n", - "def chunks_to_uint32(chunks):\n", - " return int(sum([2 ** ((NUM_CHUNKS - 1 - i) * WIDTH) * x for i, x in enumerate(chunks)]))\n", - "\n", - "\n", - "def chunks_to_hexarray(chunks):\n", - " hexes = [hex(chunks_to_uint32(word))[2:] for word in chunks]\n", - " hexes = [\n", - " \"0\" * (8 - len(y)) + y for y in hexes\n", - " ] # Appending leadning zero to the ones that are less than 8 characters TODO: write better\n", - " result = \"\".join(hexes)\n", - " return result" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "u7pA-B3As9u4" - }, - "source": [ - "### Creating Chunks\n", - "There are two list of constants in the algorithm, K and H. Before executing the algorithm, we need to break them to chunks using `split_to_chunks` function.\n", - "\n", - "\n", - "The input of the algorithm is arbitrary bytes. We might need to break each byte to smaller chunks based on the value of *WIDTH* after padding the data as per instructed by the algorithm. `break_down_data` function returns a numpy array of shape (48,NUM_CHUNKS)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "b8rlvVf42CIa" - }, - "source": [ - "## Operations\n", - "Now that the data is stores as chunks, we must modify all operations we need to work at the level of chunks. In this section we explain how we implemented the required operations. The main three category of operations that we need to implement SHA-256 are:\n", - "\n", - "* Bitwise operations (AND, OR, XOR, NEGATE)\n", - "* Shifts and Rotations\n", - "* Modular Addition " - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "zlM1RN-NnjDn" - }, - "source": [ - "### Bitwise Operations\n", - "Bitwise operations are easily implemented in concrete-numpy. A bitwise operation over a 32-bit number is equivalent to the same operation over the chunks." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "CxCwJOao2KCt" - }, - "source": [ - "### Rotation and Shifts\n", - "To understand how rotations work, consider a small example with 4 chunks of width 4, representing a 16-bit number, as shown in Figure 1. Most significant bits are located at index 0. So a 16-bit number will be `[[chunk_0], [chunk_1], [chunk_2], [chunk_3]]` with WIDTH=4. There are two possible scenario for rotations:\n", - "\n", - "1. Any rotation by a multiple of WIDTH (in this case, 4) will result in rotating the array of chunks. For example, right rotate(4) will be `[[chunk_3], [chunk_0], [chunk_1], [chunk_2]]`.\n", - "\n", - "2. For rotations less than WIDTH, for example `y`, we break every chunk into two parts of bitlength, `WIDTH-y` and `y`. We need to add the low `y`-bits of each chunk with the high `WIDTH-y` bits of the next chunk. Figure 2 illustrated this process. We leverage two lookup tables to extract the two segments of each chunk.\n", - "\n", - "\n", - "3. Rotations by other amounts are broken into the two steps described above.\n", - "\n", - "![Rotation.jpg](data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBMRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAAqACAAQAAAABAAACfaADAAQAAAABAAADDgAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/+ICQElDQ19QUk9GSUxFAAEBAAACMEFEQkUCEAAAbW50clJHQiBYWVogB9AACAALABMAMwA7YWNzcEFQUEwAAAAAbm9uZQAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1BREJFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKY3BydAAAAPwAAAAyZGVzYwAAATAAAABrd3RwdAAAAZwAAAAUYmtwdAAAAbAAAAAUclRSQwAAAcQAAAAOZ1RSQwAAAdQAAAAOYlRSQwAAAeQAAAAOclhZWgAAAfQAAAAUZ1hZWgAAAggAAAAUYlhZWgAAAhwAAAAUdGV4dAAAAABDb3B5cmlnaHQgMjAwMCBBZG9iZSBTeXN0ZW1zIEluY29ycG9yYXRlZAAAAGRlc2MAAAAAAAAAEUFkb2JlIFJHQiAoMTk5OCkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAGN1cnYAAAAAAAAAAQIzAABYWVogAAAAAAAAnBgAAE+lAAAE/FhZWiAAAAAAAAA0jQAAoCwAAA+VWFlaIAAAAAAAACYxAAAQLwAAvpz/wAARCAMOAn0DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwACAgICAgIDAgIDBQMDAwUGBQUFBQYIBgYGBgYICggICAgICAoKCgoKCgoKDAwMDAwMDg4ODg4PDw8PDw8PDw8P/9sAQwECAgIEBAQHBAQHEAsJCxAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ/90ABAAo/9oADAMBAAIRAxEAPwD9/KZuOcU+qGo6fZ6pYXOmX8QmtbyN4ZUPR0kBVgfqDigC5uJPHSjcQAa/LX/gnJ4nX4afCb4r/B3xtetv+CXibVreaSQ5ZdM+aaOX6M8c7jtjke3yh/wTb+IPxA0/9prV9c+IO6Gy/aO0rUvE2mhnLbrqw1K5Hl4bABWIXDgDjyyhHB4AP35BJOP/AK9IWJ6GvzB+E4m+L3/BSz4s/EJ3M+jfCXQrHwxZHPyLeXn72YgdMowuUP8AvDNfC3gu6uv+HNHxLnaVzJ/bmAxY7sf2vYDG6gD+ijcce9Lkmvyd8F/8Ewv2c/EXwx8OeItEvPEPhrxHqWlWV4NRsNWmEkV1NAkhkVJN6YDnOBjjgEcEZf7NMnij41ad8YP2A/2ttRm8WX/gOex26pHPJDdahpvmpcQM8wxIWR44X3sS7LIFYnaSQD9d8mm5OT0r8AfjL+wt+z94M/bG+Bnwb0Ky1KLwx46g1l9UifUbh5JGsrZ5IdkhbcnzAZwea+g/2tv2YPhP+zB+wp8aIPhHbXlguvf2HJdG5vZrpi1tqlusewysSvEjZx170Afr9TN3zYzXjPwg8Y+FE+E3glJtbslkXRNNDBrmLcCLaPOfm618Tfs76nDqX/BSH9piayu1urRtM8N+W0cgkj4sbcHaQccHOcfTrQB+oQ6UVw/xF+I/gr4TeEL7x98RNVi0Tw/ppiFzdzBikXnyrDHkIGb5ndV4HevmT/h4n+xZ/wBFU03/AL9XX/xmgD7TorzT4WfGD4b/ABt8NSeMfhXrsHiHRorh7VrmBXCieNVZ0xIqnIV1PTvWN8e/jX4V/Z8+FPiD4r+MX/0LRYC0cCkCS6uX+WC3jz/HK5Cg4woyx4BoA9hyc8U+v5+v2ffBPxW0P9v34SfEz41300njT4raNrniK+sWyI9PhktriO0tFQ8jyoUUFT9zhOqkn+gWgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9D9/KMUUUAfz+fty674i+AX7QXxd8L+EbaV2/aV8J6XZ2SwghX1RbuPTpoz6E2hnJP9+Vf7xr6b/a68Baf+zL8PP2dfjDoKFrX4C6tpumX8kKHe+i3sSWd6/HVnKKBn+KQ+p3foV8QvgN8Jfir4s8H+OfH/AIfTVtc8BXRvdFuGmniNrcF4pN5SKRElw8MbBZVdQVyAMnPTfEf4b+Cfi54J1X4c/ETTF1jw7rSJHd2rSSRCRY3WRfnhZJFKuisCrAgjrQB8P/8ABMzQb+4+BOtfGbXozHrPxe8Sat4knLffEc05hjU/7OY3dfZ6/OTwR/yhk+JYA/5joGMf9RiwxX9Bvgzwd4a+HvhLSPA3g6xXTdD0G1hsrK2VncRQQKERd8hZ2IA5ZmLMeWJJJrxmy/ZL/Z+074Man+z3Z+FfL+H+sT/abrTPt16fMl86O43faDObhf3kSNhZAOMYwSCASeAfij8NfB3wN8Ia34r8U6XpNjZaDpzyzXV5DEqBbWPOSzjntgc5r4a/YU1S4+PH7Uvx5/a50i1nt/B+vNZaDo008RiN2lnHGjyKp5+VIImOeQZNvUED33Tv+CZX7Dml3cd7bfDGJ5IyGAn1PVLiMkHPzRzXbow9ipB719reHvDfh7wloln4a8LabbaPpOnxiK2tLSJIIIY16LHGgCqPYCgD82/2kB/xsa/ZaP8A06+JP/SKSvR/+CnY/wCMGviaf+melf8Ap1s6+qPEnwa+G3i74jeFvi14h0f7X4r8FJcppF79ouI/sy3aGOYeUkixSblJH7xGx2wa0vid8MfA/wAZPA2p/Db4kab/AGx4c1kRC7tfOmt/MEEqTx/vIHjkXEkat8rDOMHgkEA+Cvhp/wAE0/2JvEHw48Ka9rHw4Fxf6lpNjc3En9r6su+aaBHdtq3gUZYk4AAHYV5t+xh8MPA3wa/b1/aK+G3w20z+x/DmjaZoAtLXzprjy/PtoZ5P3k7ySNukkZvmY4zgcAAfrTo2kaf4f0ix0HSIvIsdNgitrePczbIoVCIu5iWOFAGSST3NcF4f+DPw28LfEvxP8YdB0f7L4v8AGUVtDq199ouH+0x2caxQjyXkaGPYiKMxopOMnJyaAM348eK9C8D/AAt1nxP4l8HX/j/TrQ23m6Lptgmp3d15lxHGuy1kIWTy2YSNk/Kqluor88v+Grvgd/0aB45/8IWz/wDjlfrbgUYFAHz9+zf488MfEX4fza/4S+Hmq/DOyW+mgOlavpcekXDuiITOLeIspRwwUP1JUjtXwv8AtvaB+0H4v/aI+HM2jfCa9+Jnwt8Comstp1rewWUN9rRaRYzcNKHytuFQhPLIIZwThyB+tOBRgelAH4AfEL9oD9ofUf26vhX441L9n7UNP8VaVoGpwWXhttZt3m1CCVLgSTpcCHZGIssSpUk7eoyK/fWwmnubG3uLqA200saM8RIYxswBKEjg7TxkV55q/wAHPhxrvxR0L40arpHn+MvDVpPY6fffaLhfIt7kOJU8lZBC24O3LxlhngjAr06gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//0f38ooooAYSRTs1wnxD8FN4/8OSeHV1/VvDW+RJPtmi3QtLxfLOdokKuNrdxjmvCB+ypJj/ksvxG/wDB8n/yPQB9Z5ozXyb/AMMpy/8ARZPiN/4Pk/8Akej/AIZTl/6LJ8Rv/B8n/wAj0DsfWWaM18m/8Mpy/wDRZPiN/wCD5P8A5Ho/4ZTl/wCiyfEb/wAHyf8AyPQFj6yzRmvk3/hlOX/osnxG/wDB8n/yPR/wynL/ANFk+I3/AIPk/wDkegLH1lmjNfJv/DKcv/RZPiN/4Pk/+R6P+GU5f+iyfEb/AMHyf/I9AWPrLNGa+Tf+GU5f+iyfEb/wfJ/8j0f8Mpy/9Fk+I3/g+T/5HoCx9ZZozXyb/wAMpy/9Fk+I3/g+T/5Ho/4ZTl/6LJ8Rv/B8n/yPQFj6vLY5p9ee/DvwK3w70BtBbxFq/icmZ5vtet3QvLsbwo2eYET5Btyoxxk13+Tjii6EPopKX8KLhYKKPwo/Ci47BRR+FH4UXCwUUfhR+FFwsFFH4UfhRcLBRR+FH4UXCwUU3nNLzQIWim5NG6gB1FJ+FL+FFx2Cij8KPwouFgoo/Cj8KLhYKKPwo/Ci4WCij8KPwouFgoo/Cj8KLhYKKTNICTSbEOopKX8Kdx2Cij8KPwouFgoo/Cj8KLhYKKPwo/Ci4WCij8KPwouFgoo/Cj8KLhYKKbzmnUCCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9L9/KKKKAK800FuplndY0z95jgZ7daq/wBr6X/z+Qj/AIGv+NeZfG0D/hB5D/03i/8AQq+Nq+uyLhdYyi6rnbW2x8fnvFLwdZUuS+l9z9F/7W0r/n8h/wC/i/40f2tpP/P5D/38X/Gvzoor2/8AUGP/AD9/D/gni/8AEQJf8+vx/wCAfov/AGtpP/P5D/38X/Gj+1tJ/wCfyH/v4v8AjX50UUf6gx/5+/h/wQ/4iBL/AJ9fj/wD9F/7W0n/AJ/If+/i/wCNH9raT/z+Q/8Afxf8a/Oiij/UGP8Az9/D/gh/xECX/Pr8f+Afov8A2tpP/P5D/wB/F/xo/tbSf+fyH/v4v+NfnRRR/qDH/n7+H/BD/iIEv+fX4/8AAP0X/tbSf+fyH/v4v+NH9raT/wA/kP8A38X/ABr86KKP9QY/8/fw/wCCH/EQJf8APr8f+Afov/a2k/8AP5D/AN/F/wAaP7W0n/n8h/7+L/jX50UUf6gx/wCfv4f8EP8AiIEv+fX4/wDAP0fiuILhTJbyLKnTKkMM/hRPdW9soe4kWJScZY7Rn0ya8g+Bg/4oxz/09SfyWqnx8A/4ROxHrfIP/IclfHxyq+N+p83W1z7CebNYH64o9L2PYf7W0rveQ/8Afxf8aX+1tJ/5/If+/i/41+dFFfYf6gx/5+/h/wAE+P8A+IgS/wCfX4/8A/Rf+1tJ/wCfyH/v4v8AjR/a2k/8/kP/AH8X/Gvzooo/1Bj/AM/fw/4If8RAl/z6/H/gH6L/ANraT/z+Q/8Afxf8aP7W0n/n8h/7+L/jX50UUf6gx/5+/h/wQ/4iBL/n1+P/AAD9F/7W0n/n8h/7+L/jR/a2k/8AP5D/AN/F/wAa/Oiij/UGP/P38P8Agh/xECX/AD6/H/gH6L/2tpP/AD+Q/wDfxf8AGj+1tJ/5/If+/i/41+dFFH+oMf8An7+H/BD/AIiBL/n1+P8AwD9F/wC1tJ/5/If+/i/40f2tpP8Az+Q/9/F/xr86KKP9QY/8/fw/4If8RAl/z6/H/gH6KnVtL/5/If8Av4v+NW4poZ0EsDrIhzgqdw/MV+b9fanwbAPgCxJ/vTf+jWrw8+4YWCpRqc97u2x7mQ8TvG1XT5Lad7npM91bWwDXEqxhjgFiFz+dVv7W0vr9siP/AG0WvDPj/wD8g7SD/wBNpP8A0EV8vVvk/CSxeHVZztfyMM54tlhK7o8l7eZ+iw1bSsf8fcP/AH8X/Gl/tbSf+fyH/v4v+NfnRRXq/wCoMf8An7+H/BPJ/wCIgS/59fj/AMA/Rf8AtbSf+fyH/v4v+NH9raT/AM/kP/fxf8a/Oiij/UGP/P38P+CP/iIEv+fX4/8AAP0X/tbSf+fyH/v4v+NH9raT/wA/kP8A38X/ABr86KKP9QY/8/fw/wCCH/EQJf8APr8f+Afov/a2k/8AP5D/AN/F/wAaP7W0n/n8h/7+L/jX50UUf6gx/wCfv4f8EP8AiIEv+fX4/wDAP0X/ALW0n/n8h/7+L/jR/a2k/wDP5D/38X/Gvzooo/1Bj/z9/D/gh/xECX/Pr8f+Afov/a2k/wDP5D/38X/Gj+1tJ/5/If8Av4v+NfnRRR/qDH/n7+H/AAQ/4iBL/n1+P/AP0jiljmQSROHRuhU5B/EVDcXdralRczJFuzjcwXOOuM1xXwx/5ETSD/0yP/oRryL9oLi50X/cuP5pXx2CytVsZ9V5ratX9D7DHZs6OD+tct9Fp6n0X/a2ld7yH/v4v+NL/a2k/wDP5D/38X/GvzpPWkr7H/UGP/P38P8Agnx//EQJf8+vx/4B+i/9raT/AM/kP/fxf8aP7W0n/n8h/wC/i/41+dFFH+oMf+fv4f8ABD/iIEv+fX4/8A/Rf+1tJ/5/If8Av4v+NH9raT/z+Q/9/F/xr86KKP8AUGP/AD9/D/gh/wARAl/z6/H/AIB+i/8Aa2k/8/kP/fxf8aP7W0n/AJ/If+/i/wCNfnRRR/qDH/n7+H/BD/iIEv8An1+P/AP0X/tbSf8An8h/7+L/AI0f2tpP/P5D/wB/F/xr86KKP9QY/wDP38P+CH/EQJf8+vx/4B+i/wDa2k/8/kP/AH8X/Gj+1tJ/5/If+/i/41+dFFH+oMf+fv4f8EP+IgS/59fj/wAA/Rb+1tL/AOfyH/v4v+NXkkSRVeNg6sMgg5BFfm3X374JH/FHaJ/142//AKLWvnuIOHVgYxkp81/Kx9Bw9xG8dOUXDlt53Orooor5c+qCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//0/38ooooA8h+Nv8AyI8n/XeL/wBCr41r7K+Nv/Ijyf8AXeL/ANCr41r9b4J/3N+p+R8cf74vQKKKK+yZ8WgooopAFFYXinxLovgzw5qPivxHcCz0zSoHuLiVs4WOMZOAOWY8bQBySAMnr+d8n7f3iia0uPG2l/CbUrjwFaziF9UMrgBS23czCFolbPG3eQGIUvkiuDGZpQw7SqS3PSwOU18Qm6Ub2+R+ltFcb8P/AB34d+Jng/S/HHhOc3GmarF5kZYbXUglXR1ycMjAqwyeRwcV2VdlOpGcVKLumcFSnKEnCSs0FFFFWQFFFFAH2D8DP+RLf/r7k/8AQVqp8fP+RUsf+v5P/RUlW/gZ/wAiW/8A19yf+grVT4+f8ipY/wDX8n/oqSvySn/yOv8At8/Xqn/Ik/7dPkmiiiv1s/ImFFFFAgooqlqWo2Gj6fc6tq1wlnZWcTzTTSsEjjjjBZ3dmwFUKMkngDJpSaSu9hxi2+Vbl2ivgKL9vzwVrfxc0X4beCtAn1nTtY1G101dVef7KnmXEqxF44DE7Oi7gRuZCemAOT9+1yYTH0a7l7KV7aHZjMurYfl9tG11cKKKK7DiCiiigAr7V+Df/JP7H/em/wDRrV8VV9q/Bv8A5J/Y/wC9N/6Navi+O/8AdY+v6M+34G/3qXocX+0B/wAg7SP+u0n/AKCK+Xq+of2gP+QdpH/XaT/0EV8vV3cH/wC4R+f5nFxj/v0vl+QUUUV9MfKhRRRQAUV5L8avjH4W+BngWfxx4q8yWISLb21vEMyXNzIrMkak4VeFJZj0AJ5OFPyJ/wAN0+LPDkul618UPhJqfhjwprUiLb6iZXkKo4yDseCMOSvzgblJXJAbrXn4rNKFGfJUdn/W/Y9PCZPiK8OenG69Vr6dz9FaKq2N7aalZW+o2EontrqNJYpF6PG4DKw9iDmrVd6aaujzZRadmFFFFMQUUUUAfdHww/5ETSP+uR/9CNeRftBf8fOi/wC5cfzSvXfhh/yImkf9cj/6Ea8i/aC/4+dF/wBy4/mlfkWS/wDI3+cv1P17O/8AkUfJHzqetJSnrSV+un5EwooooEFFFeO/HD42eFPgP4Kfxl4pSW582UW9pawD95cXDKzBATwq4UlmP3QD1JVTnWqxpxc5uyRrQoyqzVOCu2exUV+eB/bl8VeGJtM1P4r/AAj1Xwr4Z1eVEh1BpXcqrjIykkEQZiMtt3KdoJAbHP6D2d3bX9pBf2cgmt7lFkjdTkMjjKsPYg5rmweYUq7apvVb9DqxuWVsPZ1Fv8/yLFFFFdpwBRRRQAV9/eCP+RO0P/rxt/8A0WtfANff3gj/AJE7Q/8Arxt//Ra18Dx9/Cp+rP0DgH+LU9P1Oqooor8xP1AKKKKACivPfiz48Hwu+F3jH4ltZHUl8JaPf6sbUSeSZxY27z+WJCr7N+zbu2tjOcHGK/IDRv8AgsR4q17RbjxRpP7PGsX2hWTlLm/tdSkntoCgVnDyrp3lqyhgSGYdR6g0AfuDRXyB+yZ+2f8ADD9rvQNUv/BUVzpGsaE0Y1DS70L50KS58uVHQlJI2KsMgggjDKuVJ+vc+poAdRXnXxX+Jeg/B34c+Ifif4oiuJ9J8NWj3lyloiPO0ceMiNXZFLHPAZgPeuO/Z3/aC8F/tM/DaD4p+AbW/stJuLme1WPUYoorjfbNtclYpJlwc8fOfcCgD3aimbjnGK8++K3xL0H4PfDnxB8T/FEVxPpXhq0e8uY7VUedo06iNXZFLc8ZYD3oA9Eorwn9nb9oPwX+038NoPin8P7W/s9IuLme1WPUYo4rgSW7bWJWKSVMHPHz/UCvdOetADqKZk9q+Hf21f20ov2PbLwfP/wh8njK58X3N1BFbxXv2N4/sqxEkfuJy5YyqAoA/GgD7lor8ctL/wCCovxg1DUrSwl/Zb8TQJcTRxNKbi6IjDtgsf8AiWDoOeo+tfsaKAP/1P38ooooA8h+Nv8AyI8n/XeL/wBCr41r7K+Nv/Ijyf8AXeL/ANCr41r9b4J/3N+p+R8cf74vQKKKK+yZ8WgooopAfFn7ft7eWn7N2rxWpIju7yxinI/55+aHAPtuVa634eeHPDzfsc6RoTRR/wBmXnhASTjaAu+5s/NmYjHXzGZifXnrzXq/xm+Gdh8YPhprvw61CX7ONWhAimxkxTxsJYXx1Kq6jcMjK5GeePzmsfDv7cHh74W3H7N9l4RtrmwkSSwi1xbqP5LGZjvRXMqjG0kDcgdUONm4KR8vj1KlipVHByUo20V9ez9T63LXGrhY0VNRcZXd3bTuvQ9j/wCCbl7eXXwM1e2nYtDZ6/cxxEnhVa2t3Kj6Mxb/AIEa/QWvEP2d/g9b/A74V6V4D89bq9QvcX06ZCSXU5BcrnBKqAqKSASFGe9e316+T0JU8NCE90tTx86xMauKnUp7N6BRRRXonlBRRRQB9g/Az/kS3/6+5P8A0FaqfHz/AJFSx/6/k/8ARUlW/gZ/yJb/APX3J/6CtVPj5/yKlj/1/J/6Kkr8kp/8jr/t8/Xqn/Ik/wC3T5Jooor9bPyJhRRRQIKw/E/h3S/F3hzVPCmto0mn6xbS2lwqsUZop12OARggkHrmtyj+v/66Uoppp9SoScWmuh+Vf7Rvhbw34J/aW/Z98N+E9Oh0vTLO9sViggUKi/8AEwjyT3ZieWY5LHkkmv1Ur4a/aH+DfxH8d/tDfCXx34X0n7bonhm8tZdQuDcQR+Skd4krHZI6u+EUn5FY9uuBX3LXi5VQcKtfSyurfce7nGIVShh/eu7O/wB4UUUV7Z4AUUUUAFfavwb/AOSf2P8AvTf+jWr4qr7V+Df/ACT+x/3pv/RrV8Xx3/usfX9Gfb8Df71L0OL/AGgP+QdpH/XaT/0EV8vV9Q/tAf8AIO0j/rtJ/wCgivl6u7g//cI/P8zi4x/36Xy/IKKKK+mPlQooooA4bx18NPAvxMs7XTvHujw6zbWUvnwxz7tqyYI3YUjPykjnPHFfn3+1D48uv2kfGOn/ALMHwfhTVDaXi3WsakBvt7X7OChXcMjbFuJkYdXwi5YkV9TftUWXxz1f4cf8I/8AAiz8/U9Um8m9lSeG2misyjFjE80kYVmbahYEsATgAnI+Lvgp4R/bT+BHhqTw94M+EWhSSXLmW6vbm8t3urlsnaJHXUFXbGDhVUAAZONxYn5TOazlU9iqcuV/E0nt2Wh9jklCMaXt5TjzL4YuSVn31P1L8L6Ba+FPDOkeFrFi9to9nb2cTMcsUt41jUk+pC1u1zXgy78T33hLR73xtZxad4gmtIXv7aFg0UVyVBkRGDOCA2QCHYYHU9a6Wvp6SXKrbHyVW/O7vUKKKK0MwooooA+6Phh/yImkf9cj/wChGvIv2gv+PnRf9y4/mleu/DD/AJETSP8Arkf/AEI15F+0F/x86L/uXH80r8iyX/kb/OX6n69nf/Io+SPnU9aSlPWkr9dPyJhRRRQIK5LxV4D8FeOFtF8Y6JZ60LGQyW4u4UmEUjYBZQ4wDwOnpXW18x/tPfDH4nfEDw1peq/CDxDPoniPw9O08cMd09vFeIwUtHIVIQsCilPMG37wOA2Ry42XLScuTmt0O3AR5qqi58t+p8LftReOfjr4pstJ8N/Hjwe/gr4fpqUctzf6bEuoTHG5U+fzzGuQT8uQc84bG0/q74Jl8PTeC9Cl8ISi40M2Fv8AYZASQ1sIl8o8gHO0DqM+tfnB8Q9G/bc/aB8ORfCrxd4L0vwtpMs8DahqAuIir+SwcMFWeZtgYBiI1YkgcgZr9FPh14Ltvh14D0HwNaTtcx6HZw2glYBWlaJQpcrk4ycnGTjOK8HJYzeIqVGm4tLWStr2Poc8nTWGp000pJvSLurPrfudmetFFFfUHyIUUUUAFff3gj/kTtD/AOvG3/8ARa18A19/eCP+RO0P/rxt/wD0WtfA8ffwqfqz9A4B/i1PT9TqqKKK/MT9QCiiigD55/a3A/4ZX+MZHX/hDtf/APTfNX4BfsO/t+6X+yX+z5r3he++H2q+J5J9cudRjvYJFt7BWmtrWFYJZyj7HzFkkK3DLwelf0PftFeFte8dfAH4leCfC1r9t1nX/Der6fZQb0j825urOWKJN8jIi7nYDLMFHUkDJr4E/Yk/Y68ZeH/2M/H37O37R3h/+w5vGGr38nki5tbtkgntLOOC5R7aSWMPHNCXQFshkBIxjIB8O/sF2HxF+Dvww+Of7eFzplvY6Pe6LqC6JZLgW9zefai5byUbckFvMgjGcEgsFOATWR+zx+xd47/bn+Ffij9pf4i/ErVB41vr28j0Ubg8Xn2yhgZSeUjaVtiRw7BGq5XPCj6y/Yh/Za/aV+Hngf4r/stfHzw21r8NfGNlfR2Gr29/Y3AhuZ0NrI8UCTvOonjKzJviGxo/mAZzXiXgX4N/8FPf2VPCviz9n/4O+GdN8U+FNduLlrPWRNbeZB9oQRNPbebdwmB2VVLJPG6q4yueWYA8R8FeLfFX7XX7CHxEsfip4l1KfVPgGseo6fdJKC9/bX0MqR2987hmlEZhcBshtpAJOOfrX/gkn+z/AKZp3wzk/aVs/El7FrVzHq2kLYXDqdJhCSRss7RgK+QUBb5xwT0617V+z3/wT98SfDH9jL4jfBvXNTth47+J1lObl1Ja1s5/JKWtuZACXVG5kdQRl2CggAnzL9jb9n39tfwB8PvHn7K/xT8PWGgfDrXtG1yKy12G6tri7h1HUIlgj8owXTP5JDPJh4AwI+8uQpAPz8+Lfh/9nGUeNfEvxV/ai1f4h/FGAXE+ktotjctYfbI0by4jNIrxFGcBFME0ccaYwSMAeteEYfEv7Un/AATN8VeI/iX4q1S51D4NalqDadIJg7XdutnbPHb3bOrPIiGVwp3btu1c4GK6f4P/ALK3/BQf4ReAfHH7P3hv4a+Fk07xT9sE3iq6ltJrsQT2/kvFayicSbZFT9yJYB5buxYpu3L9Pfsifsa/Grwz+xJ8X/gF8T9KTwp4k8a3V81gJrq2u48S2NvHDI72ckwVTNGQwzuAGdvTIB85fsA+BdK+Cf7KvjP9uGz1q/uNe0fSNes4NIlYNpZliKGBmjUB9zSqoYh8YJ718D6H4p+DXxb8OeLPin+0X8XfFFr8X7me5m0hLW1ae0R1QPCZHAO1HkJRY4miESgYyMCv1u/Yw/Zx/a68HeBvGf7J3x78J2GmfCTxBpmrxpq8F1a3N6l5fKkQ8jyrlmCEF5V8y33K4GWHCny/4ZfCv/gpb+yToOv/AAP+FPgHQvHXh+5urmXS9bme03wGYBTJGs13DsyAH8ueNlVzwWXIIB9if8Erv2g/Gfx2/Z9vbP4h6i+ra54M1I6Z9rly089m0Eclu88hP7yUEyIWPJCqWLMSx8e/4KjfDr41ePvid8DNR+E3gXUPGKeFLu8vZltoHe2WaS4smjjuJVwsSsIDlnYALk5GCa+/f2VPBnx58GfC9Iv2kPFEHibxnqFy9xKLS2treCyhKqsdsrW0MKyEFSzuVxuYqpIUM3gf7eXwE/aR+IyeFPiR+y/4tvdI8UeEZCZtJj1F7O31JPMSSJsM62zSRMp3LMAsiMVLfKFYA+O/ir+1N/wUT/Y88Q+HfHH7Rtp4e8TeCfEd4IZLTS40VbZsGR7aOZVjlSYJuKNJ5yHafmbBr9xtB1rT/Emh6d4i0iXz7HVLaG6t5B0eKdA6N+KkGvwV+JHwY/4KN/t26n4X8AfHrwvpfw28FaFeefd3VuYtrTKhQz+QLu4lmkCF1iCbIsucsB8w/ePwz4f03wl4b0nwroylLDRrSCyt1Y5Ihto1jQE8ZIVRQB//1f38ooooA8h+Nv8AyI8n/XeL/wBCr41r7J+Nv/Ijy4/57xfzr42r9b4I1wb9WfkfHF/rfyQUUUV9jddz4uwUUUUadxh2x29KKKKG1rqAUUUU7ruJBRRRS07jCiiijTuB9g/Az/kS3/6+5P8A0FaqfHz/AJFSx/6/k/8ARUlWvgZ/yJb4/wCfqT/0FaqfHs/8UnY5/wCf5D/5Dkr8jg/+Fq/94/Xan/Ik/wC3T5Kooor9c+Z+RXCiiijTuAUUUUadwCiiih27iVgoooobXcEFFFFGncYV9q/Bv/kn9j/vTf8Ao1q+Kq+0/g4ceALDnq83v/y1avieOpL6rH1/Q+24GT+tP0ON/aA/5B2kf9dpP/QRXy9X0/8AtAE/2dpGSB++k/8AQRXzB9a7uD5L6jFX6v8AM4uMU/r0vRBRRRX1Gnc+WCiiijTuAYoOD15ooodu4lZO6DrRRRRdDCiiijTuAUUUUadwPuj4Yf8AIiaR/wBcj/6Ea8i/aC/4+dF/3Lj+aV678MD/AMUJpH/XI/8AoRryL9oL/j50TP8AduPQf3K/Icl/5G//AG9L9T9dzr/kUfJHzqetJR15or9e07n5EFFFFGncAo9+/SiinzeYrLqFHA6cfSiilo3cP62Ciiii/mGgUUUUadxhX394I/5E7Q/+vG3/APRa18A19/eCf+RO0P8A68bf/wBFrXwPHv8ACper/I+/4B/jVPRfmdVRRRX5ifqAUUUUAFJtGc0tFACYFGBnNLRQAmKNoznvS0UAJgUYFLRQAmBnPejApaKAEwPyo2j0paKAE2iloooA/9b9/KKKKAMrVdH0zXLRrLVoFuICQ21umR06Vy//AArLwKemkRD8/wDGu9oxXTSxlamrQm0vJs5auCo1HzTgm/NHB/8ACsvAv/QIh/8AHv8AGl/4Vl4F/wCgRD/49/jXd0Vp/aeJ/wCfkvvZH9m4f/n2vuRwn/CsvAv/AECIf/Hv8aP+FZeBf+gRD/49/jXd0U/7TxP/AD8l97D+zcP/AM+19yOE/wCFZeBf+gRD/wCPf40f8Ky8C/8AQIh/8e/xru6KP7TxP/PyX3sP7Nw//PtfcjhP+FZeBf8AoEQ/+Pf40f8ACsvAv/QIh/8AHv8AGu7oo/tPE/8APyX3sP7Nw/8Az7X3I4T/AIVl4F/6BEP/AI9/jR/wrLwL/wBAiH/x7/Gu7oo/tPE/8/Jfew/s3D/8+19yOE/4Vl4F/wCgRD/49/jR/wAKy8C/9AiH/wAe/wAa7uij+08T/wA/Jfew/s3D/wDPtfcjJ0jRNL0G1NlpNuttAWL7VzjJ4J5+lN1rQdJ8QWyWmsW63MMbh1Vs4DAEZ4PoTWxRiuX2s+bnvr3Oj2MOT2dtOxwf/CsvAv8A0CIf/Hv8aX/hWXgX/oEQ/wDj3+Nd3RXT/aeJ/wCfkvvZz/2bh/8An2vuRwn/AArLwL/0CIf/AB7/ABo/4Vl4F/6BEP8A49/jXd0U/wC08T/z8l97D+zcP/z7X3I4T/hWXgX/AKBEP/j3+NH/AArLwL/0CIf/AB7/ABru6KP7TxP/AD8l97D+zcP/AM+19yOE/wCFZeBf+gRD/wCPf40f8Ky8C/8AQIh/8e/xru6KP7TxP/PyX3sP7Nw//PtfcjhP+FZeBf8AoEQ/+Pf40f8ACsvAv/QIh/8AHv8AGu7oo/tPE/8APyX3sP7Nw/8Az7X3I4T/AIVl4F/6BEP/AI9/jR/wrLwL/wBAiH/x7/Gu7oo/tPE/8/Jfew/s3D/8+19yOC/4Vn4Gzj+yIf8Ax7/Gur0vSdP0WyTT9LgEFvHnai9BuOT19zWlRWNbGVqitUm2vNtmlHB0abvTgk/JGFrPhzRfEMccWs2i3SxEsgbOATx2Nc9/wrPwMf8AmERfr/jXfUYqqWNrQjywm0vViqYGhN804Jv0ODHwy8C/9AiH/wAe/wAaX/hWXgX/AKBEP/j3+Nd3RV/2nif+fkvvZH9m4f8A59r7kcJ/wrLwL/0CIf8Ax7/Gj/hWXgX/AKBEP/j3+Nd3RT/tPE/8/Jfew/s3D/8APtfcjhP+FZeBf+gRD/49/jR/wrLwL/0CIf8Ax7/Gu7oo/tPE/wDPyX3sP7Nw/wDz7X3I4T/hWXgX/oEQ/wDj3+NH/CsvAv8A0CIf/Hv8a7uij+08T/z8l97D+zcP/wA+19yOE/4Vl4F/6BEP/j3+NH/CsvAv/QIh/wDHv8a7uij+08T/AM/Jfew/s3D/APPtfcjhP+FZeBf+gRD/AOPf40f8Ky8C/wDQIh/8e/xru6KP7TxP/PyX3sP7Nw//AD7X3Io6fp1npdpFYWEQht4RhEGcAe2azNb8L6D4iMTa1ZpdGDcE35+XdjPQ98CuhpMVyxrTUudPXudM6MJR5GtOxwn/AArLwL/0CIf/AB7/ABpf+FZeBf8AoEQ/+Pf413dFdP8AaeJ/5+S+9nN/ZuH/AOfa+5HCf8Ky8C/9AiH/AMe/xo/4Vl4F/wCgRD/49/jXd0U/7TxP/PyX3sP7Nw//AD7X3I4T/hWXgX/oEQ/+Pf40f8Ky8C/9AiH/AMe/xru6KP7TxP8Az8l97D+zcP8A8+19yOE/4Vl4F/6BEP8A49/jR/wrLwL/ANAiH/x7/Gu7oo/tPE/8/Jfew/s3D/8APtfcjhP+FZeBf+gRD/49/jR/wrLwL/0CIf8Ax7/Gu7oo/tPE/wDPyX3sP7Nw/wDz7X3I4T/hWXgX/oEQ/wDj3+NH/CsvAv8A0CIf/Hv8a7uij+08T/z8l97D+zcP/wA+19yOB/4Vn4FPH9kRfr/jXaWlpb2FrFZWqCOC3RY0UdFVRgD8BVqjFYVsVVqaVJt+rua0MJSptunFL0QUUUVgdAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH//X/fyiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAGFsZNPr8tf+CtfiPxB4X/Z88Hal4b1O60q4bxrpscklpM8DtE1pelkZoypKkgEg8EgelfqVQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//9D9/KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA/Jz/gsb8v7MPhqX/nl4y01v/JS9H9a/WOvyc/4LJjH7KGlSD/ln4q05v/Ja7H9a/WMc80AFFFfj1+1X+3j+0l8LP2qI/wBnD4I+D9C8T3V/bWb2Ud7HcG6mnuImkZN63cEWBtOMgcdzQB+wtFfiJa/8FNv2hfgv8RNF8HftlfCOHwlpusvgX2nCaPy4chWmjWSa4juFjLL5gSUMoPrhW/ZDxT448GeBtK/t3xtr+n+H9MJC/atRuorSDJ5A8yZlTJHIGaAOrorj/CHxA8C/EGwfVfAXiPTvEtlGwR59Mu4byJWPO0vCzqDjsTXwY/8AwUg+Hy/tcL+zSLLTj4fYZ/4S/wDtyD7AD/ZpvsFPK8vPmfuP+Pj7x9floA/SOiuR1vx54J8M6FD4p8SeIdO0rRrhUaK9u7uGC1dZF3IVmkZUIZfmBB5HI4qv4X+JHw+8c6XPrXgnxPpev6fajdNc6fewXcMa4Jy8kLso4BPJ6A0AdtRX5teE/wDgpD8PPEv7UWufs+Xlnp2l6FpC3DReKpdctzY3Pkxo4CIY1T5ixUYnP3T16D738U+PPBPgbSV1/wAa+ItO8P6WxVRdahdw2kBLcgCWZlTJHIGenNAHXUVyfhLxz4M8faZ/bXgXxBp/iPT9xT7Tpt1Ddw7gM48yFmXPtmup3HsQaAH0V+Rf7cv/AAUe8U/s+fEeH4U/BHQ9O8T63pNm1/4gkvo55obFGVXijAt5YirhDvkLHADxgfMxx9n/ALGvxw8UftF/s5+FvjB4xs7PT9X1xr9ZobBJEt1+y3s9smwSySOMrECcufmJxgYFAH1LRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAf/0f38ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD8o/+Cx6b/wBke1P93xLpx/8AINyK/Va3fzLeKT+8qn8xX5Zf8Fh03fshBv7niHTT/wCOTj+tfqJpTiTS7Nx/FDGfzUUAX6/m6/bQ8UeNPBX/AAVL0DxT8O/Dx8V+JNNttLlsdLUlTdS/ZnGzI5HBJ49K/pFr8wfiL+xb8UvF37f3hf8Aar07VNGi8J6KLMTWss1wuot9mgeJtka27QnLMMZmHGeh4oA/Kz4ueOPjv/wUW/ac8Hfs8/EjR9O+GF94dmvYfsc/mrPB5iRzXZdpCWmmMUKmJFVVPrgl69h+IHgz/hrr/gplJ+z98UNSu4vAPgKCS2sdOjlMR+y2VlG7CNuPnuZcPJIBvMY2hgFQr9qftm/sGfEr4vfHbwj+0V+ztrmkeGfGGjeS1/JqctxAks9g6tZzobe3nLvtzHIHABRUAzzWf+01+wj8ZvGvxh0T9qj9nfxXYeDPistvbjVopZZGsnuorZbcyWspgkJXygYmSWHbIgBIUlgQD43ufA8f7Cn/AAUx8A+APgdf3cXhXx8NKjvNNklM6ra6lcyWkkMrNkuIWjM8bN8ycckZLeXzfsm/BEf8FTI/2Yv7Hm/4V86FjZC7n83P/CPm/wD9fv8AN/4+Pm+904+7xX6I/s4fsGfGeL9oQftSftfeM7XxZ4v08BtOt9PZjCsqxmJJJj5EEaLCpJjhiTbuO8twQ0X7UX7BPx88Y/tQw/tUfs0+OtL8NeJmjt9w1MSxtBPBbfYy8TJb3SSJJAArJJGBy2dwOKAPFv269F/Y70b4yeB/B3xS1Xxh4tvPCmkWWm2Hgfw4iSxRWkSHyhLNI0biScYLGNzMyqudo2mvlH9lR/Dui/8ABRax8D/Dnwvr3gHwN4ytb3Trnw/rjSx3psp9JlkdJgzb9pmXzIizMQNpDZr75+Mv7Bv7Ud38evD/AO1P8EfHui2PxG/s2xh1mTUVZYTqMNgmn3M9uBaTRPFPGDlGhj28lQCQFb8KP2Af2mPDv7ZHhn9qD4sePtG8YPbGS41adTPBePPLYy2ojt7dbYQeVEXRU/eJ8i52KcJQB8KfC79kz4IeKP8AgpF4y/Z11jSJ5fA2jx3rW1oLydJFMMETpmdXEjAM7dW9jVj9rXxaPij/AMFAr7wF8Q/C3iTx94D+HqLYWXhnQvMN35MVnE0joIwX2yzkPLIMO0YVd+FXH3X8cv2BP2kz+1Tqn7T/AOy38QtJ8N6rrJEki6oJFe2kaBIJlUC2u4po5Nu7EiDbnABwGrtf2lv2GvjLr/xr0n9qb9mLxla+F/iWlvDDqq324Wl1JDAtv5seIZl+aJQjxPEUYBWG1gcgHw9+x/p3xA+GP7cOman8IPhX458B/CTxcDZalp2tWF20UQNvJ5byzyIyhIrna6O7F1Usu7DEH9t/2o/jrp37OPwM8UfF2+h+1zaVAEsrfBKzX1wwitkcjonmMC57KCRzgH5W/Zz/AGZ/2vLP4w/8Lr/ao+ME2staRstr4d0O8uItKd2jMavcwKltb4jDEiNYm3PtdpDgq33Z8T/hV8P/AIz+ELjwH8TtHTXdBu5IpZbWR5I1Z4WDod0To3ysM9aAP5rPAmtfBew/Y2+NPxL8f/EDTNZ+OPxahctaPcK9/Dbm9jlMO0dJJ3QzSAcBRGuAUNfqp/wSe+I3gbW/2UfDHw40nWre78TeG/7Rl1GwRj51rHdalcyQtIOgDqwI9an+Of8AwTB/Z38TfCnxFoXwV8E6X4d8bXUKLpl/dXl+IIJRKjMXw8/BjDL/AKpuo4716j+wv+x7pn7KfwxtrPXbPTpfiHfrNFrWp6dPczQXcS3Ustsq+esf+rhdFOIk5BHP3iAfdNFA6UUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH//0v38ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiimEkZ9KAPy5/4LAJu/Y8uD/c17TT/wCjR/Wv0w8Ov5nh/TJP71rCfzQV+a//AAV3Xf8Ascagf7mtaW3/AI+w/Lmv0c8ISF/CWiSf3rG2P5xLQB0tJgZz3pR0ooAaVBpcDrS0UAJgUEA0tFACYGc0FQetLRQAmBQFA6DFLRQAmBS0UUAJgZzRgHrS0UAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//0/38ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACoyM1JSYHPvQB/Kb/wUw1H9oj4efGTX/hL408b6vrnw/1yRNa0W2u52ktzbSOxWPB4LW0geMZOcKrH7wr9Bf8AgktfftB/FCy8RfF34reONa1rwtp6jRdGsLy6ke2kuFCvPN5ZIBEKbI0PKku/8SDH1D/wUb/ZNvf2o/g7ar4Nto5fHPhW7W40suwjE0M7LHdW7OR8qsm2Qf7cajua+s/gb8JdA+BXwl8L/Cfw0M2XhyyS3MuNrTznLzzsOzSys8je7ccUAetDkZooFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAf/1P38ooooAKKQmjnGaLgLRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRTQecUpNFwFooooAKKKKACiiigAooooAKKKKACiiigBMDrRgUmTQSe1K4WHUU3k9KMmncB1FFFABRRRQAUUUUAFFFFABRRRQAUUU3JzQA6ikpaACiiigAooooAKKKKACiiigAooooAKKKKACimkn8qASaLgOooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9X9/KKKKAMDxB4i0zwzYNqerSGOBWCkqpY5PA4FcJ/wujwGP+XqT/vy/wDhUXxsUf8ACDy8f8t4v/Qq+NsmvuuHOGqGLw/tKjd720Pg+I+JMRhMR7Kmla1/61Ps7/hdHgT/AJ+5P+/L/wCFL/wunwJ/z9yf9+X/AMK+McmjJr6D/UfCd3+H+R89/rxi+y/H/M+zv+F0+BP+fuT/AL8v/hR/wunwJ/z9yf8Afl/8K+McmjJo/wBR8J3f4f5B/rxi+y/H/M+zv+F0+BP+fuT/AL8v/hR/wunwJ/z9yf8Afl/8K+McmjJo/wBR8J3f4f5B/rxi+y/H/M+zv+F0+BP+fuT/AL8v/hR/wunwJ/z9yf8Afl/8K+McmjJo/wBR8J3f4f5B/rxi+y/H/M+zv+F0+BP+fuT/AL8v/hR/wunwJ/z9yf8Afl/8K+McmjJo/wBR8J3f4f5B/rxi+y/H/M+zv+F0+BP+fuT/AL8v/hR/wunwJ/z9yf8Afl/8K+McmjJo/wBR8J3f4f5B/rxi+y/H/M/QXw94l0rxTp51PR5DLAHKElSp3L14P1rN8S+OvD3hKaGDWpmie4BZAqM+QDg9M1xXwN58GPn/AJ+pP/QVrgv2gP8AkK6UP+mMn8xXxmEyalPMnhJN8t2faYvOKtPLo4uKXM0j03/hdHgT/n7k/wC/L/4Uf8Lp8Cf8/cn/AH5f/CvjHpwKMmvsv9R8J3f4f5Hxj44xfZfj/mfZ3/C6fAn/AD9yf9+X/wAKP+F0+BP+fuT/AL8v/hXxjk0ZNP8A1Hwnd/h/kL/XjF9l+P8AmfZ3/C6fAn/P3J/35f8Awo/4XT4E/wCfuT/vy/8AhXxjk0ZNH+o+E7v8P8g/14xfZfj/AJn2d/wunwJ/z9yf9+X/AMKP+F0+BP8An7k/78v/AIV8Y5NGTR/qPhO7/D/IP9eMX2X4/wCZ9nf8Lp8Cf8/cn/fl/wDCj/hdPgT/AJ+5P+/L/wCFfGOTRk0f6j4Tu/w/yD/XjF9l+P8AmfZ3/C6fAn/P3J/35f8Awo/4XT4E/wCfuT/vy/8AhXxjk0ZNH+o+E7v8P8g/14xfZfj/AJn2b/wufwHni6lz/wBcXr0+yu4NQtIb62OYbhFkQkYO1hkcH2NfnHX6DeFP+RY0n/r0g/8AQBXy3FGQUcHCEqTere59Xwvn9bGTnGqlZLoY3iT4heGvCl+mm6zO0c8kYlAVGf5CxUdPcGuf/wCF0eBM/wDH3Jj/AK4v/hXj3x648YWuOM2Mf/oyWvEsmvWyfhLDV8NTrTbu0ePnHF2JoYmdGCVkz7O/4XR4E/5+5P8Avy/+FL/wunwJ/wA/cn/fl/8ACvjHJoya9P8A1Hwnd/h/keZ/rzi+y/H/ADPs7/hdPgT/AJ+5P+/L/wCFH/C6fAn/AD9yf9+X/wAK+McmjJo/1Hwnd/h/kH+vGL7L8f8AM+zv+F0+BP8An7k/78v/AIUf8Lp8Cf8AP3J/35f/AAr4xyaMmj/UfCd3+H+Qf68Yvsvx/wAz7O/4XT4E/wCfuT/vy/8AhR/wunwJ/wA/cn/fl/8ACvjHJoyaP9R8J3f4f5B/rxi+y/H/ADPs7/hdPgT/AJ+5P+/L/wCFH/C6fAn/AD9yf9+X/wAK+McmjJo/1Hwnd/h/kH+vGL7L8f8AM+zv+F0+BP8An7k/78v/AIUg+NPgTPN3J/35b/CvjLJoycYzx6fhU1OB8Kk2m/w/yLp8b4tySaX4/wCZ+kcUiyxrKnKsAR+Ncn4n8caB4Re3j1qZomutxQKjPnZjPQe4rpLH/jxg/wBxf5V83ftBcXOjDttuP5x1+fZRgI4jFqhLZn3+cZjPD4R14LXQ9D/4XT4E/wCfuT/vy/8AhS/8Lp8Cf8/cn/fl/wDCvjH6UZNfof8AqPhO7/D/ACPz3/XjF9l+P+Z9nf8AC6fAn/P3J/35f/Cj/hdPgT/n7k/78v8A4V8Y5NGTR/qPhO7/AA/yD/XjF9l+P+Z9nf8AC6fAn/P3J/35f/Cj/hdPgT/n7k/78v8A4V8Y5NGTR/qPhO7/AA/yD/XjF9l+P+Z9nf8AC6fAn/P3J/35f/Cj/hdPgT/n7k/78v8A4V8Y5NGTR/qPhO7/AA/yD/XjF9l+P+Z9nf8AC6fAn/P3J/35f/Cj/hdPgT/n7k/78v8A4V8Y5NGTR/qPhO7/AA/yD/XjF9l+P+Z9nf8AC6fAn/P3J/35f/Cj/hdPgT/n7k/78v8A4V8Y5NGTR/qPhO7/AA/yD/XjF9l+P+Z9m/8AC6PAXX7VJ/35evTdOvrbU7G31G0YtDdRrKhIIJVwGBwenBr85cmvv3wR/wAidof/AF42/wD6LWvl+KMgo4OEJUm9W9z6nhfP6+MqTjVS0XQ6qiiivjT7QKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9b9/KKKKAPIfjb/AMiPJ/13i/8AQq+Na+yvjb/yI8n/AF3i/wDQq+Na/W+Cf9zfqfkfHH++L0CiiivsmfFoKKKKQBR/n8frXnXxZ8aan8PPh5rXi/RdIm13ULCEG2soI3keaaR1jQbYwzbVZgXIH3QTxX5/ajqf7eNn8N7v46an4s0/S7W2tW1B9Cksokmjs1+chle3JDbfm2mTft6tu+WvMx2aRoS5XFt2u7dEetl+UyxEOdTSV7K/Vn6jfSivGv2fvijP8ZfhJoHxEvrRbG71JJVuIUz5Ymt5Xhcx7udrFNwGTjIBJIJr2X6130KqqwVSGzPOxFF0pypz3TsFFFFaGQUUUUAfX/wM/wCRMf8A6+pP5LXB/tA/8hXSv+uMn8xXefAz/kTH/wCvqT+S1wf7QP8AyFdK/wCuMn8xX5jl/wDyO5erP1LMv+RJH0R8+nrSUp60lfpx+XMKKKKBBRRXkPxw+K8fwb8CS+Ll0mfXLuSeGzs7K34ea5nyI1JwxCkjkqrHsBk8Z1q0acXOWyNcPRlVmqcN2evUV+aH7P8A8eP2h/FH7TF58NPi9LHpkCWEl4+kpbQILYyRRTwKZAGl4SQZDSEgnDcjFfpfXJl2YQxMHUgna9tTszLLZ4Wap1Gr2T08wooorvPOCiiigBRX6DeFP+RY0n/rzg/9AFfnyK/Qbwp/yLGk/wDXnB/6AK+B4+/h0vVn6HwD/Eq+i/M+Zfj1/wAjhaf9eMf/AKMlrxKvbfj1/wAjhaf9eMf/AKMlrxKvo+G/9xpen6nzPEv+/VfUKKKK9xnhBRRRSAOego56D0r5E/al+L3xM8Dv4X8BfB3TftPifxhc+Ql28fmxWSb0jVmDKUBdn+8+VVVYkHgj5z8V/FD9qb9l3xf4Wvvi94msfG3hnxFcNDcRw26RtCwK+ZsKwxOGUOGTGUOMFRxXj4nOadKbg4uy3dtFc9vC5HUq01NSSbu0r6ux+o9FFFeundXPFatoFFFFMQUtJS0VPhfzNaXxr5H6OWP/AB4wf7i/yr5t/aC/4+tG/wBy4/8AZK+krH/jxg/3F/lXzb+0F/x9aN/uXH/slfjPDP8AyMYer/U/YuJv+Ra/RHzqetJSnrSV+zH4wFFFFABRz/gKK+RP2uv2gda+CXhjSdK8FW63Pi3xXO9tYBk80RLHtEkgjH333SIqKeCSSc7Sp5sZioUKbqVNkdWDwc69RU6e7PrvnPTnt2or8qfFfxF/bB/Zhm8PeOvi7rdl4v8ADWr3CwX1rBHHm3kZd/l70gi2ybAxQozR7lIIxjP6jaRqljrelWetabKJrPUII7iCQdGjlUOjD6gg1z4HMoV5Sgk1JbpnVmGVzw6jNyTi9mtvQ0KKKK9E8sKKKKACvv7wR/yJ2h/9eNv/AOi1r4Br7+8Ef8idof8A142//ota+B4+/hU/Vn6BwD/Fqen6nVUUUV+Yn6gFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//1/38ooooA8h+Nv8AyI8n/XeL/wBCr41r7K+Nv/Ijyf8AXeL/ANCr41r9b4J/3N+p+R8cf74vQKKKK+yZ8WgooopAc/4m8V+GfBWlnXfFuqW+jaerqhubqRYYw7fdXcxAyecfiBXwJ+058Ffi18QvDviD4h+FfiYL/wAFzWw1VNCYyQ2UtnDCsuFmikKyK4Uuu4KuTncPvV9z/EPwB4b+KHg3U/AviyJpdM1VFSXY2yRWRg6OjdmRlVhwRnGQVyK+E2/Yb+Jceit4CtfjVqUXgxwy/wBnfZpCBGTnytouQhQ98ALn+Cvn87o1qi9nGF4vs7a+fkfS5HWo0/3kqnLJd1fTy8z3z9jr4h6P8Q/gbpN3omhweHo9Elk0ySzt9xgWWEK5aMuxcrIJFZizM28nLMcsfqTpwK85+FXwt8KfB3wXZeBfB0TJZWu53kkbdLPK/wB+WRsAFmx2AGMAAAAD0Y+9ergKU4UYQqbpanj5jWpzrznS+Ft2Ciiius4gooooA+v/AIGf8iY//X1J/Ja4P9oH/kK6V/1xk/mK7z4Gf8iY/wD19SfyWuD/AGgf+QrpX/XGT+Yr8xy//kdy9WfqWZf8iSPoj59PWkpT1pK/Tj8uYUUUUCCopIIZWRpY1cxtuUkAlWxjIz0OCRn0qWiiw7n5n+GP+Ulni7/sFRf+m60r9MK+adM/Z2Gm/tKat+0P/b/mNqtqtsNN+y7dm22ht93n+ad3+q3YEY64zxk/S3B5HSvJyfDTpRmqitdtnt53iqdaVN03e0Un8gooor1jw2FFFFACiv0G8Kf8ixpP/XnB/wCgCvz5FfoN4U/5FjSf+vOD/wBAFfA8ffw6Xqz9D4B/iVfRfmfMvx6/5HC0/wCvGP8A9GS14lXtvx6/5HC0/wCvGP8A9GS14lX0fDf+40vT9T5niX/fqvqFFFFe4zwgooopAcn438b+GPhz4XvvGPjG+TT9K05C8sj9ST91FHVnY4CqBkkjFfnX4C8NeM/2z/ipYfGbx/ZvpHwz8Lzk6Lp0gybx0cHJH8SllBmcZU4ES5wzL9HftOfsyX37Ro0SAeMJPD1jo3nMbYWhuo5pZNuJCPPiAZQCBkHqcdTnyPS/2MPjRo9hbaTpX7Q2u2tlZxpDDBDb3EccccY2oiKL8ABRgKB07dK+YzKniataMXSbpLs1q/PU+tyueEpUHJVUqr01T0Xlp1P0O+tFQW0Tw20UMjmV40VS54LEDBP41PX00dj5OS1CiiimIKWkpaKnwv5mtL418j9HLH/jxg/3F/lXzb+0F/x9aN/uXH/slfSVj/x4wf7i/wAq+bf2gv8Aj60b/cuP/ZK/GeGf+RjD1f6n7FxN/wAi1+iPnU9aSlPWkr9mPxgKKKKACvy//bYMWkftDfBDxJrOI9Ghv4TI74EYEF7A8xJ6ABGBOa/UCvEfjz8CfCvx98GDwp4ikeyuLaXz7K9iUNJbzgEZCnAdGBw6EgNgHIIBrys6wkq1Bwhq00/uPXyLGQoYhTqaJ3X3qx4F/wAFD9R0+2/Z6azumXz77VLNLZSfmZ13uxGeuFVgfr9BX0r8CrS70/4J+AbO/UpcQaBpiyKwwVZbWMbT7jpXyNpH7EPijxBruiXHxu+JF3400Pw4QLTT2jkQMox8ru8rEBgoD4BZgANwxX6GqqIgRAFUDAA6AYxgYrmy+hWnXniaseVNJW9DrzKvRhh6eEoz5rNtv19R1FFFe8fOhRRRQAV9/eCP+RO0P/rxt/8A0WtfANff3gj/AJE7Q/8Arxt//Ra18Dx9/Cp+rP0DgH+LU9P1Oqooor8xP1AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//0P38ooooA8h+Nv8AyI8n/XeL/wBCr41r7N+NEU0/guWOBGlbzojhQSeG9BXyB/Zmo/8APrL/AN8N/hX6xwVVisI031PybjWlKWLul0KNFXv7M1H/AJ9Zf++G/wAKP7M1H/n1l/74b/CvsHWh3R8aqE+zKNFXv7M1H/n1l/74b/Cj+zNR/wCfWX/vhv8ACj2sO4ewn2ZR75o/rV7+zNR/59Zf++G/wo/szUf+fWX/AL4b/Cj2sNrh7Ce6iUeuQehoq9/Zmo/8+sv/AHw3+FH9maj/AM+sv/fDf4Ue1h3Qewn2ZRoq9/Zmo/8APrL/AN8N/hR/Zmo/8+sv/fDf4Ue1h3D2E+zKNFXv7M1H/n1l/wC+G/wo/szUf+fWX/vhv8KPaw7h7CfZn1j8DP8AkTH/AOvqT+S1wf7QP/IV0r/rjJ/MV6B8EoZrfwc6TxtGxupDhhg4wvYiuF+PVrdXGqaW0ELyhYZASilsfMPQGvzLAVEs6lJvS7P0/MIt5LGKWtkfO560lXv7M1H/AJ9Zf++G/wAKP7M1H/n1l/74b/Cv032sO6PzD2M/5WUaKvf2ZqP/AD6y/wDfDf4Uf2ZqP/PrL/3w3+FHtYdw9hPsyjRV7+zNR/59Zf8Avhv8KP7M1H/n1l/74b/Cj2sO4ewn2ZSye1JV7+zNR/59Zf8Avhv8KP7M1H/n1l/74b/Cn7aG/MDoT25WUaKvf2ZqP/PrL/3w3+FH9maj/wA+sv8A3w3+FL20O6D2E+zKNFXv7M1H/n1l/wC+G/wo/szUf+fWX/vhv8KPaw7h7CfZlIV+g3hT/kWNJ/684P8A0AV8DDTdSz/x6y8f7Df4V99+FlKeGtJVxgi0gBB7fIK+C48qKVOnZ9WfoHAcJRqVOZW0X5nzH8ev+RwtP+vGP/0ZLXiVe6/HO0u7jxdavBA8iiyjGVUkZEkh7e1eL/2ZqP8Az6y/98N/hX0XDlWP1Gkm+n6nzfEdKTx1Vpdf0KVFXv7M1H/n1l/74b/Cj+zNR/59Zf8Avhv8K9v20O54fsJ9mUaKvf2ZqP8Az6y/98N/hR/Zmo/8+sv/AHw3+FHtYdw9hPsyj9eaMA9avf2ZqP8Az6y/98N/hR/Zmo/8+sv/AHw3+FHtYd/xD2E+34FGir39maj/AM+sv/fDf4Uf2ZqP/PrL/wB8N/hR7aHcPYT7Mo0Ve/szUf8An1l/74b/AAo/szUf+fWX/vhv8KPaw7h7CfZlGlq7/Zmo/wDPrL/3w3+FH9m6j/z6S/8AfB5/T3qateHK9TSlRnzrQ/Q+x/48YP8AcX+VfNv7QX/H1o3+5cf+yV9JWQIsoR0+Rf5V86fHy1urm50c28Ly7VnB2qWxnZjoK/HeGpJZhFvu/wBT9f4li3lzS7I+bj1pKvf2ZqP/AD6y/wDfDf4Uf2ZqP/PrL/3w3+FfsntYd0fjnsJ/yso0Ve/szUf+fWX/AL4b/Cj+zNR/59Zf++G/wo9rDuHsJ9mUaKvf2ZqP/PrL/wB8N/hR/Zmo/wDPrL/3w3+FNVo9JB7CfWJRoq9/Zmo/8+sv/fDf4Uf2ZqP/AD6y/wDfDf4Ue2h3/Eboz7fgUaKvf2ZqP/PrL/3w3+FH9maj/wA+sv8A3w3+FL2sO4vYT7FGir39maj/AM+sv/fDf4Uf2ZqP/PrL/wB8N/hR7WHcPYT7Mo19/eCP+RO0P/rxt/8A0WtfB/8AZmo/8+sv/fDf4V95eC1aPwhokbgqwsrcEHgg+WvFfBceTjKlTs+r/I++4DpyjVqcytovzOoooor80P00KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/0f38ooooAjIpcKOoFOx3paBWQ3aO1GwU6incdhuwUbBTqKLhYbsFGwU6ii4WG7BRsFOoouFhuwUbBTqKLhYbsFGwU6ii4WGBRnj8qCin5iOafRikhW0sM2Cl2CnUUXHYbsFGwU6incLDdgo2CnUUXCw3YKNgp1FFwsN2CjYKdRRcLDdgo2CnUUXCxCUU9QKeMY+lPxSYApCUUthm1c5xS4X0p2BS4oQ2hm0dqXYKdRTuKw3YKNgp1FFx2G7BRsFOoouFhuwUbBTqKLhYbsFGwU6ii4WG7V7Ck2r2Ap9GKQuVDfamlQwyR0p+BQVBoHYaFGKXYKdRTuKw3YKNgp1FFx2G7BRsFOoouFhuwUbBTqKLhYbsFGwU6ii4WG7BRsFOoouFiIop4IFOAAFOxmjApCSQtFFFAwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//S/fyiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAOB+IXxQ+H3wo0m2174k+ILPw5p17dJZQXF5KIkkuZFd0iUnqzKjEDvg131fk3/AMFiRt/Zv8HSjjy/G+lt/wCSd8K/WSgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9P9/KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA/Jv/gsf8v7L3h6X/nl4w01v/JW8/wAa/WSvyd/4LJD/AIxN02QdY/FOnN/5L3Y/rX6wg5AI70ALRRTNxzigB9FNGfWnUAFFFFABRRRQAUUUUAFFFFABRTN2CRT6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA/9T9/KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA/KX/gsam79kaBv7viTTj/5CuB/Wv1StH8y0hk/vIp/MV+W//BYZN37IBb+54h00/wDjsw/rX6faM/maRYyf3oIj+aigDSr8ef8Agr/8VPiZ8LPh98Pb74aeK9U8KXF/ql3HcS6XeTWbzIkClVcwspYA8gE8V+w1fh5/wW7/AOSa/DI+mr33/pOtAHkPxh+FP7en7NnwVh/aR0n9ozVPEunWcFjdXlnd3NzI0Ud88UcZSK7e4hmw8qhgwU4yQD0r9IPg9+3R4Ju/2OvDv7TPxzv4NBe586yu47dGY3GoW07wbLWEbmdpQnmbQSEBO4hVJH4lftm+Cv2tPhl4B+HVj8cviZe+L/h14phgeOKyZlgtzCkciwTRERpJKsTBot5IJVsEbSa9K/4KBeBfhv8AD/wl+y74Y8C3ct18Hmsbq4trkyb5Llbq4tri8uZWUKPNljlVuAMHIUKBgAH6h/Cf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oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9b9/KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApMClooA/lO/4Ksfs2f8ACmPj6/xF8PWvleFviOZb9Ai4jg1NCDexcdN7Msy9M72UDCGv0I/4I5/s4jwd8N9V/aG8R2u3VfGe6y0reuGi0q3k/eOM8j7ROnT+7EjDhq/RL9q/9mzwz+1R8I7v4YeIpzp832mC8sb5UDyWlxA3LoD13xF4yPRyeoFe7+FfDGheC/DOk+EPDNqtjpGiWsFlaQJ92KC3QJGo+igc96AOgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//X/fyiiigAopucUvX2oAWiiigAooooAKKKKACim5x1pMmgB9FJmjIouAtFJnNB+tK4C0UUmTTAWiiigAooooAKKKKACik/SgmgBaKbz60uc8UXAWikyKMii4BgUtN5paLgLRSUZoAWiiigAooooAKKKKACikJxRk0rgLRSZoyKdwFopMijIouAtFFNyaAHUUUUAFFFFABRRRQAUU3JoyTQA6im5OBS55oAWikzRkUXAWim5pc5FAWFooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9D9/KKKKAPLfiN8QZ/AosvKsRd/bDJnc+wLs2+gPXdXmX/DQV730aP/AL/n/wCIq3+0H00Qe9x/7Tr5rFfpnDXD+Er4SNSrC7179/U/L+I+IMXQxcqdKei9D6J/4aDvf+gNH/3/AD/8RR/w0He/9AaP/v8An/4ivnaivf8A9VcB/wA+/wAWeF/rXj/+fn4I+if+Gg73/oDR/wDf8/8AxFH/AA0He/8AQGj/AO/5/wDiK+dqKP8AVXAf8+/xf+Yf614//n5+C/yPon/hoO9/6A0f/f8AP/xFH/DQd7/0Bo/+/wCf/iK+dqKP9VcB/wA+/wAWH+teP/5+fgj6Li+P968qodGQBmA/1xJwe4Gyu81v4nz6Rq9zpq2CyLA+zd5hGfw218fW/wDx8RD1Zf517d41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)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "575ogsJhFDIo" - }, - "source": [ - "### Shift\n", - "The shift operation is the same as rotation, but we prepend the encrypted scalar zero when we move the bits to the right." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "TRAFRZime-Jv" - }, - "outputs": [], - "source": [ - "def right_rotate_list_of_chunks(list_to_rotate, amount):\n", - " return np.concatenate((list_to_rotate[-amount:], list_to_rotate[:-amount]))\n", - "\n", - "\n", - "def right_shift_list_of_chunks(list_to_rotate, amount):\n", - " return np.concatenate(([0] * list_to_rotate[-amount:].shape[0], list_to_rotate[:-amount]))\n", - "\n", - "\n", - "def left_shift_list_of_chunks(list_to_rotate, amount):\n", - " return np.concatenate((list_to_rotate[amount:], [0] * list_to_rotate[:amount].shape[0]))\n", - "\n", - "\n", - "def rotate_less_than_width(chunks, shift):\n", - " raised_low_bits = fhe.univariate(lambda x: (x % 2**shift) << (WIDTH - shift))(chunks)\n", - " shifted_raised_low_bits = right_rotate_list_of_chunks(raised_low_bits, 1)\n", - "\n", - " high_bits = chunks >> shift\n", - " return shifted_raised_low_bits + high_bits\n", - "\n", - "\n", - "def right_rotate(chunks, rotate_amount):\n", - " x = rotate_amount // WIDTH\n", - " y = rotate_amount % WIDTH\n", - " rotated_chunks = right_rotate_list_of_chunks(chunks, x) if x != 0 else chunks\n", - " rotated = rotate_less_than_width(rotated_chunks, y) if y != 0 else rotated_chunks\n", - "\n", - " return rotated\n", - "\n", - "\n", - "def right_shift(chunks, shift_amount):\n", - " x = shift_amount // WIDTH\n", - " y = shift_amount % WIDTH\n", - " shifted_chunks = right_shift_list_of_chunks(chunks, x) if x != 0 else chunks\n", - "\n", - " if y != 0:\n", - " # shift within chunks\n", - " raised_low_bits = fhe.univariate(lambda x: (x % 2**y) << (WIDTH - y))(shifted_chunks)\n", - " shifted_raised_low_bits = right_shift_list_of_chunks(raised_low_bits, 1)\n", - " high_bits = shifted_chunks >> y\n", - " result = shifted_raised_low_bits + high_bits\n", - " else:\n", - " result = shifted_chunks\n", - " return result" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "SKg8mKFOPXSV" - }, - "source": [ - "### Modular 32-bit Addition\n", - "Modular 32-bit addition is frequently used in SHA256. While Concrete supports additions of 32-bit numbers, modulizing the result requires a lookup table which is too large for Concrete. Hence, the addition must be done over chunks.\n", - "\n", - "Below is the function to add two 32-bit numbers mod $2^{32}$." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "id": "EJEPvp2wQms9" - }, - "outputs": [], - "source": [ - "def add_two_32_bits(a, b):\n", - " added = np.sum([a, b], axis=0)\n", - "\n", - " for i in range(NUM_CHUNKS):\n", - " results = added % (2**WIDTH)\n", - " if i < NUM_CHUNKS - 1:\n", - " carries = added >> WIDTH\n", - " added = left_shift_list_of_chunks(carries, 1) + results\n", - "\n", - " return results" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "id": "Uo6o_QMQn_fw" - }, - "outputs": [], - "source": [ - "# Testing the addition function, adding four 32-bit numbers\n", - "test_inputs = np.random.randint(0, 2**32, size=(2,))\n", - "input_chunks = break_down_data(test_inputs, 32)\n", - "\n", - "assert chunks_to_uint32(add_two_32_bits(input_chunks[0], input_chunks[1])) == np.sum(\n", - " test_inputs\n", - ") % (2**32)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "IOr8DTRJRYTl" - }, - "source": [ - "Adding two 4-bit numbers results in a 5-bit number. We then use two lookup tables:\n", - "\n", - "* `extract_carry` which extracts the carry of adding two chunks\n", - "* `extract_result` which extracts the 4-bit chunk which results from adding two chunks (without the carry)\n", - "\n", - "Each carry must now be added to the chunk next chunk and this process is repeated for as many chunks as there are. The figure below illustrates this process.\n", - "\n", - "![add-chunks.png](data:image/png;base64,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3QgggAACCCCAAAJeCRBAe8XGRggggAACCCCAAALBKkAAHaxHnnYjgAACCCCAAAIIeCVAAO0VGxshgAACCCCAAAIIBKsAAXSwHnnajQACCCCAAAIIIOCVAAG0V2xshAACCCCAAAIIIBCsAgTQwXrkaTcCCCCAAAIIIICAVwIE0F6xsRECCCCAAAIIIIBAsAoQQAfrkafdCCCAAAIIIIAAAl4JEEB7xcZGCCCAAAIIIIAAAsEqQAAdrEeediOAAAIIIIAAAgh4JUAA7RUbGyGAAAIIIIAAAggEqwABdLAeedqNAAIIIIAAAggg4JUAAbRXbGyEAAIIIIAAAgggEKwCBNDBeuRpNwIIIIAAAggggIBXAgTQXrGxEQIIIIAAAggggECwChBAB+uRp90IIIAAAggggAACXgkQQHvFxkYIIIAAAggggAACwSpAAB2sR552I4AAAggggAACCHglQADtFRsbIYAAAggggAACCASrAAF0sB552o0AAggggAACCCDglQABtFdsbIQAAggggAACCCAQrAIE0MF65Gk3AggggAACCCCAgFcCBNBesbERAggggAACCCCAQLAKEEAH65Gn3QgggAACCCCAAAJeCRBAe8XGRggggAACCCCAAALBKkAAHaxHnnYjgAACCCCAAAIIeCUQ7tVWbOR3Ais3nvC7OlGh7hSYP7VPdxZP2QgggAACCCDQpgABdJs0rEAgSASWvrg/SFpKMw2BO68Y3FUUExYu66qiKMcSAttX3Nol9cyZdnOXlEMhVhE4sOEJq1S1g/VkCEcHociGAAIIIIAAAggggIBdgACa8wABBBBAAAEEEEAAAQ8ECKA9wCIrAggggAACCCCAAAIE0JwDVhU4a2Ty2WNSw0JDXBugxKS4iMgITm9XG1IQQAABBBBAoLMCPETYWUG270KB5LiIySOSnQpc93FBTV2jU6LeJsZGRISHhob2anBZObx/XE5mbG5+1cf7S5RTMXZ4WGhtvUs+10JJCQ6By87NjIoIfWndsbr6JqcWh4eFpCVGllbUV9Y0OK3iLQISWPbTi+JjI2++77XK6jonEFtU+NABKUdPlhWWVDmt4i0C82af+d0bvvrSGxueWb7aVSOnf+/IyPA9+4+6riLFPwUIoP3zuARprdRnrJhYja/rXLDb0GiPipqaWmKjUdkJ2X1idhwoOZTHt1rAnlqZ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Nsr0TioBCUhAAhKQgAQkIIHSIqCALi1/aa0EJCABCUhAAhKQQCMTUEA3sgOcXgISkIAEJCABCUigtAiYA11a/mpYa4vtvbSGXa2jS0ACEpCABCQggZwIuAtHTtjsJAEJSEACEpCABCSQVAKmcCTV865bAhKQgAQkIAEJSCAnAgronLDZSQISkIAEJCABCUggqQQU0En1vOuWgAQkIAEJSEACEsiJgAI6J2x2koAEJCABCUhAAhJIKgEFdFI977olIAEJSEACEpCABHIioIDOCZudJCABCUhAAhKQgASSSkABnVTPu24JSEACEpCABCQggZwIKKBzwmYnCUhAAhKQgAQkIIGkElBAJ9XzrlsCEpCABCQgAQlIICcCCuicsNlJAhKQgAQkIAEJSCCpBBTQSfW865aABCQgAQlIQAISyImAAjonbHaSgAQkIAEJSEACEkgqAQV0Uj3vuiUgAQlIQAISkIAEciKggM4Jm50kIAEJSEACEpCABJJKQAGdVM+7bglIQAISkIAEJCCBnAgooHPCZicJSEACEpCABCQggaQSUEAn1fOuWwISkIAEJCABCUggJwIK6Jyw2UkCEpCABCQgAQlIIKkEFNBJ9bzrloAEJCABCUhAAhLIiYACOidsdpKABCQgAQlIQAISSCoBBXRSPe+6JSABCUhAAhKQgARyIqCAzgmbnSQgAQlIQAISkIAEkkpAAZ1Uz7tuCUhAAhKQgAQkIIGcCCigc8JmJwlIQAISkIAEJCCBpBJQQCfV865bAhKQgAQkIAEJSCAnAgronLDZSQISkIAEJCABCUggqQQU0En1vOuWgAQkIAEJSEACEsiJgAI6J2x2koAEJCABCUhAAhJIKgEFdFI977olIAEJSEACEpCABHIioIDOCZudJCABCSSJwPe+970BAwZUVFTwTTlJS3etEpCABNIQqNi0aVOaaqskIAEJSEACmwkMGjRo6tSpGzZsCDxatGjRr1+/iRMnikcCEpBAYgkYgU6s6124BCQggW0TGDFiRFw902HdunXUDB48eNudbSEBCUigTAkYgS5Tx7osCUhAAvkgQNpG2mH69u07c+bMtKeslIAEJFD2BIxAl72LXaAEJCCB3AkglNN2njVrVtp6KyUgAQkkgYACOgledo0SkIAEciTQpUuXpk2bpnSm5tJLL02p9FACEpBAcgiYwpEcX7tS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)\n", - "\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "4-dQk0wbtPOe" - }, - "source": [ - "The benefit of this addition algorithm is that it can be extended to the case where more two 32-bit numbers are added. The only difference is that the carry from the first iteration of the loop can be larger than 1. Specifically, by adding $k$ 4-bit numbers, the carry can be as big as $\\log_2 k$. For correctness, $\\log_2 k$ must be less than 4 or $k<16$.\n", - "\n", - "In our implementation of SHA-256, we only have two input and four input additions, so we only implement those.\n", - "\n", - "For four input addition, he first iteration of the loop, we use a different lookup table that extract a 2-bit carry and rest of the chunk. The rest of the algorithm does not change." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "obO8wHRbXHfj" - }, - "outputs": [], - "source": [ - "def add_four_32_bits(a, b, c, d):\n", - " added = np.sum([a, b, c, d], axis=0)\n", - "\n", - " # First iteration of the loop is seperated\n", - " carries = added >> WIDTH\n", - " results = added % (2**WIDTH)\n", - " shifted_carries = left_shift_list_of_chunks(carries, 1)\n", - " added = shifted_carries + results\n", - "\n", - " for i in range(1, NUM_CHUNKS):\n", - " results = added % (2**WIDTH)\n", - "\n", - " # In the last iteration, carries need not be calculated\n", - " if i != NUM_CHUNKS - 1:\n", - " carries = added >> WIDTH\n", - " shifted_carries = left_shift_list_of_chunks(carries, 1)\n", - " added = shifted_carries + results\n", - "\n", - " return results" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "zcwnDdPFdqE1" - }, - "outputs": [], - "source": [ - "# Testing the addition function, adding four 32-bit numbers\n", - "\n", - "for _ in range(1000):\n", - " test_inputs = np.random.randint(0, 2**32, size=(4,))\n", - " input_chunks = break_down_data(test_inputs, 32)\n", - "\n", - " assert chunks_to_uint32(\n", - " add_four_32_bits(input_chunks[0], input_chunks[1], input_chunks[2], input_chunks[3])\n", - " ) == np.sum(test_inputs) % (2**32)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "1g6eEdhGoJl9" - }, - "source": [ - "## Operations for SHA-256\n", - "\n", - "Using the basic operations from the previous section, we can now implement all the necessary functions for SHA256" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "id": "uo7HfO1DpVFK" - }, - "outputs": [], - "source": [ - "# Used in the expansion\n", - "\n", - "\n", - "def s0(w):\n", - " return right_rotate(w, 7) ^ right_rotate(w, 18) ^ right_shift(w, 3)\n", - "\n", - "\n", - "def s1(w):\n", - " return right_rotate(w, 17) ^ right_rotate(w, 19) ^ right_shift(w, 10)\n", - "\n", - "\n", - "# Used in main loop\n", - "\n", - "\n", - "def S0(a_word): # noqa: N802\n", - " return right_rotate(a_word, 2) ^ right_rotate(a_word, 13) ^ right_rotate(a_word, 22)\n", - "\n", - "\n", - "def S1(e_word): # noqa: N802\n", - " return right_rotate(e_word, 6) ^ right_rotate(e_word, 11) ^ right_rotate(e_word, 25)\n", - "\n", - "\n", - "def Ch(e_word, f_word, g_word): # noqa: N802\n", - " return (e_word & f_word) ^ ((2**WIDTH - 1 - e_word) & g_word)\n", - "\n", - "\n", - "def Maj(a_word, b_word, c_word): # noqa: N802\n", - " return (a_word & b_word) ^ (a_word & c_word) ^ (b_word & c_word)\n", - "\n", - "\n", - "def main_loop(args, w_i_plus_k_i):\n", - " a, b, c, d, e, f, g, h = args\n", - " temp1 = add_four_32_bits(h, S1(e), Ch(e, f, g), w_i_plus_k_i)\n", - " temp2 = add_two_32_bits(S0(a), Maj(a, b, c))\n", - " new_a = add_two_32_bits(temp1, temp2)\n", - " new_e = add_two_32_bits(d, temp1)\n", - " return np.array([new_a, a, b, c, new_e, e, f, g])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "biM997KmvwUL" - }, - "source": [ - "We also need a function to pad the input as the first step of SHA256." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "fZ-3sEH5vopA" - }, - "source": [ - "Moreover, we need a function to parse the input given to the program. The input is given as bytes, but the chunks might be smaller. We extract smaller chunks from bytes using lookup tables." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "L4leg-z_skkU" - }, - "source": [ - "## Bringing it all together\n", - "Using all the components from the above, we can implement SHA256 as shown below." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "tmSfvdpyrwUx" - }, - "outputs": [], - "source": [ - "K = [\n", - " 0x428A2F98,\n", - " 0x71374491,\n", - " 0xB5C0FBCF,\n", - " 0xE9B5DBA5,\n", - " 0x3956C25B,\n", - " 0x59F111F1,\n", - " 0x923F82A4,\n", - " 0xAB1C5ED5,\n", - " 0xD807AA98,\n", - " 0x12835B01,\n", - " 0x243185BE,\n", - " 0x550C7DC3,\n", - " 0x72BE5D74,\n", - " 0x80DEB1FE,\n", - " 0x9BDC06A7,\n", - " 0xC19BF174,\n", - " 0xE49B69C1,\n", - " 0xEFBE4786,\n", - " 0x0FC19DC6,\n", - " 0x240CA1CC,\n", - " 0x2DE92C6F,\n", - " 0x4A7484AA,\n", - " 0x5CB0A9DC,\n", - " 0x76F988DA,\n", - " 0x983E5152,\n", - " 0xA831C66D,\n", - " 0xB00327C8,\n", - " 0xBF597FC7,\n", - " 0xC6E00BF3,\n", - " 0xD5A79147,\n", - " 0x06CA6351,\n", - " 0x14292967,\n", - " 0x27B70A85,\n", - " 0x2E1B2138,\n", - " 0x4D2C6DFC,\n", - " 0x53380D13,\n", - " 0x650A7354,\n", - " 0x766A0ABB,\n", - " 0x81C2C92E,\n", - " 0x92722C85,\n", - " 0xA2BFE8A1,\n", - " 0xA81A664B,\n", - " 0xC24B8B70,\n", - " 0xC76C51A3,\n", - " 0xD192E819,\n", - " 0xD6990624,\n", - " 0xF40E3585,\n", - " 0x106AA070,\n", - " 0x19A4C116,\n", - " 0x1E376C08,\n", - " 0x2748774C,\n", - " 0x34B0BCB5,\n", - " 0x391C0CB3,\n", - " 0x4ED8AA4A,\n", - " 0x5B9CCA4F,\n", - " 0x682E6FF3,\n", - " 0x748F82EE,\n", - " 0x78A5636F,\n", - " 0x84C87814,\n", - " 0x8CC70208,\n", - " 0x90BEFFFA,\n", - " 0xA4506CEB,\n", - " 0xBEF9A3F7,\n", - " 0xC67178F2,\n", - "]\n", - "H = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "id": "NHGCiC-Gk_tw" - }, - "outputs": [], - "source": [ - "k_in = reshape_data(break_down_data(K, 32))\n", - "h_in = reshape_data(break_down_data(H, 32))" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "id": "yTiMkmBsHmKy" - }, - "outputs": [], - "source": [ - "def uint64_to_bin(uint64: int):\n", - " return \"\".join([str(uint64 >> i & 1) for i in range(63, -1, -1)])\n", - "\n", - "\n", - "def sha256_preprocess(text):\n", - " \"\"\"\n", - " Takes a message of arbitrary length and returns a message\n", - " of length that is a multiple of 512 bits, with the original message padded\n", - " with a 1 bit, followed by 0 bits, followed by the original message length\n", - " in bits\n", - " \"\"\"\n", - " data = text\n", - " # convert to uint4 and group into 32 bit words (8 uint4s)\n", - " # #log (\"data is:\", data, data.shape)\n", - " message_len = data.shape[0] * 8 # denoted as 'l' in spec\n", - " # find padding length 'k'\n", - " k = (((448 - 1 - message_len) % 512) + 512) % 512\n", - " # #log (\"k is:\", k)\n", - " zero_pad_width_in_bits = k\n", - " padstring = \"1\" + \"0\" * zero_pad_width_in_bits + str(uint64_to_bin(message_len))\n", - " # log (\"padstring size:\", len(padstring))\n", - " # log (\"padstring is:\", padstring)\n", - "\n", - " total_size = len(padstring) + message_len\n", - " # log (\"total size:\", total_size)\n", - " assert total_size % 512 == 0\n", - "\n", - " pad = np.array(\n", - " [int(padstring[i : i + 8], 2) for i in range(0, len(padstring), 8)], dtype=np.uint8\n", - " )\n", - " padded = np.concatenate((data, pad))\n", - " words = break_down_data(padded, 8)\n", - " chunks = reshape_data(words)\n", - " return chunks" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "id": "3ox6Zs-ysoLr" - }, - "outputs": [], - "source": [ - "# Number of rounds must be 64 to have correct SHA256\n", - "# If looking to get a faster run, reduce the number of rounds (but it will not be correct)\n", - "\n", - "\n", - "def sha256(data, number_of_rounds=64):\n", - " h_chunks = fhe.zeros((len(h_in), NUM_CHUNKS))\n", - " k_chunks = fhe.zeros((len(k_in), NUM_CHUNKS))\n", - " h_chunks += h_in\n", - " k_chunks += k_in\n", - "\n", - " num_of_iters = data.shape[0] * 32 // 512\n", - " for chunk_iter in range(0, num_of_iters):\n", - "\n", - " # Initializing the variables\n", - " chunk = data[chunk_iter * 16 : (chunk_iter + 1) * 16]\n", - " w = [None for _ in range(number_of_rounds)]\n", - " # Starting the main loop and expansion\n", - " working_vars = h_chunks\n", - " for j in range(0, number_of_rounds):\n", - " if j < 16:\n", - " w[j] = chunk[j]\n", - " else:\n", - " w[j] = add_four_32_bits(w[j - 16], s0(w[j - 15]), w[j - 7], s1(w[j - 2]))\n", - " w_i_k_i = add_two_32_bits(w[j], k_chunks[j])\n", - " working_vars = main_loop(working_vars, w_i_k_i)\n", - "\n", - " # Accumulating the results\n", - " for j in range(8):\n", - " h_chunks[j] = fhe.array(add_two_32_bits(h_chunks[j], working_vars[j]))\n", - " return h_chunks" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "w89rhSOh4In2" - }, - "source": [ - "We can test the correctness of this function as below (this is not in encrypted form yet)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "006LZp7c0yBA", - "outputId": "31588127-23e9-4b49-e481-d14842e336e7" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " SHA256: a412c46b0be134c593b0ad520d4a4c4e1d8aecca799be0be2c4d233ccf455cb7\n", - "Our SHA256: a412c46b0be134c593b0ad520d4a4c4e1d8aecca799be0be2c4d233ccf455cb7\n", - "Match: True\n" - ] - } - ], - "source": [ - "import hashlib\n", - "\n", - "text = (\n", - " b\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. \"\n", - " b\"Curabitur bibendum, urna eu bibendum egestas, neque augue eleifend odio, \"\n", - " b\"et sagittis viverra. and more than 150\"\n", - ")\n", - "\n", - "result = sha256(sha256_preprocess(np.frombuffer(text, dtype=np.uint8)))\n", - "\n", - "m = hashlib.sha256()\n", - "m.update(text)\n", - "\n", - "print(\" SHA256:\", m.hexdigest())\n", - "print(\"Our SHA256:\", chunks_to_hexarray(result))\n", - "print(\"Match:\", chunks_to_hexarray(result) == m.hexdigest())" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "id": "1uHN9GXgla_z" - }, - "outputs": [], - "source": [ - "class HomomorphicSHA: # noqa: N802\n", - " circuit: fhe.Circuit\n", - "\n", - " def __init__(self, input_size_in_bytes=150, number_of_rounds=64) -> None:\n", - " self.input_size_in_bytes = input_size_in_bytes\n", - " assert 0 <= number_of_rounds <= 64, \"Number of rounds must be betweem zero and 64\"\n", - " self.number_of_rounds = number_of_rounds\n", - " inputset = [\n", - " sha256_preprocess(np.random.randint(0, 2**8, size=(input_size_in_bytes,)))\n", - " for _ in range(100)\n", - " ]\n", - " # Compilation of the circuit should take a few minutes\n", - " compiler = fhe.Compiler(\n", - " lambda data: sha256(data, self.number_of_rounds), {\"data\": \"encrypted\"}\n", - " )\n", - " self.circuit = compiler.compile(\n", - " inputset=inputset,\n", - " configuration=fhe.Configuration(\n", - " enable_unsafe_features=True,\n", - " use_insecure_key_cache=True,\n", - " insecure_key_cache_location=\".keys\",\n", - " dataflow_parallelize=platform.system() != \"Darwin\",\n", - " ),\n", - " verbose=False,\n", - " )\n", - "\n", - " def getSHA(self, data): # noqa: N802\n", - " assert (\n", - " len(data) == self.input_size_in_bytes\n", - " ), f\"Input size is not correct, should be {self.input_size_in_bytes} bytes/characters\"\n", - " return self.circuit.encrypt_run_decrypt(sha256_preprocess(data))\n", - "\n", - " def getPlainSHA(self, data): # noqa: N802\n", - " return sha256(sha256_preprocess(data), self.number_of_rounds)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "SpxY6dScee-k" - }, - "source": [ - "Now we are ready to compile the circuit! Note that **the compilation will take a long time**, so if you are looking to get a test run, you can set the number of rounds to something smaller than 64." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "id": "P0cMOZUGee-k" - }, - "outputs": [], - "source": [ - "# Warning: This will compile the circuit and will take a few minutes\n", - "\n", - "input_size_in_bytes = 150\n", - "running_small_example = True\n", - "\n", - "if running_small_example:\n", - " number_of_rounds = 2\n", - " sha = HomomorphicSHA(input_size_in_bytes, number_of_rounds)\n", - "else:\n", - " sha = HomomorphicSHA(input_size_in_bytes)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "zz1rd7VWee-k" - }, - "source": [ - "And after compilation, we are ready to run the circuit. Remember that the input size has to match what you gave in the previous cell. Our function will check this first to make sure the input is of the correct size. " - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "EkF0UxTcv_cQ", - "outputId": "c4e2c710-02bc-40e2-a921-4a29ac88380b" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This cell is disabled. It can takes hours. If you want to run this cell, set accept_a_very_long_run=True\n" - ] - } - ], - "source": [ - "# WARNING: This takes a LONG time\n", - "accept_a_very_long_run = False\n", - "if not accept_a_very_long_run:\n", - " print(\n", - " \"This cell is disabled. It can takes hours. If you want to run this \"\n", - " \"cell, set accept_a_very_long_run=True\"\n", - " )\n", - "else:\n", - " text = (\n", - " b\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. \"\n", - " b\"Curabitur bibendum, urna eu bibendum egestas, neque augue eleifend odio, \"\n", - " b\"et sagittis viverra.\"\n", - " )\n", - " input_bytes = np.frombuffer(text, dtype=np.uint8)\n", - " encrypted_evaluation = sha.getSHA(input_bytes)\n", - "\n", - " print(\"Encrypted Evaluation: \", chunks_to_hexarray(encrypted_evaluation))\n", - " print(\" Plain Evaluation: \", chunks_to_hexarray(sha.getPlainSHA(input_bytes)))" - ] - } - ], - "metadata": { - "colab": { - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3.10.7 64-bit", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.7" - }, - "vscode": { - "interpreter": { - "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" - } - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} From bd9332ba615c8ab6f80ed5f23313065af7054d74 Mon Sep 17 00:00:00 2001 From: Quentin Bourgerie Date: Fri, 8 Nov 2024 15:07:39 +0100 Subject: [PATCH 2/4] fix(compiler): Macos build --- .../concrete-compiler/compiler/lib/ClientLib/ClientLib.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compilers/concrete-compiler/compiler/lib/ClientLib/ClientLib.cpp b/compilers/concrete-compiler/compiler/lib/ClientLib/ClientLib.cpp index 36f49e8ec..f6954da88 100644 --- a/compilers/concrete-compiler/compiler/lib/ClientLib/ClientLib.cpp +++ b/compilers/concrete-compiler/compiler/lib/ClientLib/ClientLib.cpp @@ -236,9 +236,9 @@ Result importTfhersInteger(llvm::ArrayRef buffer, lwe.setIntegerPrecision(64); // dimensions lwe.initAbstractShape().setDimensions( - ::kj::ArrayPtr(abstractDims.data(), abstractDims.size())); + ::kj::ArrayPtr(abstractDims.data(), abstractDims.size())); lwe.initConcreteShape().setDimensions( - ::kj::ArrayPtr(concreteDims.data(), concreteDims.size())); + ::kj::ArrayPtr(concreteDims.data(), concreteDims.size())); // encryption auto encryption = lwe.initEncryption(); encryption.setLweDimension((uint32_t)integerDesc.lwe_size - 1); From 84cb1b5b737a4b7971e5f7be00765937d8c702f4 Mon Sep 17 00:00:00 2001 From: Bourgerie Quentin Date: Fri, 18 Oct 2024 18:35:32 +0200 Subject: [PATCH 3/4] refactor(ci): Refactoring CI workflows --- .github/workflows/action-pin.yaml | 19 -- .github/workflows/action_compliance.yaml | 32 ++ .github/workflows/actionlint.yml | 16 - .github/workflows/check_commit_signature.yml | 11 - ...{block_merge.yml => commit_compliance.yml} | 19 +- .github/workflows/compiler_benchmark.yml | 142 -------- .../workflows/compiler_build_and_test_cpu.yml | 158 --------- ...ompiler_build_and_test_cpu_distributed.yml | 89 ----- .../workflows/compiler_build_and_test_gpu.yml | 91 ------ .../workflows/compiler_format_and_linting.yml | 39 --- .../compiler_macos_build_and_test.yml | 104 ------ .../compiler_publish_docker_images.yml | 191 ----------- .../workflows/concrete_compiler_benchmark.yml | 167 ++++++++++ ...oncrete_compiler_publish_docker_images.yml | 218 +++++++++++++ .../workflows/concrete_compiler_test_cpu.yml | 181 +++++++++++ ...concrete_compiler_test_cpu_distributed.yml | 109 +++++++ .../workflows/concrete_compiler_test_gpu.yml | 106 ++++++ .../concrete_compiler_test_macos_cpu.yml | 89 +++++ .github/workflows/concrete_cpu_test.yml | 53 +-- .github/workflows/concrete_ml_test.yml | 138 ++++++++ .github/workflows/concrete_ml_tests.yml | 112 ------- .../{optimizer.yml => concrete_optimizer.yml} | 61 ++-- .../workflows/concrete_python_benchmark.yml | 54 ++-- .github/workflows/concrete_python_checks.yml | 16 - .../concrete_python_finalize_release.yml | 79 +++++ .../concrete_python_push_docker_image.yml | 55 ---- ...se.yml => concrete_python_release_cpu.yml} | 177 ++++++---- .../workflows/concrete_python_release_gpu.yml | 217 +++++++++---- .../workflows/concrete_python_test_macos.yml | 96 +++--- .../workflows/concrete_python_tests_linux.yml | 296 +++++++++-------- .github/workflows/docker-lint.yml | 18 -- .github/workflows/docker_compliance.yml | 29 ++ .github/workflows/linelint.yml | 18 -- .github/workflows/main.yml | 303 ------------------ .github/workflows/markdown_link_check.yml | 20 -- .github/workflows/optimizer_setup/action.yml | 7 +- .../workflows/push_wheels_to_public_pypi.yml | 35 -- .github/workflows/scripts/teardown-check.sh | 10 + .github/workflows/start_slab.yml | 62 ---- ci/ec2_products_cost.json | 1 + ci/slab.toml | 127 +------- compilers/concrete-compiler/compiler/Makefile | 27 -- .../lib/Bindings/Python/requirements_dev.txt | 1 + frontends/concrete-python/Makefile | 3 - .../concrete-python/scripts/checks/checks.sh | 8 - 45 files changed, 1701 insertions(+), 2103 deletions(-) delete mode 100644 .github/workflows/action-pin.yaml create mode 100644 .github/workflows/action_compliance.yaml delete mode 100644 .github/workflows/actionlint.yml delete mode 100644 .github/workflows/check_commit_signature.yml rename .github/workflows/{block_merge.yml => commit_compliance.yml} (62%) delete mode 100644 .github/workflows/compiler_benchmark.yml delete mode 100644 .github/workflows/compiler_build_and_test_cpu.yml delete mode 100644 .github/workflows/compiler_build_and_test_cpu_distributed.yml delete mode 100644 .github/workflows/compiler_build_and_test_gpu.yml delete mode 100644 .github/workflows/compiler_format_and_linting.yml delete mode 100644 .github/workflows/compiler_macos_build_and_test.yml delete mode 100644 .github/workflows/compiler_publish_docker_images.yml create mode 100644 .github/workflows/concrete_compiler_benchmark.yml create mode 100644 .github/workflows/concrete_compiler_publish_docker_images.yml create mode 100644 .github/workflows/concrete_compiler_test_cpu.yml create mode 100644 .github/workflows/concrete_compiler_test_cpu_distributed.yml create mode 100644 .github/workflows/concrete_compiler_test_gpu.yml create mode 100644 .github/workflows/concrete_compiler_test_macos_cpu.yml create mode 100644 .github/workflows/concrete_ml_test.yml delete mode 100644 .github/workflows/concrete_ml_tests.yml rename .github/workflows/{optimizer.yml => concrete_optimizer.yml} (74%) delete mode 100644 .github/workflows/concrete_python_checks.yml create mode 100644 .github/workflows/concrete_python_finalize_release.yml delete mode 100644 .github/workflows/concrete_python_push_docker_image.yml rename .github/workflows/{concrete_python_release.yml => concrete_python_release_cpu.yml} (74%) delete mode 100644 .github/workflows/docker-lint.yml create mode 100644 .github/workflows/docker_compliance.yml delete mode 100644 .github/workflows/linelint.yml delete mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/markdown_link_check.yml delete mode 100644 .github/workflows/push_wheels_to_public_pypi.yml create mode 100755 .github/workflows/scripts/teardown-check.sh delete mode 100644 .github/workflows/start_slab.yml delete mode 100755 frontends/concrete-python/scripts/checks/checks.sh diff --git a/.github/workflows/action-pin.yaml b/.github/workflows/action-pin.yaml deleted file mode 100644 index feb3cb4ba..000000000 --- a/.github/workflows/action-pin.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Action Pin - -on: - pull_request: - push: - branches: - - main - -jobs: - check-action-pin: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - - name: Ensure SHA pinned actions - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@0901cf7b71c7ea6261ec69a3dc2bd3f9264f893e # v3.0.12 - with: - allowlist: | - slsa-framework/slsa-github-generator diff --git a/.github/workflows/action_compliance.yaml b/.github/workflows/action_compliance.yaml new file mode 100644 index 000000000..7fad8b0d9 --- /dev/null +++ b/.github/workflows/action_compliance.yaml @@ -0,0 +1,32 @@ +name: check action compliance + +on: + pull_request: + paths: + - .github/workflows/** + push: + branches: + - main + - 'release/*' + +jobs: + action-pin: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Ensure SHA pinned actions + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@0901cf7b71c7ea6261ec69a3dc2bd3f9264f893e # v3.0.12 + with: + allowlist: | + slsa-framework/slsa-github-generator + + action-lint: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: check-missing-teardown + run: .github/workflows/scripts/teardown-check.sh + - name: actionlint + uses: raven-actions/actionlint@01fce4f43a270a612932cb1c64d40505a029f821 # v2.0.0 diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml deleted file mode 100644 index fbd5087f2..000000000 --- a/.github/workflows/actionlint.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Action Lint - -on: - pull_request: - push: - branches: - - main - -jobs: - action-lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - - name: actionlint - uses: raven-actions/actionlint@01fce4f43a270a612932cb1c64d40505a029f821 # v2.0.0 diff --git a/.github/workflows/check_commit_signature.yml b/.github/workflows/check_commit_signature.yml deleted file mode 100644 index ad2fa4119..000000000 --- a/.github/workflows/check_commit_signature.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Check Commit Signatures - -on: - pull_request: - -jobs: - check-commit-signatures: - runs-on: ubuntu-latest - steps: - - name: Check commit signatures - uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 diff --git a/.github/workflows/block_merge.yml b/.github/workflows/commit_compliance.yml similarity index 62% rename from .github/workflows/block_merge.yml rename to .github/workflows/commit_compliance.yml index 650084583..ac14dbb27 100644 --- a/.github/workflows/block_merge.yml +++ b/.github/workflows/commit_compliance.yml @@ -1,13 +1,10 @@ -# Check commit and PR compliance -name: Check commit message compliance +name: check commit compliance on: pull_request: - types: [opened, synchronize, reopened] jobs: - check-commit-pr: - name: Check commit and PR - runs-on: ubuntu-20.04 + format: + runs-on: ubuntu-latest steps: - name: Check first line uses: gsactions/commit-message-checker@16fa2d5de096ae0d35626443bcd24f1e756cafee # v2.0.0 @@ -19,3 +16,13 @@ jobs: excludeTitle: 'true' # optional: this excludes the title of a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request accessToken: ${{ secrets.GITHUB_TOKEN }} # github access token is only required if checkAllCommitMessages is true + - name: checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: linelint + uses: fernandrone/linelint@8136e0fa9997122d80f5f793e0bb9a45e678fbb1 # 0.0.4 + id: linelint + - name: markdown-link-check + uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # v1 + with: + use-quiet-mode: 'yes' + use-verbose-mode: 'yes' diff --git a/.github/workflows/compiler_benchmark.yml b/.github/workflows/compiler_benchmark.yml deleted file mode 100644 index 0b1754514..000000000 --- a/.github/workflows/compiler_benchmark.yml +++ /dev/null @@ -1,142 +0,0 @@ -# Run benchmarks on an AWS instance for compiler and return parsed results to Slab CI bot. -name: Compiler - Performance benchmarks - -on: - workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} - -env: - CARGO_TERM_COLOR: always - RESULTS_FILENAME: parsed_benchmark_results_${{ github.sha }}.json - CUDA_PATH: /usr/local/cuda-11.8 - GCC_VERSION: 8 - -jobs: - run-benchmarks: - name: Execute end-to-end benchmarks in EC2 - runs-on: ${{ github.event.inputs.runner_name }} - if: ${{ !cancelled() }} - steps: - - name: Instance configuration used - run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - - - name: Get benchmark date - run: | - echo "BENCH_DATE=$(date --iso-8601=seconds)" >> "${GITHUB_ENV}" - - - name: Fetch submodules - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Set up home - # "Install rust" step require root user to have a HOME directory which is not set. - run: | - echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - - - name: Export specific variables (CPU) - if: ${{ !startswith(inputs.instance_type, 'p3.') }} - run: | - echo "CUDA_SUPPORT=OFF" >> "${GITHUB_ENV}" - echo "BENCHMARK_TARGET=run-cpu-benchmarks" >> "${GITHUB_ENV}" - - - name: Export specific variables (GPU) - if: ${{ startswith(inputs.instance_type, 'p3.') }} - run: | - echo "CUDA_SUPPORT=ON" >> "${GITHUB_ENV}" - echo "BENCHMARK_TARGET=run-gpu-benchmarks" >> "${GITHUB_ENV}" - echo "CUDA_PATH=$CUDA_PATH" >> "${GITHUB_ENV}" - echo "$CUDA_PATH/bin" >> "${GITHUB_PATH}" - echo "LD_LIBRARY_PATH=$CUDA_PATH/lib:$LD_LIBRARY_PATH" >> "${GITHUB_ENV}" - echo "CC=/usr/bin/gcc-${{ env.GCC_VERSION }}" >> "${GITHUB_ENV}" - echo "CXX=/usr/bin/g++-${{ env.GCC_VERSION }}" >> "${GITHUB_ENV}" - echo "CUDAHOSTCXX=/usr/bin/g++-${{ env.GCC_VERSION }}" >> "${GITHUB_ENV}" - echo "CUDACXX=$CUDA_PATH/bin/nvcc" >> "${GITHUB_ENV}" - - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Build compiler benchmarks - run: | - set -e - git config --global --add safe.directory '*' - cd compilers/concrete-compiler/compiler - make BINDINGS_PYTHON_ENABLED=OFF CUDA_SUPPORT=${{ env.CUDA_SUPPORT }} build-benchmarks - - - name: Run end-to-end benchmarks - run: | - set -e - cd compilers/concrete-compiler/compiler - make ${{ env.BENCHMARK_TARGET }} - - - name: Upload raw results artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 - with: - name: compiler_${{ github.sha }}_raw - path: compilers/concrete-compiler/compiler/benchmarks_results.json - - - name: Parse results - shell: bash - run: | - COMMIT_DATE="$(git --no-pager show -s --format=%cd --date=iso8601-strict ${{ github.sha }})" - COMMIT_HASH="$(git describe --tags --dirty)" - python3 ./ci/benchmark_parser.py compilers/concrete-compiler/compiler/benchmarks_results.json ${{ env.RESULTS_FILENAME }} \ - --database compiler_benchmarks \ - --hardware ${{ inputs.instance_type }} \ - --project-version ${COMMIT_HASH} \ - --branch ${{ github.ref_name }} \ - --commit-date ${COMMIT_DATE} \ - --bench-date "${{ env.BENCH_DATE }}" \ - --throughput - - - name: Upload parsed results artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 - with: - name: compiler_${{ github.sha }} - path: ${{ env.RESULTS_FILENAME }} - - - name: Checkout Slab repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - repository: zama-ai/slab - path: slab - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Send data to Slab - shell: bash - run: | - echo "Computing HMac on downloaded artifact" - SIGNATURE="$(slab/scripts/hmac_calculator.sh ${{ env.RESULTS_FILENAME }} '${{ secrets.JOB_SECRET }}')" - echo "Sending results to Slab..." - curl -v -k \ - -H "Content-Type: application/json" \ - -H "X-Slab-Repository: ${{ github.repository }}" \ - -H "X-Slab-Command: store_data" \ - -H "X-Hub-Signature-256: sha256=${SIGNATURE}" \ - -d @${{ env.RESULTS_FILENAME }} \ - ${{ secrets.SLAB_URL }} diff --git a/.github/workflows/compiler_build_and_test_cpu.yml b/.github/workflows/compiler_build_and_test_cpu.yml deleted file mode 100644 index e20cc664e..000000000 --- a/.github/workflows/compiler_build_and_test_cpu.yml +++ /dev/null @@ -1,158 +0,0 @@ -name: Compiler - Build and Test (CPU) - -on: - workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - matrix_item: - description: 'Build matrix item' - type: string - -# concurrency: -# group: compiler_build_and_test_cpu-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -env: - DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler - GLIB_VER: 2_28 - -jobs: - BuildAndTest: - name: Build and test compiler in EC2 - runs-on: ${{ github.event.inputs.runner_name }} - if: ${{ !cancelled() }} - steps: - - - name: Instance configuration used - run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - echo "Matrix item: ${{ inputs.matrix_item }}" - - - name: Set up env - run: | - echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - #echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK)" >> "${GITHUB_ENV}" - echo "SSH_AUTH_SOCK_DIR=$(dirname $SSH_AUTH_SOCK)" >> "${GITHUB_ENV}" - - - name: Fetch repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Create build dir - run: mkdir build - - - name: Build compiler - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - id: build-compiler - with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE_TEST }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - -v ${{ github.workspace }}/wheels:/wheels - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket - ${{ env.DOCKER_GPU_OPTION }} - shell: bash - run: | - rustup toolchain install nightly-2024-09-30 - pip install mypy - set -e - cd /concrete/compilers/concrete-compiler/compiler - rm -rf /build/* - make DATAFLOW_EXECUTION_ENABLED=ON CCACHE=ON Python3_EXECUTABLE=$PYTHON_EXEC BUILD_DIR=/build all - echo "Debug: ccache statistics (after the build):" - ccache -s - - - name: Build compiler Dialects docs and check diff - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - id: build-compiler-docs - with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE_TEST }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - -v ${{ github.workspace }}/wheels:/wheels - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket - ${{ env.DOCKER_GPU_OPTION }} - shell: bash - run: | - set -e - cd /concrete/compilers/concrete-compiler/compiler - make BUILD_DIR=/build doc - cd /build/tools/concretelang/docs/concretelang/ - sed -i -e 's/\[TOC\]//' *Dialect.md - for i in `ls *Dialect.md`; do diff $i /concrete/docs/explanations/$i; done; - - - name: Enable complete tests on push to main - if: github.ref == 'refs/heads/main' - run: echo "MINIMAL_TESTS=OFF" >> $GITHUB_ENV - - - name: Enable minimal tests otherwise - if: github.ref != 'refs/heads/main' - run: echo "MINIMAL_TESTS=ON" >> $GITHUB_ENV - - - name: Test compiler - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE_TEST }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - ${{ env.DOCKER_GPU_OPTION }} - shell: bash - run: | - set -e - rustup toolchain install nightly-2024-09-30 - cd /concrete/compilers/concrete-compiler/compiler - pip install pytest - pip install mypy - dnf install -y libzstd libzstd-devel - sed "s/pytest/python -m pytest/g" -i Makefile - mkdir -p /tmp/concrete_compiler/gpu_tests/ - make MINIMAL_TESTS=${{ env.MINIMAL_TESTS }} DATAFLOW_EXECUTION_ENABLED=ON CCACHE=ON Python3_EXECUTABLE=$PYTHON_EXEC BUILD_DIR=/build run-tests - chmod -R ugo+rwx /tmp/KeySetCache - - - name: Analyze logs - run: | - cd build/gtest-parallel-logs/passed - ls -1 | xargs grep -H "WARNING RETRY" | sed -e "s/.log.*//g" | uniq -c | sed -re "s/ *([0-9]*) (.*)/::warning ::Test \2 retried \1 times/g" | cat - - # - name: Archive python package - # uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 - # with: - # name: concrete-compiler.whl - # path: build/wheels/concrete_compiler-*-manylinux_{{ env.GLIB_VER }}_x86_64.whl - # retention-days: 14 diff --git a/.github/workflows/compiler_build_and_test_cpu_distributed.yml b/.github/workflows/compiler_build_and_test_cpu_distributed.yml deleted file mode 100644 index 713653b4c..000000000 --- a/.github/workflows/compiler_build_and_test_cpu_distributed.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Compiler - Distributed Build and Test (CPU) - -on: - workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - matrix_item: - description: 'Build matrix item' - type: string - - -env: - GLIB_VER: 2_28 - -jobs: - BuildAndTest: - name: Build and test compiler on Slurm cluster in EC2 - runs-on: distributed-ci - if: ${{ !cancelled() }} - steps: - - name: Instance configuration used - run: | - echo "ID: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - echo "Matrix item: ${{ inputs.matrix_item }}" - - - name: Instance cleanup - run: | - sudo rm -rf /home/ubuntu/actions-runner/_work/concrete/concrete - mkdir -p /home/ubuntu/actions-runner/_work/concrete/concrete - docker system prune -af - - - name: Fetch repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Set up home - # "Install rust" step require root user to have a HOME directory which is not set. - run: | - echo "HOME=/shared" >> "${GITHUB_ENV}" - - - name: Export specific variables (CPU) - if: ${{ !startswith(inputs.instance_type, 'p3.') }} - run: | - echo "CUDA_SUPPORT=OFF" >> "${GITHUB_ENV}" - echo "DATAFLOW_EXECUTION_ENABLED=ON" >> "${GITHUB_ENV}" - - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Build compiler benchmarks - run: | - set -e - git config --global --add safe.directory '*' - cd compilers/concrete-compiler/compiler - rm -rf /shared/build - make HPX_DIR=/shared/hpx install-hpx-from-source - make HPX_DIR=/shared/hpx BUILD_DIR=/shared/build CCACHE=ON DATAFLOW_EXECUTION_ENABLED=ON BINDINGS_PYTHON_ENABLED=OFF CUDA_SUPPORT=${{ env.CUDA_SUPPORT }} build-end-to-end-tests - - - name: Run end-to-end benchmarks - run: | - set -e - cd compilers/concrete-compiler/compiler - rm -rf /shared/KeyCache - make BUILD_DIR=/shared/build KEY_CACHE_DIRECTORY=/shared/KeyCache run-end-to-end-distributed-tests - - - name: Instance cleanup - run: | - sudo rm -rf /home/ubuntu/actions-runner/_work/concrete/concrete/* - docker system prune -af diff --git a/.github/workflows/compiler_build_and_test_gpu.yml b/.github/workflows/compiler_build_and_test_gpu.yml deleted file mode 100644 index 9879d5476..000000000 --- a/.github/workflows/compiler_build_and_test_gpu.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Compiler - Build and Test (GPU) - -on: - workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - matrix_item: - description: 'Build matrix item' - type: string - -# concurrency: -# group: compiler_build_and_test_gpu-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -env: - DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler - CUDA_PATH: /usr/local/cuda-11.8 - GCC_VERSION: 11 - -jobs: - BuildAndTest: - name: Build and test compiler in EC2 with CUDA support - runs-on: ${{ github.event.inputs.runner_name }} - if: ${{ !cancelled() }} - steps: - - name: Instance configuration used - run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - echo "Matrix item: ${{ inputs.matrix_item }}" - - - name: Set up env - # "Install rust" step require root user to have a HOME directory which is not set. - run: | - echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - echo "SSH_AUTH_SOCK_DIR=$(dirname $SSH_AUTH_SOCK)" >> "${GITHUB_ENV}" - - - name: Fetch repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Create build dir - run: mkdir build - - - name: Build and test compiler - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - id: build-compiler - with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE_TEST }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - -v ${{ github.workspace }}/wheels:/wheels - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket - --gpus all - shell: bash - run: | - rustup toolchain install nightly-2024-09-30 - pip install mypy - set -e - cd /concrete/compilers/concrete-compiler/compiler - rm -rf /build/* - mkdir -p /tmp/concrete_compiler/gpu_tests/ - make BINDINGS_PYTHON_ENABLED=OFF CCACHE=ON Python3_EXECUTABLE=$PYTHON_EXEC CUDA_SUPPORT=ON CUDA_PATH=${{ env.CUDA_PATH }} run-end-to-end-tests-gpu - echo "Debug: ccache statistics (after the build):" - ccache -s diff --git a/.github/workflows/compiler_format_and_linting.yml b/.github/workflows/compiler_format_and_linting.yml deleted file mode 100644 index 4057fce6b..000000000 --- a/.github/workflows/compiler_format_and_linting.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Compiler - Compliance - -on: - workflow_call: - workflow_dispatch: - -jobs: - FormattingAndLinting: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Format with clang-format (Cpp) - run: | - sudo apt install moreutils - cd compilers/concrete-compiler/compiler - ./scripts/format_cpp.sh - - name: Format with cmake-format (Cmake) - run: | - pip3 install cmakelang - cd compilers/concrete-compiler/compiler - ./scripts/format_cmake.sh - - name: Format with black (Python) - run: | - cd compilers/concrete-compiler/compiler - pip install -r lib/Bindings/Python/requirements_dev.txt - make check-python-format - - name: Lint with pylint (Python) - run: | - cd compilers/concrete-compiler/compiler - # compiler requirements to lint - pip install numpy - make python-lint - - CheckLicense: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Check if sources include the license header - run: .github/workflows/scripts/check_for_license.sh diff --git a/.github/workflows/compiler_macos_build_and_test.yml b/.github/workflows/compiler_macos_build_and_test.yml deleted file mode 100644 index b44ec0148..000000000 --- a/.github/workflows/compiler_macos_build_and_test.yml +++ /dev/null @@ -1,104 +0,0 @@ -# Perform a build on MacOS platform with M1 chip. -name: Compiler - Build and Test (MacOS) - -on: - workflow_call: - workflow_dispatch: - secrets: - CONCRETE_CI_SSH_PRIVATE: - required: true - CONCRETE_ACTIONS_TOKEN: - required: true - -concurrency: - group: compiler_macos_build_and_test-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -jobs: - BuildAndTestMacOS: - strategy: - # if a failure happens, we want to know if it's specific - # to the architecture or the operating system - fail-fast: false - matrix: - runson: ["aws-mac1-metal", "aws-mac2-metal"] - runs-on: ${{ matrix.runson }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Install Deps - run: | - brew install ninja ccache - pip3.10 install numpy pybind11==2.8 wheel delocate - pip3.10 install pytest - pip3.10 install mypy - - - name: Cache compilation (push) - if: github.event_name == 'push' - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 - with: - path: /Users/runner/Library/Caches/ccache - key: ${{ runner.os }}-${{ runner.arch }}-compilation-cache-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-compilation-cache- - - - name: Cache compilation (pull_request) - if: github.event_name == 'pull_request' - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 - with: - path: /Users/runner/Library/Caches/ccache - key: ${{ runner.os }}-${{ runner.arch }}-compilation-cache-${{ github.event.pull_request.base.sha }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-compilation-cache- - - - name: Get tmpdir path - if: github.event_name == 'push' - id: tmpdir-path - run: echo "::set-output name=TMPDIR_PATH::$TMPDIR" - - # We do run run-check-tests as part of the build, as they aren't that costly - # and will at least give minimum confidence that the compiler works in PRs - - name: Build - run: | - set -e - cd compilers/concrete-compiler/compiler - echo "Debug: ccache statistics (prior to the build):" - ccache -s - make Python3_EXECUTABLE=$(which python3.10) all run-check-tests python-package - echo "Debug: ccache statistics (after the build):" - ccache -s - - - name: Enable complete tests on push to main - if: github.ref == 'refs/heads/main' - run: echo "MINIMAL_TESTS=OFF" >> $GITHUB_ENV - - - name: Enable minimal tests otherwise - if: github.ref != 'refs/heads/main' - run: echo "MINIMAL_TESTS=ON" >> $GITHUB_ENV - - - name: Test - run: | - set -e - export KEY_CACHE_DIRECTORY=$(mktemp -d)/KeySetCache - echo "KEY_CACHE_DIRECTORY=$KEY_CACHE_DIRECTORY" >> "${GITHUB_ENV}" - mkdir $KEY_CACHE_DIRECTORY - - cd compilers/concrete-compiler/compiler - echo "Debug: ccache statistics (prior to the tests):" - ccache -s - export CONCRETE_COMPILER_DATAFLOW_EXECUTION_ENABLED=OFF - pip3.10 install build/wheels/*macosx*.whl - make MINIMAL_TESTS=${{ env.MINIMAL_TESTS }} Python3_EXECUTABLE=$(which python3.10) run-tests - echo "Debug: ccache statistics (after the tests):" - ccache -s - - - name: Cleanup host - if: success() || failure() - run: | - rm -rf $KEY_CACHE_DIRECTORY diff --git a/.github/workflows/compiler_publish_docker_images.yml b/.github/workflows/compiler_publish_docker_images.yml deleted file mode 100644 index 30c015228..000000000 --- a/.github/workflows/compiler_publish_docker_images.yml +++ /dev/null @@ -1,191 +0,0 @@ -# Build and publish Docker images for different applications using AWS EC2. -name: Compiler - Docker images build & publish - -on: - workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - matrix_item: - description: 'Build matrix item' - type: string - -# concurrency: -# group: compiler_publish_docker_images-${{ github.ref }} -# cancel-in-progress: true - -env: - THIS_FILE: .github/workflows/compiler_publish_docker_images.yml - -jobs: - BuildAndPushDockerImages: - needs: [BuildAndPublishHPXDockerImage, BuildAndPublishCUDADockerImage] - name: Build & Publish Docker Images - runs-on: ${{ github.event.inputs.runner_name }} - strategy: - matrix: - include: - - name: test-env - image: ghcr.io/zama-ai/concrete-compiler - dockerfile: docker/Dockerfile.concrete-compiler-env - - steps: - - name: Instance configuration used - run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Login to Registry - run: echo "${{ secrets.GHCR_PASSWORD }}" | docker login -u ${{ secrets.GHCR_LOGIN }} --password-stdin ghcr.io - - # label was initially a need from the frontend CI - - name: Build Image - run: | - DOCKER_BUILDKIT=1 docker build --no-cache \ - --label "commit-sha=${{ github.sha }}" -t ${{ matrix.image }} -f ${{ matrix.dockerfile }} . - - # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 - # - name: Run Trivy vulnerability scanner - # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 - # with: - # image-ref: '${{ matrix.image }}' - # format: 'table' - # exit-code: '1' - # ignore-unfixed: true - # vuln-type: 'os,library' - # severity: 'CRITICAL,HIGH' - - - name: Tag and Publish Image - run: | - docker image tag ${{ matrix.image }} ${{ matrix.image }}:${{ github.sha }} - docker image push ${{ matrix.image }}:latest - docker image push ${{ matrix.image }}:${{ github.sha }} - - - name: Tag and Publish Release Image - if: startsWith(github.ref, 'refs/tags/v') - run: | - docker image tag ${{ matrix.image }} ${{ matrix.image }}:${{ github.ref_name }} - docker image push ${{ matrix.image }}:${{ github.ref_name }} - - BuildAndPublishHPXDockerImage: - name: Build & Publish HPX Docker Image - runs-on: ${{ github.event.inputs.runner_name }} - env: - IMAGE: ghcr.io/zama-ai/hpx - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - - name: Set up env - run: | - echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 # v44.5.24 - - - name: Login - id: login - if: contains(steps.changed-files.outputs.modified_files, 'docker/Dockerfile.hpx-env') || contains(steps.changed-files.outputs.modified_files, env.THIS_FILE) - run: echo "${{ secrets.GHCR_PASSWORD }}" | docker login -u ${{ secrets.GHCR_LOGIN }} --password-stdin ghcr.io - - - name: Build - if: ${{ steps.login.conclusion != 'skipped' }} - run: docker build -t "${IMAGE}" -f docker/Dockerfile.hpx-env . - - # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 - # - name: Run Trivy vulnerability scanner - # if: ${{ steps.login.conclusion != 'skipped' }} - # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 - # with: - # image-ref: '${{ env.IMAGE }}' - # format: 'table' - # exit-code: '1' - # ignore-unfixed: true - # vuln-type: 'os,library' - # severity: 'CRITICAL,HIGH' - - - name: Publish - if: ${{ steps.login.conclusion != 'skipped' }} - run: docker push "${IMAGE}:latest" - - BuildAndPublishCUDADockerImage: - name: Build & Publish CUDA Docker Image - runs-on: ${{ github.event.inputs.runner_name }} - env: - IMAGE: ghcr.io/zama-ai/cuda - strategy: - matrix: - include: - - name: cuda-12-3 - tag: 12-3 - dockerfile: docker/Dockerfile.cuda-123-env - - name: cuda-11-8 - tag: 11-8 - dockerfile: docker/Dockerfile.cuda-118-env - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - - name: Set up env - run: | - echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 # v44.5.24 - - - name: Login - id: login - # from the docs: The jobs..if condition is evaluated before jobs..strategy.matrix is applied. So we can't just use matrix.dockerfile - # so we have to build both images if one of the two files change, or we will have to split this into two - # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idif - if: contains(steps.changed-files.outputs.modified_files, 'docker/Dockerfile.cuda-118-env') || contains(steps.changed-files.outputs.modified_files, 'docker/Dockerfile.cuda-123-env') || contains(steps.changed-files.outputs.modified_files, env.THIS_FILE) - run: echo "${{ secrets.GHCR_PASSWORD }}" | docker login -u ${{ secrets.GHCR_LOGIN }} --password-stdin ghcr.io - - - name: Build Tag and Publish - if: ${{ steps.login.conclusion != 'skipped' }} - run: | - docker build -t "${IMAGE}" -f ${{ matrix.dockerfile }} . - docker image tag "${IMAGE}" "${IMAGE}:${{ matrix.tag }}" - docker push "${IMAGE}:${{ matrix.tag }}" - - # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 - # - name: Run Trivy vulnerability scanner - # if: ${{ steps.login.conclusion != 'skipped' }} - # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 - # with: - # image-ref: '${{ env.IMAGE }}' - # format: 'table' - # exit-code: '1' - # ignore-unfixed: true - # vuln-type: 'os,library' - # severity: 'CRITICAL,HIGH' - - - name: Push Latest Image - if: ${{ steps.login.conclusion != 'skipped' && matrix.tag == '11-8' }} - run: docker push "${IMAGE}:latest" diff --git a/.github/workflows/concrete_compiler_benchmark.yml b/.github/workflows/concrete_compiler_benchmark.yml new file mode 100644 index 000000000..cd660a016 --- /dev/null +++ b/.github/workflows/concrete_compiler_benchmark.yml @@ -0,0 +1,167 @@ +name: concrete-compiler benchmark linux-cpu + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/concrete_compiler_benchmark.yml + - compilers/** + - backends/** + - tools/** + push: + branches: + - 'main' + - 'release/*' + +env: + DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +concurrency: + group: concrete_compiler_benchmark_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: cpu-bench + + build-and-run-benchmarks: + needs: setup-instance + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + outputs: + bench_date: ${{ steps.benchmark-details.outputs.bench_date }} + commit_date: ${{ steps.benchmark-details.outputs.commit_date }} + commit_hash: ${{ steps.benchmark-details.outputs.commit_hash }} + steps: + - name: Checkout concrete + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + fetch-depth: 0 + - name: Ouput benchmark details + id: benchmark-details + run: | + echo "bench_date=$(date --iso-8601=seconds)" >> "$GITHUB_OUTPUT" + echo "commit_date=$(git --no-pager show -s --format=%cd --date=iso8601-strict ${{ github.sha }})" >> "$GITHUB_OUTPUT" + echo "commit_hash=$(git describe --tags --dirty)" >> "$GITHUB_OUTPUT" + - name: Set up home + # "Install rust" step require root user to have a HOME directory which is not set. + run: | + echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" + - name: Setup rust toolchain for concrete-cpu + uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu + - name: Build compiler benchmarks + run: | + set -e + git config --global --add safe.directory '*' + cd compilers/concrete-compiler/compiler + make BINDINGS_PYTHON_ENABLED=OFF build-benchmarks + - name: Run compiler benchmarks + run: | + set -e + cd compilers/concrete-compiler/compiler + make run-cpu-benchmarks + - name: Upload raw results artifact + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: compiler-benchmarks-result + path: compilers/concrete-compiler/compiler/benchmarks_results.json + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-and-run-benchmarks finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + parse-and-send-results: + name: Parse and send results + needs: [setup-instance, build-and-run-benchmarks] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + steps: + - name: Download compiler-benchmarks-result + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: compiler-benchmarks-result + - name: Parse results + shell: bash + run: | + # TODO output setup-instance (https://github.com/zama-ai/slab-github-runner/issues/38) + python3 ./ci/benchmark_parser.py benchmarks_results.json parsed_benchmark_results.json \ + --database compiler_benchmarks \ + --hardware "hpc7a.96xlarge" \ + --project-version ${{ needs.build-and-run-benchmarks.outputs.commit_hash}} \ + --branch ${{ github.ref_name }} \ + --commit-date "${{ needs.build-and-run-benchmarks.outputs.commit_date }}" \ + --bench-date "${{ needs.build-and-run-benchmarks.outputs.bench_date }}" \ + --throughput + - name: Upload parsed results artifact + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: compiler-benchmarks-parsed-result + path: parsed_benchmark_results.json + - name: Checkout Slab repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: zama-ai/slab + path: slab + token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} + - name: Send data to Slab + shell: bash + run: | + echo "Computing HMac on downloaded artifact" + SIGNATURE="$(slab/scripts/hmac_calculator.sh $parsed_benchmark_results.json '${{ secrets.JOB_SECRET }}')" + echo "Sending results to Slab..." + curl -v -k \ + -H "Content-Type: application/json" \ + -H "X-Slab-Repository: ${{ github.repository }}" \ + -H "X-Slab-Command: store_data" \ + -H "X-Hub-Signature-256: sha256=${SIGNATURE}" \ + -d @parsed_benchmark_results.json \ + ${{ secrets.SLAB_URL }} + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "parse-and-send-results finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + name: Teardown instance + needs: [ setup-instance, parse-and-send-results ] + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_compiler_publish_docker_images.yml b/.github/workflows/concrete_compiler_publish_docker_images.yml new file mode 100644 index 000000000..8e7c14a93 --- /dev/null +++ b/.github/workflows/concrete_compiler_publish_docker_images.yml @@ -0,0 +1,218 @@ +name: concrete-compiler publish docker images + +on: + workflow_dispatch: + push: + branches: + - 'main' + - 'force-docker-images' + +env: + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + THIS_FILE: .github/workflows/concrete_compiler_publish_docker_images.yml + +concurrency: + group: concrete_compiler_publish_docker_images + cancel-in-progress: true + +jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: cpu-test + + hpx-image: + needs: [setup-instance] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + env: + image: ghcr.io/zama-ai/hpx + dockerfile: docker/Dockerfile.hpx-env + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 # v44.5.24 + - name: Login + id: login + if: contains(steps.changed-files.outputs.modified_files, env.dockerfile) || contains(steps.changed-files.outputs.modified_files, env.THIS_FILE) + run: echo "${{ secrets.GHCR_PASSWORD }}" | docker login -u ${{ secrets.GHCR_LOGIN }} --password-stdin ghcr.io + - name: Build + if: ${{ steps.login.conclusion != 'skipped' }} + run: docker build -t "${{ env.image }}" -f ${{ env.dockerfile }} . + # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 + # - name: Run Trivy vulnerability scanner + # if: ${{ steps.login.conclusion != 'skipped' }} + # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 + # with: + # image-ref: '${{ env.IMAGE }}' + # format: 'table' + # exit-code: '1' + # ignore-unfixed: true + # vuln-type: 'os,library' + # severity: 'CRITICAL,HIGH' + - name: Publish + if: ${{ steps.login.conclusion != 'skipped' }} + run: docker push "${{ env.image }}:latest" + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "hpx-image finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + cuda-image: + needs: [setup-instance] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + env: + image: ghcr.io/zama-ai/cuda + strategy: + matrix: + include: + - name: cuda-12-3 + tag: 12-3 + dockerfile: docker/Dockerfile.cuda-123-env + - name: cuda-11-8 + tag: 11-8 + dockerfile: docker/Dockerfile.cuda-118-env + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Set up env + run: | + echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 # v44.5.24 + - name: Login + id: login + # from the docs: The jobs..if condition is evaluated before jobs..strategy.matrix is applied. So we can't just use matrix.dockerfile + # so we have to build both images if one of the two files change, or we will have to split this into two + # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idif + if: contains(steps.changed-files.outputs.modified_files, 'docker/Dockerfile.cuda-118-env') || contains(steps.changed-files.outputs.modified_files, 'docker/Dockerfile.cuda-123-env') || contains(steps.changed-files.outputs.modified_files, env.THIS_FILE) + run: echo "${{ secrets.GHCR_PASSWORD }}" | docker login -u ${{ secrets.GHCR_LOGIN }} --password-stdin ghcr.io + - name: Build Tag and Publish + if: ${{ steps.login.conclusion != 'skipped' }} + run: | + docker build -t "${{ env.image }}" -f ${{ matrix.dockerfile }} . + docker image tag "${{ env.image }}" "${{ env.image }}:${{ matrix.tag }}" + docker push "${{ env.image }}:${{ matrix.tag }}" + # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 + # - name: Run Trivy vulnerability scanner + # if: ${{ steps.login.conclusion != 'skipped' }} + # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 + # with: + # image-ref: '${{ env.image }}' + # format: 'table' + # exit-code: '1' + # ignore-unfixed: true + # vuln-type: 'os,library' + # severity: 'CRITICAL,HIGH' + - name: Push Latest Image + if: ${{ steps.login.conclusion != 'skipped' && matrix.tag == '11-8' }} + run: docker push "${{ env.image }}:latest" + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "cuda-image finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + compiler-image: + needs: [setup-instance, hpx-image, cuda-image] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + env: + image: ghcr.io/zama-ai/concrete-compiler + dockerfile: docker/Dockerfile.concrete-compiler-env + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + submodules: recursive + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 # v44.5.24 + with: + files: | + backends/** + compilers/** + third_party/** + tools/** + - name: Login to Registry + id: login + if: steps.changed-files.outputs.any_changed == 'true' + run: echo "${{ secrets.GHCR_PASSWORD }}" | docker login -u ${{ secrets.GHCR_LOGIN }} --password-stdin ghcr.io + - name: Build Image + if: steps.login.conclusion != 'skipped' + run: | + DOCKER_BUILDKIT=1 docker build --no-cache \ + --label "commit-sha=${{ github.sha }}" -t ${{ env.image }} -f ${{ env.dockerfile }} . + # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 + # - name: Run Trivy vulnerability scanner + # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 + # with: + # image-ref: '${{ matrix.image }}' + # format: 'table' + # exit-code: '1' + # ignore-unfixed: true + # vuln-type: 'os,library' + # severity: 'CRITICAL,HIGH' + - name: Tag and Publish Image + if: steps.login.conclusion != 'skipped' + run: | + docker image tag ${{ env.image }} ${{ env.image }}:${{ github.sha }} + docker image push ${{ env.image }}:latest + docker image push ${{ env.image }}:${{ github.sha }} + - name: Tag and Publish Release Image + if: steps.login.conclusion != 'skipped' && startsWith(github.ref, 'refs/tags/v') + run: | + docker image tag ${{ env.image }} ${{ env.image }}:${{ github.ref_name }} + docker image push ${{ env.image }}:${{ github.ref_name }} + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "compiler-image finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + name: Teardown instance + needs: [ setup-instance, compiler-image ] + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_compiler_test_cpu.yml b/.github/workflows/concrete_compiler_test_cpu.yml new file mode 100644 index 000000000..ad2e658ea --- /dev/null +++ b/.github/workflows/concrete_compiler_test_cpu.yml @@ -0,0 +1,181 @@ +name: concrete-compiler test linux-cpu + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/concrete_compiler_test_cpu.yml + - compilers/** + - backends/concrete-cpu/** + - tools/** + push: + branches: + - 'main' + - 'release/*' + +env: + DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +concurrency: + group: concrete_compiler_test_cpu_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: cpu-test + + format-and-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Format with clang-format (Cpp) + run: | + sudo apt install moreutils + cd compilers/concrete-compiler/compiler + ./scripts/format_cpp.sh + - name: Format with cmake-format (Cmake) + run: | + pip3 install cmakelang + cd compilers/concrete-compiler/compiler + ./scripts/format_cmake.sh + - name: Format with black (Python) + run: | + cd compilers/concrete-compiler/compiler + pip install -r lib/Bindings/Python/requirements_dev.txt + make check-python-format + - name: Lint with pylint (Python) + run: | + cd compilers/concrete-compiler/compiler + # compiler requirements to lint + pip install numpy + make python-lint + - name: Check if sources include the license header + run: .github/workflows/scripts/check_for_license.sh + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "format-and-lint finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + build-and-run-test: + needs: [ setup-instance ] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + steps: + - name: Fetch repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + fetch-depth: 0 + - name: Create build dir + run: mkdir build + - name: Build compiler + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + id: build-compiler + with: + registry: ghcr.io + image: ${{ env.DOCKER_IMAGE_TEST }} + username: ${{ secrets.GHCR_LOGIN }} + password: ${{ secrets.GHCR_PASSWORD }} + options: >- + -v ${{ github.workspace }}:/concrete + -v ${{ github.workspace }}/build:/build + -v ${{ github.workspace }}/wheels:/wheels + shell: bash + run: | + set -e + cd /concrete/compilers/concrete-compiler/compiler + rm -rf /build/* + make DATAFLOW_EXECUTION_ENABLED=ON Python3_EXECUTABLE=$PYTHON_EXEC BUILD_DIR=/build all + echo "Debug: ccache statistics (after the build):" + ccache -s + - name: Check compiler dialects docs is up to date + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + id: build-compiler-docs + with: + registry: ghcr.io + image: ${{ env.DOCKER_IMAGE_TEST }} + username: ${{ secrets.GHCR_LOGIN }} + password: ${{ secrets.GHCR_PASSWORD }} + options: >- + -v ${{ github.workspace }}:/concrete + -v ${{ github.workspace }}/build:/build + -v ${{ github.workspace }}/wheels:/wheels + shell: bash + run: | + set -e + cd /build/tools/concretelang/docs/concretelang/ + sed -i -e 's/\[TOC\]//' *Dialect.md + for i in `ls *Dialect.md`; do diff $i /concrete/docs/explanations/$i; done; + - name: Enable complete tests on push to main + if: github.ref == 'refs/heads/main' + run: echo "MINIMAL_TESTS=OFF" >> ${GITHUB_ENV} + - name: Enable minimal tests otherwise + if: github.ref != 'refs/heads/main' + run: echo "MINIMAL_TESTS=ON" >> ${GITHUB_ENV} + - name: Run compiler tests + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + with: + registry: ghcr.io + image: ${{ env.DOCKER_IMAGE_TEST }} + username: ${{ secrets.GHCR_LOGIN }} + password: ${{ secrets.GHCR_PASSWORD }} + options: >- + -v ${{ github.workspace }}:/concrete + -v ${{ github.workspace }}/build:/build + shell: bash + run: | + set -e + cd /concrete/compilers/concrete-compiler/compiler + mkdir -p /tmp/concrete_compiler/gpu_tests/ + pip install pytest + sed "s/pytest/python -m pytest/g" -i Makefile + make MINIMAL_TESTS=${{ env.MINIMAL_TESTS }} DATAFLOW_EXECUTION_ENABLED=ON Python3_EXECUTABLE=$PYTHON_EXEC BUILD_DIR=/build run-tests + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-and-run-test finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + needs: [ setup-instance, build-and-run-test ] + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_compiler_test_cpu_distributed.yml b/.github/workflows/concrete_compiler_test_cpu_distributed.yml new file mode 100644 index 000000000..f09ca686b --- /dev/null +++ b/.github/workflows/concrete_compiler_test_cpu_distributed.yml @@ -0,0 +1,109 @@ +name: concrete-compiler test linux-cpu-distributed + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/concrete_compiler_test_cpu_distributed.yml + - compilers/concrete-compiler/** + push: + branches: + - 'main' + - 'release/*' + +env: + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +concurrency: + group: concrete_compiler_test_cpu_distributed_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: slurm-cluster + + build-and-run-test: + # The distributed-ci runner is registered on the instance configured in the slurm-cluster profile. + # It's why we need to setup-instance + needs: setup-instance + runs-on: distributed-ci + steps: + - name: Instance cleanup + run: | + sudo rm -rf /home/ubuntu/actions-runner/_work/concrete/concrete + mkdir -p /home/ubuntu/actions-runner/_work/concrete/concrete + docker system prune -af + + - name: Fetch repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + submodules: recursive + + - name: Set up home + # "Install rust" step require root user to have a HOME directory which is not set. + run: | + echo "HOME=/shared" >> "${GITHUB_ENV}" + + - name: Setup rust toolchain for concrete-cpu + uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu + + - name: Build end-to-end distributed test + run: | + cd compilers/concrete-compiler/compiler + rm -rf /shared/build + make HPX_DIR=/shared/hpx install-hpx-from-source + make HPX_DIR=/shared/hpx BUILD_DIR=/shared/build CCACHE=ON DATAFLOW_EXECUTION_ENABLED=ON BINDINGS_PYTHON_ENABLED=OFF build-end-to-end-tests + + - name: Run end-to-end distributed test + run: | + cd compilers/concrete-compiler/compiler + rm -rf /shared/KeyCache + make BUILD_DIR=/shared/build KEY_CACHE_DIRECTORY=/shared/KeyCache run-end-to-end-distributed-tests + + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-and-run-test finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + needs: [ setup-instance, build-and-run-test ] + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_compiler_test_gpu.yml b/.github/workflows/concrete_compiler_test_gpu.yml new file mode 100644 index 000000000..0e674e293 --- /dev/null +++ b/.github/workflows/concrete_compiler_test_gpu.yml @@ -0,0 +1,106 @@ +name: concrete-compiler test linux-gpu + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/concrete_compiler_test_gpu.yml + - compilers/** + - backends/concrete-cuda/** + - tools/** + push: + branches: + - 'main' + - 'release/*' + +env: + DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + CUDA_PATH: /usr/local/cuda-11.8 + +concurrency: + group: concrete_compiler_test_gpu_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: gpu-test + + build-and-test: + needs: [ setup-instance ] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + if: ${{ !cancelled() }} + steps: + - name: Fetch repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + submodules: recursive + - name: Create build dir + run: mkdir build + - name: Build and test compiler + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + id: build-compiler + with: + registry: ghcr.io + image: ${{ env.DOCKER_IMAGE_TEST }} + username: ${{ secrets.GHCR_LOGIN }} + password: ${{ secrets.GHCR_PASSWORD }} + options: >- + -v ${{ github.workspace }}:/concrete + -v ${{ github.workspace }}/build:/build + --gpus all + shell: bash + run: | + set -e + cd /concrete/compilers/concrete-compiler/compiler + rm -rf /build/* + mkdir -p /tmp/concrete_compiler/gpu_tests/ + make BINDINGS_PYTHON_ENABLED=OFF Python3_EXECUTABLE=$PYTHON_EXEC CUDA_SUPPORT=ON CUDA_PATH=${{ env.CUDA_PATH }} run-end-to-end-tests-gpu + echo "Debug: ccache statistics (after the build):" + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-and-run-test finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + needs: [ setup-instance, build-and-test ] + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_compiler_test_macos_cpu.yml b/.github/workflows/concrete_compiler_test_macos_cpu.yml new file mode 100644 index 000000000..b5d9c5c30 --- /dev/null +++ b/.github/workflows/concrete_compiler_test_macos_cpu.yml @@ -0,0 +1,89 @@ +name: concrete-compiler test macos-cpu + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/concrete_compiler_test_macos_cpu.yml + - compilers/** + - backends/** + - tools/** + push: + branches: + - 'main' + - 'release/*' + +env: + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +concurrency: + group: concrete_compiler_test_macos_cpu_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + build-and-test: + strategy: + # if a failure happens, we want to know if it's specific + # to the architecture or the operating system + fail-fast: false + matrix: + runson: ["aws-mac1-metal", "aws-mac2-metal"] + python-version: ["3.10"] + runs-on: ${{ matrix.runson }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + fetch-depth: 0 + - name: Setup rust toolchain for concrete-cpu + uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu + - name: Set python variables + run: | + set -e + echo "PIP=${{ format('pip{0}', matrix.python-version) }}" >> "${GITHUB_ENV}" + echo "PYTHON=${{ format('python{0}', matrix.python-version) }}" >> "${GITHUB_ENV}" + echo "PYTHON_EXEC=$(which ${{ format('python{0}', matrix.python-version) }})" >> "${GITHUB_ENV}" + - name: Install dependencies + run: | + brew install ninja ccache + ${PIP} install pytest + - name: Build compiler + run: | + set -e + cd compilers/concrete-compiler/compiler + echo "Debug: ccache statistics (prior to the build):" + ccache -s + make Python3_EXECUTABLE=$PYTHON_EXEC all + echo "Debug: ccache statistics (after the build):" + ccache -s + - name: Enable complete tests on push to main + if: github.ref == 'refs/heads/main' + run: echo "MINIMAL_TESTS=OFF" >> $GITHUB_ENV + - name: Enable minimal tests otherwise + if: github.ref != 'refs/heads/main' + run: echo "MINIMAL_TESTS=ON" >> $GITHUB_ENV + - name: Create keyset cache directory + run: | + export KEY_CACHE_DIRECTORY=$(mktemp -d)/KeySetCache + echo "KEY_CACHE_DIRECTORY=$KEY_CACHE_DIRECTORY" >> "${GITHUB_ENV}" + mkdir $KEY_CACHE_DIRECTORY + - name: Test + run: | + set -e + cd compilers/concrete-compiler/compiler + export CONCRETE_COMPILER_DATAFLOW_EXECUTION_ENABLED=OFF + make MINIMAL_TESTS=${{ env.MINIMAL_TESTS }} Python3_EXECUTABLE=$PYTHON_EXEC run-tests + - name: Cleanup host + if: success() || failure() + run: | + rm -rf $KEY_CACHE_DIRECTORY + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-and-test finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_cpu_test.yml b/.github/workflows/concrete_cpu_test.yml index 40a1ed254..6e7241c02 100644 --- a/.github/workflows/concrete_cpu_test.yml +++ b/.github/workflows/concrete_cpu_test.yml @@ -1,17 +1,28 @@ -name: Concrete CPU - Tests +name: concrete-cpu test on: - workflow_call: workflow_dispatch: + pull_request: + paths: + - .github/workflows/concrete_cpu_test.yml + - backends/concrete-cpu/** + push: + branches: + - 'main' + - 'release/*' concurrency: - group: concrete_cpu_test-${{ github.ref }} + group: concrete_cpu_test_${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} env: CARGO_TERM_COLOR: always jobs: tests-linux: + strategy: + fail-fast: false + matrix: + runson: ["ubuntu-20.04"] runs-on: ubuntu-20.04 env: RUSTFLAGS: -D warnings @@ -57,39 +68,3 @@ jobs: run: | cd backends/concrete-cpu/implementation cargo test --no-fail-fast --all-targets --features=nightly - - tests-mac_x86: - runs-on: macos-11 - env: - RUSTFLAGS: -D warnings - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Download cargo cache - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 - - - name: Tests - run: | - cd backends/concrete-cpu/implementation - cargo test --no-fail-fast --all-targets - - tests-mac-m1: - runs-on: "aws-mac2-metal" - env: - RUSTFLAGS: -D warnings - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Download cargo cache - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 - - - name: Tests - run: | - cd backends/concrete-cpu/implementation - cargo test --no-fail-fast --all-targets diff --git a/.github/workflows/concrete_ml_test.yml b/.github/workflows/concrete_ml_test.yml new file mode 100644 index 000000000..26a8ea37e --- /dev/null +++ b/.github/workflows/concrete_ml_test.yml @@ -0,0 +1,138 @@ +name: concrete-ml test + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/concrete_ml_test.yml + - frontends/concrete-python/** + push: + branches: + - 'main' + - 'release/*' + +env: + DOCKER_IMAGE: ghcr.io/zama-ai/concrete-compiler + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +concurrency: + group: concrete_ml_test_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: cpu-test + + build-and-run-tests: + strategy: + matrix: + python-version: ["3.8"] + needs: setup-instance + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + - name: Set release version + run: echo "__version__ = \"$(date +"%Y.%m.%d")\"" >| frontends/concrete-python/version.txt + - name: Expose release version from Python + run: cp frontends/concrete-python/version.txt frontends/concrete-python/concrete/fhe/version.py + + - name: Create build directory + run: mkdir build + + - name: Build wheel + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + id: build-compiler-bindings + with: + registry: ghcr.io + image: ${{ env.DOCKER_IMAGE }} + username: ${{ secrets.GHCR_LOGIN }} + password: ${{ secrets.GHCR_PASSWORD }} + options: >- + -v ${{ github.workspace }}:/concrete + -v ${{ github.workspace }}/build:/build + shell: bash + run: | + set -e + rm -rf /build/* + + export PYTHON=${{ format('python{0}', matrix.python-version) }} + echo "Using $PYTHON" + + cd /concrete/frontends/concrete-python + make PYTHON=$PYTHON venv + source .venv/bin/activate + + cd /concrete/compilers/concrete-compiler/compiler + make BUILD_DIR=/build CCACHE=ON DATAFLOW_EXECUTION_ENABLED=ON Python3_EXECUTABLE=$(which python) python-bindings + + echo "Debug: ccache statistics (after the build):" + ccache -s + + cd /concrete/frontends/concrete-python + + export COMPILER_BUILD_DIRECTORY="/build" + make whl + + deactivate + + - name: Setup Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: ${{ matrix.python-version }} + - name: ML Tests + run: | + export HOME="/home/ubuntu" + export CONCRETE_PYTHON_WHEEL=$(pwd)/frontends/concrete-python/dist/*manylinux*.whl + apt update + apt install git git-lfs -y + pip install poetry==1.7.1 + ./ci/scripts/test_cml.sh --use-wheel $CONCRETE_PYTHON_WHEEL --verbose + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-and-run-tests finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + needs: [ setup-instance, build-and-run-tests ] + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_ml_tests.yml b/.github/workflows/concrete_ml_tests.yml deleted file mode 100644 index 47f7e6e69..000000000 --- a/.github/workflows/concrete_ml_tests.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Concrete ML Tests -on: - workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - - -env: - DOCKER_IMAGE: ghcr.io/zama-ai/concrete-compiler - -jobs: - linux-x86: - strategy: - matrix: - python-version: ["3.8"] - - runs-on: ${{ github.event.inputs.runner_name }} - steps: - - name: Log instance configuration - run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - echo "User Inputs: ${{ inputs.user_inputs }}" - - - name: Set up GitHub environment - run: | - echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - #echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK)" >> "${GITHUB_ENV}" - echo "SSH_AUTH_SOCK_DIR=$(dirname $SSH_AUTH_SOCK)" >> "${GITHUB_ENV}" - - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Set release version - run: echo "__version__ = \"$(date +"%Y.%m.%d")\"" >| frontends/concrete-python/version.txt - - - name: Expose release version from Python - run: cp frontends/concrete-python/version.txt frontends/concrete-python/concrete/fhe/version.py - - - name: Create build directory - run: mkdir build - - - name: Build wheel - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - id: build-compiler-bindings - with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket - ${{ env.DOCKER_GPU_OPTION }} - shell: bash - run: | - set -e - rm -rf /build/* - - export PYTHON=${{ format('python{0}', matrix.python-version) }} - echo "Using $PYTHON" - - cd /concrete/frontends/concrete-python - make PYTHON=$PYTHON venv - source .venv/bin/activate - - cd /concrete/compilers/concrete-compiler/compiler - make BUILD_DIR=/build CCACHE=ON DATAFLOW_EXECUTION_ENABLED=ON Python3_EXECUTABLE=$(which python) python-bindings - - echo "Debug: ccache statistics (after the build):" - ccache -s - - cd /concrete/frontends/concrete-python - - export COMPILER_BUILD_DIRECTORY="/build" - make whl - - deactivate - - - name: Setup Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 - with: - python-version: ${{ matrix.python-version }} - - - name: ML Tests - run: | - export CONCRETE_PYTHON_WHEEL=$(pwd)/frontends/concrete-python/dist/*manylinux*.whl - apt update - apt install git git-lfs -y - pip install poetry==1.7.1 - ./ci/scripts/test_cml.sh --use-wheel $CONCRETE_PYTHON_WHEEL --verbose diff --git a/.github/workflows/optimizer.yml b/.github/workflows/concrete_optimizer.yml similarity index 74% rename from .github/workflows/optimizer.yml rename to .github/workflows/concrete_optimizer.yml index 48e86b8a6..f74229d1c 100644 --- a/.github/workflows/optimizer.yml +++ b/.github/workflows/concrete_optimizer.yml @@ -1,73 +1,72 @@ -name: Optimizer - Tests +name: concrete-optimizer test on: - workflow_call: workflow_dispatch: - secrets: - CONCRETE_CI_SSH_PRIVATE: - required: true - CONCRETE_ACTIONS_TOKEN: - required: true + pull_request: + paths: + - .github/workflows/concrete_optimizer.yml + - compilers/concrete-optimizer/** + - backends/** + - tools/** + push: + branches: + - 'main' + - 'release/*' + +env: + CARGO_TERM_COLOR: always + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} concurrency: - group: optimizer-${{ github.ref }} + group: concrete_optimizer-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} -env: - CARGO_TERM_COLOR: always jobs: tests: strategy: matrix: - os: [ubuntu-20.04, macos-11] - runs-on: ${{ matrix.os }} + runson: ["ubuntu-latest", "aws-mac1-metal", "aws-mac2-metal"] + runs-on: ${{ matrix.runson }} env: RUSTFLAGS: -D warnings steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: "Setup" + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup uses: ./.github/workflows/optimizer_setup - with: - ssh_private_key: ${{ secrets.CONCRETE_CI_SSH_PRIVATE }} - - name: Formatting run: | + cargo --version cd compilers/concrete-optimizer cargo fmt --check - - name: Build run: | cd compilers/concrete-optimizer cargo build --release --all-targets - - name: Lint run: | cd compilers/concrete-optimizer cargo clippy --release --all-targets - - name: Tests - if: matrix.os == 'ubuntu-20.04' run: | cd compilers/concrete-optimizer cargo test --release --no-fail-fast --all-targets make -C concrete-optimizer-cpp test-ci benchmarks: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: "Setup" + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup uses: ./.github/workflows/optimizer_setup - with: - ssh_private_key: ${{ secrets.CONCRETE_CI_SSH_PRIVATE }} - - name: Run benchmark run: | cd compilers/concrete-optimizer cargo bench -p v0-parameters -- --output-format bencher | tee bench_result.txt - - name: Download PR base benchmark data if: ${{ github.event_name == 'pull_request' }} # for artifacts restrictions see https://github.com/actions/download-artifact/issues/3 @@ -81,7 +80,6 @@ jobs: name: ${{ runner.os }}-benchmark if_no_artifact_found: warn path: ./benchmark - - name: Save benchmark result to file uses: benchmark-action/github-action-benchmark@4de1bed97a47495fc4c5404952da0499e31f5c29 # v1.20.3 with: @@ -94,7 +92,6 @@ jobs: comment-always: true # Enable Job Summary for PRs summary-always: true - - name: Upload benchmark data uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: diff --git a/.github/workflows/concrete_python_benchmark.yml b/.github/workflows/concrete_python_benchmark.yml index b62b99836..5142d117c 100644 --- a/.github/workflows/concrete_python_benchmark.yml +++ b/.github/workflows/concrete_python_benchmark.yml @@ -1,17 +1,25 @@ -name: Concrete Python Benchmark +name: concrete-python benchmark linux-cpu on: workflow_dispatch: schedule: - cron: "0 1 * * SAT" - + pull_request: + paths: + - .github/workflows/concrete_pyhon_benchmark.yml + push: + branches: + - 'main' + - 'release/*' env: DOCKER_IMAGE: ghcr.io/zama-ai/concrete-compiler - GLIB_VER: 2_28 + +concurrency: + group: concrete_python_benchmark_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: setup-instance: - name: Setup Instance runs-on: ubuntu-latest outputs: runner-name: ${{ steps.start-instance.outputs.label }} @@ -25,10 +33,9 @@ jobs: slab-url: ${{ secrets.SLAB_BASE_URL }} job-secret: ${{ secrets.JOB_SECRET }} backend: aws - profile: m7i-cpu-bench + profile: cpu-bench concrete-python-benchmarks: - name: Run Concrete Python Benchmarks needs: setup-instance runs-on: ${{ needs.setup-instance.outputs.runner-name }} steps: @@ -49,22 +56,14 @@ jobs: options: >- -v ${{ github.workspace }}:/concrete -v ${{ github.workspace }}/build:/build - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket - ${{ env.DOCKER_GPU_OPTION }} shell: bash run: | set -e - - rustup toolchain install nightly-2024-09-30 - pip install mypy rm -rf /build/* export PYTHON=${{ format('python{0}', matrix.python-version) }} echo "Using $PYTHON" - dnf -y install graphviz graphviz-devel - cd /concrete/frontends/concrete-python make PYTHON=$PYTHON venv source .venv/bin/activate @@ -72,14 +71,12 @@ jobs: cd /concrete/compilers/concrete-compiler/compiler make BUILD_DIR=/build CCACHE=ON DATAFLOW_EXECUTION_ENABLED=ON Python3_EXECUTABLE=$(which python) python-bindings - echo "Debug: ccache statistics (after the build):" - ccache -s - cd /concrete/frontends/concrete-python export COMPILER_BUILD_DIRECTORY="/build" - export PROGRESS_MACHINE_NAME="m7i.48xlarge" - + # TODO output setup-instance (https://github.com/zama-ai/slab-github-runner/issues/38) + export PROGRESS_MACHINE_NAME="hpc7a.96xlarge" + make benchmark make process-benchmark-results-for-grafana @@ -97,9 +94,9 @@ jobs: run: | echo "Computing HMac on results file" SIGNATURE="$(slab/scripts/hmac_calculator.sh frontends/concrete-python/progress.processed.json '${{ secrets.JOB_SECRET }}')" - + cd frontends/concrete-python - + echo "Sending results to Slab..." curl -v -k \ -H "Content-Type: application/json" \ @@ -108,9 +105,15 @@ jobs: -H "X-Hub-Signature-256: sha256=${SIGNATURE}" \ -d @progress.processed.json \ ${{ secrets.SLAB_URL }} + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "concrete-python-benchmarks finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" teardown-instance: - name: Teardown Instance if: ${{ always() && needs.setup-instance.result != 'skipped' }} needs: [ setup-instance, concrete-python-benchmarks ] runs-on: ubuntu-latest @@ -124,3 +127,10 @@ jobs: slab-url: ${{ secrets.SLAB_BASE_URL }} job-secret: ${{ secrets.JOB_SECRET }} label: ${{ needs.setup-instance.outputs.runner-name }} + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_python_checks.yml b/.github/workflows/concrete_python_checks.yml deleted file mode 100644 index fa2f908b4..000000000 --- a/.github/workflows/concrete_python_checks.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Concrete Python Checks - -on: - workflow_call: - -jobs: - Checks: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Install Platform Dependencies - run: | - sudo apt install -y graphviz libgraphviz-dev - - name: Pre-Commit Checks - run: | - ./frontends/concrete-python/scripts/checks/checks.sh diff --git a/.github/workflows/concrete_python_finalize_release.yml b/.github/workflows/concrete_python_finalize_release.yml new file mode 100644 index 000000000..482c6f60c --- /dev/null +++ b/.github/workflows/concrete_python_finalize_release.yml @@ -0,0 +1,79 @@ +# This workflows should be runned after that releases has been validated and ready to push to pypi.org and docker hub. +name: concrete-python finalize-release + +on: + workflow_dispatch: + inputs: + version: + description: 'version of concrete-python to push to pypi and docker hub' + required: true + type: string + +jobs: + publish-to-pypi: + runs-on: ubuntu-latest + steps: + - name: Pull wheels from S3 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_IAM_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + S3_BUCKET_NAME: ${{ secrets.AWS_S3_PYPI_BUCKET_NAME }} + run: | + mkdir wheels + aws s3 cp s3://${S3_BUCKET_NAME}/cpu/concrete-python/ ./wheels/ --recursive --exclude "*" --include "concrete_python-${{ inputs.version }}-*" + echo "============== Downloaded wheels ===============" + ls -la ./wheels/ + - name: Push wheels to public PyPI (public) + run: | + pip install twine==4.0.2 + twine upload wheels/concrete_python-${{ inputs.version }}*.whl \ + -u "${{ secrets.PUBLIC_PYPI_USER }}" \ + -p "${{ secrets.PUBLIC_PYPI_PASSWORD }}" \ + -r pypi + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "test-linux-x86 (${{ matrix.python-version }}) finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + publish-to-dockerhub: + runs-on: ubuntu-latest + env: + DOCKER_IMAGE_NAME: zamafhe/concrete-python + DOCKER_FILE: docker/Dockerfile.concrete-python + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Get version from tag + run: | + # remove leading 'v' and '-' from tag + export VERSION=`echo ${{ inputs.tag }} | sed "s/^v*//g" | sed "s/-//g"` + echo "VERSION=$VERSION" >> "${GITHUB_ENV}" + echo "NAME_TAG=${{ env.DOCKER_IMAGE_NAME }}:v$VERSION" >> "${GITHUB_ENV}" + - name: Build image + run: | + mkdir empty_context + docker image build -t ${{ env.NAME_TAG }} --build-arg version=${{ env.VERSION }} -f ${{ env.DOCKER_FILE }} empty_context + + # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 + # - name: Run Trivy vulnerability scanner + # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 + # with: + # image-ref: '${{ env.NAME_TAG }}' + # format: 'table' + # exit-code: '1' + # ignore-unfixed: true + # vuln-type: 'os,library' + # severity: 'CRITICAL,HIGH' + + - name: Login to Docker Hub + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push image + run: docker image push ${{ env.NAME_TAG }} diff --git a/.github/workflows/concrete_python_push_docker_image.yml b/.github/workflows/concrete_python_push_docker_image.yml deleted file mode 100644 index f88248a46..000000000 --- a/.github/workflows/concrete_python_push_docker_image.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Concrete Python Push Docker Image -on: - workflow_dispatch: - inputs: - tag: - description: 'tag to use for the docker image' - type: string - workflow_call: - inputs: - tag: - description: 'tag to use for the docker image' - type: string - -env: - DOCKER_IMAGE_NAME: zamafhe/concrete-python - DOCKER_FILE: docker/Dockerfile.concrete-python - -jobs: - build_and_push: - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Get version from tag - run: | - # remove leading 'v' and '-' from tag - export VERSION=`echo ${{ inputs.tag }} | sed "s/^v*//g" | sed "s/-//g"` - echo "VERSION=$VERSION" >> "${GITHUB_ENV}" - echo "NAME_TAG=${{ env.DOCKER_IMAGE_NAME }}:v$VERSION" >> "${GITHUB_ENV}" - - - name: Build image - run: | - mkdir empty_context - docker image build -t ${{ env.NAME_TAG }} --build-arg version=${{ env.VERSION }} -f ${{ env.DOCKER_FILE }} empty_context - - # disabled because of https://github.com/aquasecurity/trivy/discussions/7668 - # - name: Run Trivy vulnerability scanner - # uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 - # with: - # image-ref: '${{ env.NAME_TAG }}' - # format: 'table' - # exit-code: '1' - # ignore-unfixed: true - # vuln-type: 'os,library' - # severity: 'CRITICAL,HIGH' - - - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Push image - run: docker image push ${{ env.NAME_TAG }} diff --git a/.github/workflows/concrete_python_release.yml b/.github/workflows/concrete_python_release_cpu.yml similarity index 74% rename from .github/workflows/concrete_python_release.yml rename to .github/workflows/concrete_python_release_cpu.yml index 7282b50ee..9922fc550 100644 --- a/.github/workflows/concrete_python_release.yml +++ b/.github/workflows/concrete_python_release_cpu.yml @@ -1,37 +1,46 @@ -name: Concrete Python Release +name: concrete-python release-cpu + on: workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - user_inputs: - description: 'either "nightly" or "public" or "private" to specify the release type' - required: true - default: 'nightly' - type: string - + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + schedule: + # Nightly Release @ 3AM after each work day + - cron: "0 3 * * 2-6" env: DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler - GLIB_VER: 2_28 - RELEASE_TYPE: ${{ inputs.user_inputs }} + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + RELEASE_TYPE: ${{ (github.event_name == 'push' && contains(github.ref, 'refs/tags/')) && 'public' || 'nightly' }} + +concurrency: + group: concrete_python_release_cpu_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: release + release-checks: - runs-on: ${{ github.event.inputs.runner_name }} + needs: setup-instance + runs-on: ${{ needs.setup-instance.outputs.runner-name }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,32 +49,29 @@ jobs: fetch-depth: 0 - name: Check python api doc is up to date run: ci/scripts/make_apidocs.sh + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "release-checks finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" build-linux-x86: strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - - runs-on: ${{ github.event.inputs.runner_name }} + needs: setup-instance + runs-on: ${{ needs.setup-instance.outputs.runner-name }} steps: - - name: Log instance configuration - run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - echo "User Inputs: ${{ inputs.user_inputs }}" - - name: Set up GitHub environment run: | echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: recursive fetch-depth: 0 - - name: Set release version (nightly) if: ${{ env.RELEASE_TYPE == 'nightly' }} run: | @@ -75,14 +81,11 @@ jobs: echo "__version__ = \"${LATEST_RELEASE_VERSION}-dev${NIGHTLY_VERSION_ONE_NUMBER}\"" >| frontends/concrete-python/version.txt git tag nightly-$NIGHTLY_VERSION || true git push origin nightly-$NIGHTLY_VERSION || true - - name: Set release version (public) if: ${{ env.RELEASE_TYPE == 'public' }} run: echo "__version__ = \"`git describe --tags --abbrev=0 | grep -e '[0-9].*' -o`\"" >| frontends/concrete-python/version.txt - - name: Expose release version from Python run: cp frontends/concrete-python/version.txt frontends/concrete-python/concrete/fhe/version.py - - name: Build wheel uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 id: build-compiler-bindings @@ -94,15 +97,10 @@ jobs: options: >- -v ${{ github.workspace }}:/concrete -v ${{ github.workspace }}/build:/build - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket - ${{ env.DOCKER_GPU_OPTION }} shell: bash run: | set -e - rustup toolchain install nightly-2024-09-30 - pip install mypy rm -rf /build/* export PYTHON=${{ format('python{0}', matrix.python-version) }} @@ -126,20 +124,25 @@ jobs: make whl deactivate - - name: Upload wheel uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ format('wheel-{0}-linux-x86', matrix.python-version) }} path: frontends/concrete-python/dist/*manylinux*.whl retention-days: 3 + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-linux-x86 finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" build-macos: strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ["aws-mac1-metal", "aws-mac2-metal"] - runs-on: ${{ matrix.runs-on }} steps: - name: Checkout @@ -147,14 +150,11 @@ jobs: with: submodules: recursive fetch-depth: 0 - - name: Install OS Dependencies run: | brew install ninja ccache - - name: Setup rust toolchain for concrete-cpu uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - name: Set release version (nightly) if: ${{ env.RELEASE_TYPE == 'nightly' }} run: | @@ -162,14 +162,11 @@ jobs: NIGHTLY_VERSION_ONE_NUMBER=$(date +"%Y%m%d") LATEST_RELEASE_VERSION=`git tag -l |grep "v.*" |sort |tail -n 1 | grep -e '[0-9].*' -o` echo "__version__ = \"${LATEST_RELEASE_VERSION}-dev${NIGHTLY_VERSION_ONE_NUMBER}\"" >| frontends/concrete-python/version.txt - - name: Set release version (public) if: ${{ env.RELEASE_TYPE == 'public' }} run: echo "__version__ = \"`git describe --tags --abbrev=0 | grep -e '[0-9].*' -o`\"" >| frontends/concrete-python/version.txt - - name: Expose release version from Python run: cp frontends/concrete-python/version.txt frontends/concrete-python/concrete/fhe/version.py - - name: Build wheel run: | export CONCRETE_PYTHON=$(pwd)/frontends/concrete-python @@ -203,13 +200,19 @@ jobs: delocate-wheel -v dist/*macos*.whl deactivate - - name: Upload wheel uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ format('wheel-{0}-{1}', matrix.python-version, matrix.runs-on) }} path: frontends/concrete-python/dist/*macos*.whl retention-days: 3 + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-macos finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" hash: # Generate hashes for the wheels, used later for provenance. @@ -279,28 +282,24 @@ jobs: aws s3 sync ./wheels/ s3://${S3_BUCKET_NAME}/cpu/concrete-python # update indexes and invalidate cloudfront cache python .github/workflows/scripts/s3_update_html_indexes.py - - - name: Start pushing Docker images - if: ${{ env.RELEASE_TYPE == 'public' }} - run: | - export TAG=$(git describe --tags --abbrev=0) - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/zama-ai/concrete/actions/workflows/concrete_python_push_docker_image.yml/dispatches \ - -d "{\"ref\": \"$TAG\", \"inputs\": {\"tag\":\"v$TAG\"}}" + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "push finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" test-linux-x86: - needs: [build-linux-x86] + needs: [setup-instance, build-linux-x86] continue-on-error: true strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - runs-on: ${{ github.event.inputs.runner_name }} + runs-on: ${{ needs.setup-instance.outputs.runner-name }} steps: - - uses: actions-rust-lang/setup-rust-toolchain@11df97af8e8102fd60b60a77dfbf58d40cd843b8 # v1.10.1 + - name: Install rust + uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 - name: Setup Python uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: @@ -343,6 +342,36 @@ jobs: # Running tests make tfhers-utils pytest tests -svv -n auto + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "test-linux-x86 (${{ matrix.python-version }}) finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + needs: [ setup-instance, test-linux-x86 ] + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" test-macos: needs: [build-macos] @@ -396,8 +425,14 @@ jobs: make tfhers-utils mkdir ./KeySetCache pytest tests -svv -n auto --key-cache "./KeySetCache" -m "not dataflow and not graphviz" - - name: Cleanup host if: success() || failure() run: | rm -rf $TEST_TMP_DIR + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "test-macos (${{matrix.runs-on}}/${{ matrix.python-version }}) finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_python_release_gpu.yml b/.github/workflows/concrete_python_release_gpu.yml index 26411b570..231ab2706 100644 --- a/.github/workflows/concrete_python_release_gpu.yml +++ b/.github/workflows/concrete_python_release_gpu.yml @@ -1,61 +1,58 @@ -name: Concrete Python Release (GPU) - +name: concrete-python release-gpu on: workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - user_inputs: - description: 'either "nightly" or "public" or "private" to specify the release type' - required: true - default: 'nightly' - type: string + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + schedule: + # Nightly Release @ 3AM after each work day + - cron: "0 3 * * 2-6" env: DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler CUDA_PATH: /usr/local/cuda-11.8 - GCC_VERSION: 11 - RELEASE_TYPE: ${{ inputs.user_inputs }} + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + RELEASE_TYPE: ${{ (github.event_name == 'push' && contains(github.ref, 'refs/tags/')) && 'public' || 'nightly' }} + +concurrency: + group: concrete_python_release_gpu_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: release + build-linux-x86: + needs: setup-instance + runs-on: ${{ needs.setup-instance.outputs.runner-name }} strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - - runs-on: ${{ github.event.inputs.runner_name }} steps: - - name: Log instance configuration - run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - echo "User Inputs: ${{ inputs.user_inputs }}" - - name: Set up GitHub environment run: | echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: recursive fetch-depth: 0 - - name: Set release version (nightly) if: ${{ env.RELEASE_TYPE == 'nightly' }} run: | @@ -65,14 +62,11 @@ jobs: echo "__version__ = \"${LATEST_RELEASE_VERSION}-dev${NIGHTLY_VERSION_ONE_NUMBER}\"" >| frontends/concrete-python/version.txt git tag nightly-$NIGHTLY_VERSION || true git push origin nightly-$NIGHTLY_VERSION || true - - name: Set release version (public) if: ${{ env.RELEASE_TYPE == 'public' }} run: echo "__version__ = \"`git describe --tags --abbrev=0 | grep -e '[0-9].*' -o`\"" >| frontends/concrete-python/version.txt - - name: Expose release version from Python run: cp frontends/concrete-python/version.txt frontends/concrete-python/concrete/fhe/version.py - - name: Build wheel uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 id: build-compiler-bindings @@ -84,29 +78,23 @@ jobs: options: >- -v ${{ github.workspace }}:/concrete -v ${{ github.workspace }}/build:/build - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket shell: bash run: | set -e - rustup toolchain install nightly-2024-09-30 - pip install mypy rm -rf /build/* - + export PYTHON=${{ format('python{0}', matrix.python-version) }} echo "Using $PYTHON" - dnf -y install graphviz graphviz-devel - cd /concrete/frontends/concrete-python make PYTHON=$PYTHON venv source .venv/bin/activate - + cd /concrete/compilers/concrete-compiler/compiler make BUILD_DIR=/build CCACHE=ON DATAFLOW_EXECUTION_ENABLED=OFF Python3_EXECUTABLE=$(which python) \ CUDA_SUPPORT=ON TIMING_ENABLED=ON CUDA_PATH=${{ env.CUDA_PATH }} python-bindings - + echo "Debug: ccache statistics (after the build):" ccache -s @@ -114,37 +102,62 @@ jobs: export COMPILER_BUILD_DIRECTORY="/build" make whl - - deactivate + deactivate - name: Upload wheel uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ format('wheel-{0}-linux-x86', matrix.python-version) }} path: frontends/concrete-python/dist/*manylinux*.whl retention-days: 3 + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-linux-x86 (${{matrix.python-version}}) finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" - push: + teardown-instance: + needs: [ setup-instance, build-linux-x86 ] + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + push-wheels: needs: [build-linux-x86] runs-on: ubuntu-latest outputs: wheel_version: ${{ steps.version.outputs.wheel_version }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: wheels merge-multiple: true - - - name: Install aws-cli if not present + - name: Install aws-cli run: | aws --version || (curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \ unzip awscliv2.zip && \ sudo ./aws/install) - - name: Upload wheels to S3 - if: ${{ env.RELEASE_TYPE == 'public' || env.RELEASE_TYPE == 'nightly' }} env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_IAM_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_KEY }} @@ -157,18 +170,92 @@ jobs: aws s3 sync ./wheels/ s3://${S3_BUCKET_NAME}/gpu/concrete-python # update indexes and invalidate cloudfront cache python .github/workflows/scripts/s3_update_html_indexes.py - - name: Output Wheel Version id: version run: | export VERSION=`ls ./wheels/*manylinux* | head -n1 | cut -d "-" -f2` echo "VERSION=$VERSION" echo "wheel_version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "push-wheels finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + setup-test-instance: + runs-on: ubuntu-latest + needs: [push-wheels] + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: gpu-test + + test-linux-x86: + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + fail-fast: false + needs: [setup-test-instance, push-wheels] + runs-on: ${{ needs.setup-test-instance.outputs.runner-name }} + steps: + - name: Setup Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: ${{ matrix.python-version }} - test-gpu-wheel: - needs: [push] - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-test-gpu-wheel - user_inputs: "${{ needs.push.outputs.wheel_version }}" + - name: Install concrete-python + run: pip install --pre --extra-index-url https://pypi.zama.ai/gpu/ "concrete-python==${{ needs.push-wheels.outputs.wheel_version }}" + + - name: Checkout the repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: repo + + - name: Test wheel + run: | + CONCRETE_PYTHON=$(pwd)/repo/frontends/concrete-python + + # Install extra requirements for tests + sudo apt update -y + sudo apt install -y graphviz libgraphviz-dev + pip install -r $CONCRETE_PYTHON/requirements.extra-full.txt + pip install -r $CONCRETE_PYTHON/requirements.dev.txt + + # Running tests + cd $CONCRETE_PYTHON + make pytest-gpu + + + teardown-test-instance: + needs: [ setup-test-instance, test-linux-x86 ] + if: ${{ always() && needs.setup-test-instance.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-test-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/concrete_python_test_macos.yml b/.github/workflows/concrete_python_test_macos.yml index 79e940921..9c12d5638 100644 --- a/.github/workflows/concrete_python_test_macos.yml +++ b/.github/workflows/concrete_python_test_macos.yml @@ -1,40 +1,37 @@ -name: Concrete Python Tests (macOS) +name: concrete-python tests macos on: - workflow_call: workflow_dispatch: - secrets: - CONCRETE_CI_SSH_PRIVATE: - required: true - CONCRETE_ACTIONS_TOKEN: - required: true + pull_request: + paths: + - .github/workflows/concrete_python_tests_macos.yml + push: + branches: + - 'main' + - 'release/*' concurrency: - group: concrete_python_tests_macos-${{ github.ref }} + group: concrete_python_tests_macos_${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: - BuildConcreteCompilerAndTestConcretePythonInMacOS: + concrete-python-test-pytest: strategy: fail-fast: false matrix: machine: ["aws-mac1-metal", "aws-mac2-metal"] - runs-on: ${{ matrix.machine }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Install OS Dependencies + fetch-depth: 0 + - name: Install build dependencies run: | brew install ninja ccache - - name: Setup rust toolchain for concrete-cpu uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Cache Compilation (push) + - name: Cache compilation (push) if: github.event_name == 'push' uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: @@ -42,8 +39,7 @@ jobs: key: ${{ runner.os }}-${{ runner.arch }}-compilation-cache-${{ github.sha }} restore-keys: | ${{ runner.os }}-${{ runner.arch }}-compilation-cache- - - - name: Cache Compilation (pull_request) + - name: Cache compilation (pull_request) if: github.event_name == 'pull_request' uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: @@ -51,88 +47,72 @@ jobs: key: ${{ runner.os }}-${{ runner.arch }}-compilation-cache-${{ github.event.pull_request.base.sha }} restore-keys: | ${{ runner.os }}-${{ runner.arch }}-compilation-cache- - - - name: Get tmpdir path - if: github.event_name == 'push' - id: tmpdir-path - run: echo "::set-output name=TMPDIR_PATH::$TMPDIR" - - - name: Build + - name: Prepare build environment run: | set -e - cd frontends/concrete-python - + cd $GITHUB_WORKSPACE/frontends/concrete-python + # Setup pkg-config to find OpenBLAS (scipy need it) export PKG_CONFIG_PATH="/opt/homebrew/opt/openblas/lib/pkgconfig" - + rm -rf .venv python3.10 -m venv .venv - - . .venv/bin/activate - + + . $GITHUB_WORKSPACE/frontends/concrete-python/.venv/bin/activate + pip install -r requirements.dev.txt pip install -r requirements.txt - + - name: Build concrete-compiler python-bindings + run: | + $GITHUB_WORKSPACE/frontends/concrete-python .venv/bin/activate cd $GITHUB_WORKSPACE/compilers/concrete-compiler/compiler - - echo "Debug: ccache statistics (prior to the build):" - ccache -s - + ccache -z make Python3_EXECUTABLE=$(which python) python-bindings - - echo "Debug: ccache statistics (after the build):" ccache -s - - export COMPILER_BUILD_DIRECTORY=$(pwd)/build + - name: Create wheels + run: | + $GITHUB_WORKSPACE/frontends/concrete-python .venv/bin/activate cd $GITHUB_WORKSPACE/frontends/concrete-python - - rm -rf dist - mkdir -p dist - + + export COMPILER_BUILD_DIRECTORY=$GITHUB_WORKSPACE/compilers/concrete-compiler/compiler + rm -rf dist && mkdir -p dist pip wheel -v --no-deps -w dist . delocate-wheel -v dist/*macos*.whl - - deactivate + deactivate - name: Prepare test environment run: | set -e export TEST_TMP_DIR=$(mktemp -d) echo "TEST_TMP_DIR=$TEST_TMP_DIR" >> "${GITHUB_ENV}" cd $TEST_TMP_DIR - + python3.10 -m venv .testenv . .testenv/bin/activate - + pip install $GITHUB_WORKSPACE/frontends/concrete-python/dist/*macos*.whl pip install -r $GITHUB_WORKSPACE/frontends/concrete-python/requirements.dev.txt # MacOS x86 have conflict between our OpenMP library, and one from torch # we fix it by using a single one (from torch) # see discussion: https://discuss.python.org/t/conflicting-binary-extensions-in-different-packages/25332/8 - + find .testenv/lib/python3.10/site-packages -not \( -path .testenv/lib/python3.10/site-packages/concrete -prune \) -name 'lib*omp5.dylib' -or -name 'lib*omp.dylib' | xargs -n 1 ln -f -s $(pwd)/.testenv/lib/python3.10/site-packages/concrete/.dylibs/libomp.dylib cp -R $GITHUB_WORKSPACE/frontends/concrete-python/examples ./examples cp -R $GITHUB_WORKSPACE/frontends/concrete-python/tests ./tests - - cp $GITHUB_WORKSPACE/frontends/concrete-python/Makefile . - - name: Test + cp $GITHUB_WORKSPACE/frontends/concrete-python/Makefile . + - name: Run pytest-macos run: | - set -e - export TEST_TMP_DIR="testing_concrete_python" cd $TEST_TMP_DIR && . .testenv/bin/activate KEY_CACHE_DIRECTORY=./KeySetCache PYTEST_MARKERS="not dataflow and not graphviz" make pytest-macos - - - name: Test notebooks + - name: Run test-notebooks run: | set -e - export TEST_TMP_DIR="testing_concrete_python" cd $TEST_TMP_DIR && . .testenv/bin/activate make test-notebooks - - name: Cleanup host if: success() || failure() run: | diff --git a/.github/workflows/concrete_python_tests_linux.yml b/.github/workflows/concrete_python_tests_linux.yml index 696092695..3cbbc9340 100644 --- a/.github/workflows/concrete_python_tests_linux.yml +++ b/.github/workflows/concrete_python_tests_linux.yml @@ -1,69 +1,72 @@ -name: Concrete Python Tests (Linux) +name: concrete-python tests linux-cpu on: workflow_dispatch: - inputs: - instance_id: - description: 'Instance ID' - type: string - instance_image_id: - description: 'Instance AMI ID' - type: string - instance_type: - description: 'Instance product type' - type: string - runner_name: - description: 'Action runner name' - type: string - request_id: - description: 'Slab request ID' - type: string - -# concurrency: -# group: concrete_python_tests_linux-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + pull_request: + paths: + - .github/workflows/concrete_python_tests_linux.yml + - frontends/concrete-python/** + push: + branches: + - 'main' + - 'release/*' env: DOCKER_IMAGE_TEST: ghcr.io/zama-ai/concrete-compiler - CUDA_PATH: /usr/local/cuda-11.8 - GCC_VERSION: 11 - GLIB_VER: 2_28 + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +concurrency: + group: concrete_python_tests_linux_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: - BuildConcreteCompilerAndTestConcretePythonInLinux: - name: Build Concrete Compiler and Test Concrete Python in Linux - runs-on: ${{ github.event.inputs.runner_name }} - if: ${{ !cancelled() }} + setup-instance: + runs-on: ubuntu-latest + outputs: + runner-name: ${{ steps.start-instance.outputs.label }} + steps: + - name: Start instance + id: start-instance + uses: zama-ai/slab-github-runner@447a2d0fd2d1a9d647aa0d0723a6e9255372f261 + with: + mode: start + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + backend: aws + profile: cpu-test + + pre-commit-check: + runs-on: ubuntu-22.04 steps: - - name: Log instance configuration + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Install platform dependencies run: | - echo "IDs: ${{ inputs.instance_id }}" - echo "AMI: ${{ inputs.instance_image_id }}" - echo "Type: ${{ inputs.instance_type }}" - echo "Request ID: ${{ inputs.request_id }}" - - - name: Set up GitHub environment + sudo apt install -y graphviz libgraphviz-dev + - name: Pre-commit Checks run: | - echo "HOME=/home/ubuntu" >> "${GITHUB_ENV}" - #echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK)" >> "${GITHUB_ENV}" - echo "SSH_AUTH_SOCK_DIR=$(dirname $SSH_AUTH_SOCK)" >> "${GITHUB_ENV}" - - - name: Checkout + cd frontends/concrete-python + make venv + source .venv/bin/activate + make pcc + + build-python-bindings: + needs: setup-instance + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + steps: + - name: Checkout concrete uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: recursive - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Create build directory + fetch-depth: 0 + - name: Create concrete build directory run: mkdir build - - name: Setup rust toolchain for concrete-cpu - uses: ./.github/workflows/setup_rust_toolchain_for_concrete_cpu - - - name: Build bindings + - name: Build concrete-compiler python bindings uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - if: ${{ !contains(inputs.instance_type, 'p3') }} - id: build-compiler-bindings with: registry: ghcr.io image: ${{ env.DOCKER_IMAGE_TEST }} @@ -72,73 +75,56 @@ jobs: options: >- -v ${{ github.workspace }}:/concrete -v ${{ github.workspace }}/build:/build - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket shell: bash run: | - rustup toolchain install nightly-2024-09-30 - pip install mypy set -e - rm -rf /build/* + rustup toolchain install nightly-2024-09-30 dnf -y install graphviz graphviz-devel cd /concrete/frontends/concrete-python make venv source .venv/bin/activate - + cd /concrete/compilers/concrete-compiler/compiler make BUILD_DIR=/build DATAFLOW_EXECUTION_ENABLED=ON CCACHE=ON Python3_EXECUTABLE=$(which python3) python-bindings - + echo "Debug: ccache statistics (after the build):" ccache -s - - - name: Prepare test environment - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - if: ${{ !contains(inputs.instance_type, 'p3') }} + - name: Create artifact archive + run: | + cd build + tar czvf artifacts.tgz lib/libConcretelangRuntime.so tools/concretelang/python_packages + - name: Upload concrete-compiler python-bindings + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE_TEST }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - shell: bash - run: | - set -e - - dnf -y install graphviz graphviz-devel - - cd /concrete/frontends/concrete-python - make venv - - - name: Test - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - if: ${{ !contains(inputs.instance_type, 'p3') }} + name: concrete-compiler-python-bindings + include-hidden-files: true + retention-days: 3 + path: build/artifacts.tgz + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "build-python-bindings finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + test-pytest: + needs: [setup-instance, build-python-bindings] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + steps: + - name: Download concrete-compiler python-bindings + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE_TEST }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - shell: bash - run: | - set -e - - cd /concrete/frontends/concrete-python - source .venv/bin/activate - - export COMPILER_BUILD_DIRECTORY=/build - - mkdir ./KeySetCache - KEY_CACHE_DIRECTORY=./KeySetCache make pytest - - - name: Test notebooks + name: concrete-compiler-python-bindings + path: compiler-artifacts + - name: Extract artifacts archive + run: | + cd compiler-artifacts + tar xzvf artifacts.tgz + - name: Run pytest uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - if: ${{ !contains(inputs.instance_type, 'p3') }} with: registry: ghcr.io image: ${{ env.DOCKER_IMAGE_TEST }} @@ -146,53 +132,39 @@ jobs: password: ${{ secrets.GHCR_PASSWORD }} options: >- -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build + -v ${{ github.workspace }}/compiler-artifacts:/compiler-artifacts shell: bash run: | set -e - + export COMPILER_BUILD_DIRECTORY=/compiler-artifacts cd /concrete/frontends/concrete-python source .venv/bin/activate - - export COMPILER_BUILD_DIRECTORY=/build - - make test-notebooks - - - - name: Build bindings gpu - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - if: ${{ contains(inputs.instance_type, 'p3') }} - id: build-compiler-bindings-gpu + export KEY_CACHE_DIRECTORY=./key-set-cache + mkdir $KEY_CACHE_DIRECTORY + make pytest + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "test-pytest finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + test-notebooks: + needs: [setup-instance, build-python-bindings] + runs-on: ${{ needs.setup-instance.outputs.runner-name }} + steps: + - name: Download concrete-compiler python-bindings + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - registry: ghcr.io - image: ${{ env.DOCKER_IMAGE_TEST }} - username: ${{ secrets.GHCR_LOGIN }} - password: ${{ secrets.GHCR_PASSWORD }} - options: >- - -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - -v ${{ github.workspace }}/wheels:/wheels - -v ${{ env.SSH_AUTH_SOCK }}:/ssh.socket - -e SSH_AUTH_SOCK=/ssh.socket - --gpus all - shell: bash - run: | - set -e - rm -rf /build/* - - cd /concrete/frontends/concrete-python - make venv - source .venv/bin/activate - - cd /concrete/compilers/concrete-compiler/compiler - make BUILD_DIR=/build CCACHE=ON DATAFLOW_EXECUTION_ENABLED=ON Python3_EXECUTABLE=$(which python3) CUDA_SUPPORT=ON CUDA_PATH=${{ env.CUDA_PATH }} python-bindings - - echo "Debug: ccache statistics (after the build):" - ccache -s - - - name: Test gpu + name: concrete-compiler-python-bindings + path: compiler-artifacts + - name: Extract artifacts archive + run: | + cd compiler-artifacts + tar xzvf artifacts.tgz + - name: Run pytest uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - if: ${{ contains(inputs.instance_type, 'p3') }} with: registry: ghcr.io image: ${{ env.DOCKER_IMAGE_TEST }} @@ -200,19 +172,41 @@ jobs: password: ${{ secrets.GHCR_PASSWORD }} options: >- -v ${{ github.workspace }}:/concrete - -v ${{ github.workspace }}/build:/build - -v ${{ github.workspace }}/wheels:/wheels - --gpus all + -v ${{ github.workspace }}/compiler-artifacts:/compiler-artifacts shell: bash run: | set -e - + export COMPILER_BUILD_DIRECTORY=/compiler-artifacts cd /concrete/frontends/concrete-python - make venv source .venv/bin/activate - - export COMPILER_BUILD_DIRECTORY=/build - KEY_CACHE_DIRECTORY=/tmp/KeySetCache mkdir ./KeySetCache - make pytest-gpu - - chmod -R ugo+rwx /tmp/KeySetCache + make test-notebooks + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "test-notebooks finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" + + teardown-instance: + if: ${{ always() && needs.setup-instance.result != 'skipped' }} + needs: [ setup-instance, test-pytest, test-notebooks ] + runs-on: ubuntu-latest + steps: + - name: Stop instance + id: stop-instance + uses: zama-ai/slab-github-runner@c0e7168795bd78f61f61146951ed9d0c73c9b701 + with: + mode: stop + github-token: ${{ secrets.SLAB_ACTION_TOKEN }} + slab-url: ${{ secrets.SLAB_BASE_URL }} + job-secret: ${{ secrets.JOB_SECRET }} + label: ${{ needs.setup-instance.outputs.runner-name }} + + - name: Slack Notification + if: ${{ failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Instance teardown finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/docker-lint.yml b/.github/workflows/docker-lint.yml deleted file mode 100644 index 197f72a97..000000000 --- a/.github/workflows/docker-lint.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Lint Dockerfiles - -on: - pull_request: - push: - branches: - - main - -jobs: - lint: - runs-on: ubuntu-latest - container: - image: hadolint/hadolint@sha256:27173fe25e062448490a32de410c08491c626a0bef360aa2ce5d5bdd9384b50d #2.12.0-debian - steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - - name: Lint All Dockerfiles (except third_party) - run: hadolint -V `find -name "*Dockerfile*" -not -path "./third_party/*" |xargs ` diff --git a/.github/workflows/docker_compliance.yml b/.github/workflows/docker_compliance.yml new file mode 100644 index 000000000..d5da1eda4 --- /dev/null +++ b/.github/workflows/docker_compliance.yml @@ -0,0 +1,29 @@ +name: check docker files compliance + +on: + pull_request: + paths: + - .github/workflows/docker_compliance.yml + - '**Dockerfile**' + push: + branches: + - main + - 'release/*' + +jobs: + lint: + runs-on: ubuntu-latest + container: + image: hadolint/hadolint@sha256:27173fe25e062448490a32de410c08491c626a0bef360aa2ce5d5bdd9384b50d #2.12.0-debian + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: Lint All Dockerfiles (except third_party) + run: hadolint -V `find -name "*Dockerfile*" -not -path "./third_party/*" |xargs ` + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "lint finished with status: ${{ job.status }}. (${{ env.ACTION_RUN_URL }})" diff --git a/.github/workflows/linelint.yml b/.github/workflows/linelint.yml deleted file mode 100644 index 03b814c83..000000000 --- a/.github/workflows/linelint.yml +++ /dev/null @@ -1,18 +0,0 @@ -# This job is the main jobs will dispatch build and test for every modules of our mono repo. -name: Linelint - -on: - pull_request: - push: - branches: - - 'main' - -jobs: - linelint: - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Linelint - uses: fernandrone/linelint@8136e0fa9997122d80f5f793e0bb9a45e678fbb1 # 0.0.4 - id: linelint diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index e39d983fd..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,303 +0,0 @@ -# This job is the main jobs will dispatch build and test for every modules of our mono repo. -name: Main - -on: - pull_request: - push: - branches: - - 'main' - - 'release/*' - - 'force-docker-images' - - 'private_release/*' - tags: - - 'v[0-9]+.[0-9]+.[0-9]+*' - schedule: - # Nightly Release @ 3AM after each work day - - cron: "0 3 * * 2-6" - -jobs: - # This jobs outputs for each modules of our mono-repo if it changed, - # in order to launch jobs only for the changed modules - file-change: - if: ${{ github.event_name != 'schedule' }} - runs-on: ubuntu-latest - outputs: - compiler: ${{ steps.compiler.outputs.any_changed }} - optimizer: ${{ steps.optimizer.outputs.any_changed }} - concrete-cpu: ${{ steps.concrete-cpu.outputs.any_changed }} - concrete-cpu-api: ${{ steps.concrete-cpu-api.outputs.any_changed }} - concrete-cuda-api: ${{ steps.concrete-cuda-api.outputs.any_changed }} - concrete-python: ${{ steps.concrete-python.outputs.any_changed }} - concrete-compiler-cpu-workflow: ${{ steps.concrete-compiler-cpu-workflow.outputs.any_changed }} - concrete-compiler-gpu-workflow: ${{ steps.concrete-compiler-gpu-workflow.outputs.any_changed }} - concrete-compiler-format-and-linting-workflow: ${{ steps.concrete-compiler-format-and-linting-workflow.outputs.any_changed }} - concrete-compiler-macos-workflow: ${{ steps.concrete-compiler-macos-workflow.outputs.any_changed }} - concrete-compiler-docker-images-workflow: ${{ steps.concrete-compiler-docker-images-workflow.outputs.any_changed }} - concrete-cpu-workflow: ${{ steps.concrete-cpu-workflow.outputs.any_changed }} - concrete-python-workflow: ${{ steps.concrete-python-workflow.outputs.any_changed }} - concrete-optimizer-workflow: ${{ steps.concrete-optimizer-workflow.outputs.any_changed }} - push-main: ${{ steps.github.outputs.push-main }} - steps: - - name: Checkout the repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Get changed files in the concrete-compiler directory - id: compiler - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./compilers/concrete-compiler/** - - - name: Get changed files for concrete-optimizer - id: optimizer - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: | - ./tools/parameter-curves/concrete-security-curves-rust/** - ./compilers/concrete-optimizer/** - ./.github/workflows/optimizer.yml - - - name: Get changed files in the concrete-cpu directory - id: concrete-cpu - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./backends/concrete-cpu/implementation/** - - - name: Get changed files in the concrete-python directory - id: concrete-python - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./frontends/concrete-python/** - - - name: Check if compiler_build_and_test_cpu workflow has changed - id: concrete-compiler-cpu-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./.github/workflows/compiler_build_and_test_cpu.yml - - - name: Check if compiler_build_and_test_gpu workflow has changed - id: concrete-compiler-gpu-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./.github/workflows/compiler_build_and_test_gpu.yml - - - name: Check if compiler_format_and_linting.yml workflow has changed - id: concrete-compiler-format-and-linting-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./.github/workflows/compiler_format_and_linting.yml - - - name: Check if compiler_macos_build_and_test workflow has changed - id: concrete-compiler-macos-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./.github/workflows/compiler_macos_build_and_test.yml - - - name: Check if compiler_publish_docker_images workflow has changed - id: concrete-compiler-docker-images-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: | - ./.github/workflows/compiler_publish_docker_images.yml - ./docker/** - - - name: Check if concrete_cpu_test workflow has changed - id: concrete-cpu-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./.github/workflows/concrete_cpu_test.yml - - - name: Check if concrete_python_checks workflow has changed - id: concrete-python-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./.github/workflows/concrete_python_checks.yml - - - name: Check if optimizer workflow has changed - id: concrete-optimizer-workflow - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./.github/workflows/optimizer.yml - - - name: Get changed files in the concrete-cpu directory - id: concrete-cpu-api - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./backends/concrete-cpu/implementation/include/** - - - name: Get changed files in the concrete-cuda directory - id: concrete-cuda-api - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 - with: - files: ./backends/concrete-cuda/implementation/include/** - - - name: Set some github event outputs - id: github - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release')) - run: echo "push-main=true" >> "$GITHUB_OUTPUT" - -################################################# -# Compiler jobs ################################# - compiler-compliance: - needs: file-change - if: needs.file-change.outputs.compiler == 'true' || needs.file-change.outputs.concrete-compiler-format-and-linting-workflow == 'true' || needs.file-change.outputs.push-main == 'true' - uses: ./.github/workflows/compiler_format_and_linting.yml - - compiler-cpu-build: - needs: file-change - if: needs.file-change.outputs.compiler == 'true' || needs.file-change.outputs.concrete-cpu-api == 'true'|| needs.file-change.outputs.concrete-compiler-cpu-workflow == 'true' || needs.file-change.outputs.push-main == 'true' - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: compiler-cpu-build - - compiler-cpu-build-distributed: - needs: file-change - if: needs.file-change.outputs.compiler == 'true' || needs.file-change.outputs.concrete-cpu-api == 'true'|| needs.file-change.outputs.concrete-compiler-cpu-workflow == 'true' || needs.file-change.outputs.push-main == 'true' - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: compiler-cpu-build-distributed - - compiler-gpu-build: - needs: file-change - if: needs.file-change.outputs.compiler == 'true' || needs.file-change.outputs.concrete-cuda-api == 'true' || needs.file-change.outputs.concrete-compiler-gpu-workflow == 'true' || needs.file-change.outputs.push-main == 'true' - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: compiler-gpu-build - - compiler-macos-tests: - needs: file-change - if: needs.file-change.outputs.compiler == 'true' || needs.file-change.outputs.concrete-compiler-macos-workflow == 'true' || needs.file-change.outputs.push-main == 'true' - uses: ./.github/workflows/compiler_macos_build_and_test.yml - secrets: inherit - - compiler-publish-docker-images: - needs: file-change - if: (needs.file-change.outputs.compiler == 'true' || needs.file-change.outputs.concrete-compiler-docker-images-workflow == 'true') && (needs.file-change.outputs.push-main == 'true' || contains(github.ref, 'refs/heads/force-docker-images')) - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: compiler-publish-docker-images - - compiler-cpu-benchmark: - needs: file-change - if: needs.file-change.outputs.push-main == 'true' - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: compiler-cpu-benchmark - - # compiler-gpu-benchmark: - # needs: file-change - # if: needs.file-change.outputs.push-main == 'true' - # uses: ./.github/workflows/start_slab.yml - # secrets: inherit - # with: - # command: compiler-gpu-benchmark - -################################################# -# Optimizer jobs ################################ - optimizer: - needs: file-change - if: | - needs.file-change.outputs.parameters-curves == 'true' || - needs.file-change.outputs.concrete-cpu == 'true' || - needs.file-change.outputs.optimizer == 'true'|| - needs.file-change.outputs.push-main - uses: ./.github/workflows/optimizer.yml - secrets: inherit - -################################################# -# ConcreteCPU jobs ############################## - concrete-cpu: - needs: file-change - if: needs.file-change.outputs.concrete-cpu == 'true' || needs.file-change.outputs.concrete-cpu-workflow == 'true' || needs.file-change.outputs.push-main - uses: ./.github/workflows/concrete_cpu_test.yml - secrets: inherit - -################################################# -# Concrete Python jobs ########################## - concrete-python: - needs: file-change - if: needs.file-change.outputs.concrete-python == 'true' || needs.file-change.outputs.concrete-python-workflow == 'true' || needs.file-change.outputs.push-main - uses: ./.github/workflows/concrete_python_checks.yml - secrets: inherit - - concrete-python-tests-linux: - needs: file-change - if: needs.file-change.outputs.concrete-python == 'true' || needs.file-change.outputs.push-main - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-tests-linux - - concrete-python-tests-linux-gpu: - needs: file-change - if: needs.file-change.outputs.concrete-python == 'true' && needs.file-change.outputs.push-main - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-tests-linux-gpu - - concrete-python-tests-macos: - needs: file-change - if: needs.file-change.outputs.concrete-python == 'true' || needs.file-change.outputs.push-main - uses: ./.github/workflows/concrete_python_test_macos.yml - secrets: inherit - -################################################# -# Concrete-ML tests ############################# - concrete-ml-tests-linux: - needs: file-change - if: needs.file-change.outputs.concrete-python == 'true' || needs.file-change.outputs.compiler == 'true' || needs.file-change.outputs.push-main - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: ml-test - -################################################# -# Release jobs ################################# - concrete-python-nightly-release: - if: ${{ github.event_name == 'schedule' }} - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-release - user_inputs: 'nightly' - - concrete-python-nightly-release-gpu: - if: ${{ github.event_name == 'schedule' }} - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-release-gpu - user_inputs: 'nightly' - - concrete-python-public-release: -# needs: [compiler-cpu-build, compiler-macos-tests, compiler-publish-docker-images, concrete-python-tests-linux, concrete-python-tests-macos] - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-release - user_inputs: 'public' - - concrete-python-public-release-gpu: - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-release-gpu - user_inputs: 'public' - - concrete-python-private-release: - if: github.event_name == 'push' && contains(github.ref, 'refs/heads/private_release/') - uses: ./.github/workflows/start_slab.yml - secrets: inherit - with: - command: concrete-python-release - user_inputs: 'private' diff --git a/.github/workflows/markdown_link_check.yml b/.github/workflows/markdown_link_check.yml deleted file mode 100644 index b2c6ffb84..000000000 --- a/.github/workflows/markdown_link_check.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Check Markdown links - -on: - pull_request: - paths: - - '**.md' - - .github/workflows/markdown_link_check.yml - push: - branches: - - main - -jobs: - markdown-link-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # v1 - with: - use-quiet-mode: 'yes' - use-verbose-mode: 'yes' diff --git a/.github/workflows/optimizer_setup/action.yml b/.github/workflows/optimizer_setup/action.yml index d5887aa7d..b4048c82d 100644 --- a/.github/workflows/optimizer_setup/action.yml +++ b/.github/workflows/optimizer_setup/action.yml @@ -1,8 +1,3 @@ -inputs: - ssh_private_key: - description: 'A ssh key to access private github repository' - required: true - runs: using: "composite" steps: @@ -10,6 +5,8 @@ runs: uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 with: toolchain: stable + default: true + override: true - name: Download cargo cache uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 diff --git a/.github/workflows/push_wheels_to_public_pypi.yml b/.github/workflows/push_wheels_to_public_pypi.yml deleted file mode 100644 index 3f1ff7d80..000000000 --- a/.github/workflows/push_wheels_to_public_pypi.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Push Wheels to Public PyPI - -on: - workflow_dispatch: - inputs: - version: - description: 'version of concrete-python to pull from Zama PyPI and push to public PyPI. Use the version as it appears in the wheel file (e.g. 2.7.0rc1)' - required: true - type: string - - -jobs: - pull_and_push: - runs-on: ubuntu-latest - steps: - - name: Pull wheels from S3 - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_IAM_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} - S3_BUCKET_NAME: ${{ secrets.AWS_S3_PYPI_BUCKET_NAME }} - run: | - mkdir wheels - aws s3 cp s3://${S3_BUCKET_NAME}/cpu/concrete-python/ ./wheels/ --recursive --exclude "*" --include "concrete_python-${{ inputs.version }}-*" - echo "============== Downloaded wheels ===============" - ls -la ./wheels/ - - - name: Push wheels to public PyPI (public) - run: | - pip install twine==4.0.2 - twine upload wheels/concrete_python-${{ inputs.version }}*.whl \ - -u "${{ secrets.PUBLIC_PYPI_USER }}" \ - -p "${{ secrets.PUBLIC_PYPI_PASSWORD }}" \ - -r pypi - diff --git a/.github/workflows/scripts/teardown-check.sh b/.github/workflows/scripts/teardown-check.sh new file mode 100755 index 000000000..bff9254ea --- /dev/null +++ b/.github/workflows/scripts/teardown-check.sh @@ -0,0 +1,10 @@ +#!/bin/bash -e + +grep setup-instance -Rl .github/workflows/ | xargs grep -L teardown-instance &> missing-teardown.txt + +if [ -s missing-teardown.txt ]; then + echo "There are missing teardown-instance jobs in following jobs:" + echo + cat missing-teardown.txt + exit 1 +fi diff --git a/.github/workflows/start_slab.yml b/.github/workflows/start_slab.yml deleted file mode 100644 index 0bdd7961c..000000000 --- a/.github/workflows/start_slab.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Start job on Slab CI bot given by input command. -name: Start AWS job - -on: - workflow_call: - inputs: - command: - required: true - type: string - user_inputs: - required: false - type: string - workflow_dispatch: - inputs: - command: - required: true - type: string - user_inputs: - description: 'user inputs to be forwarded to the called workflow' - required: false - type: string - -env: - GIT_REF: ${{ github.head_ref }} - -jobs: - sl: - runs-on: ubuntu-latest - steps: - - name: Checkout concrete - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - - name: Get git ref - # github.head_ref is only available from a Pull Request - if: env.GIT_REF == '' - run: | - echo "GIT_REF=${{ github.ref_name }}" >> $GITHUB_ENV - - - name: Checkout Slab repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - repository: zama-ai/slab - path: slab - token: ${{ secrets.CONCRETE_ACTIONS_TOKEN }} - - - name: Start AWS job in Slab - shell: bash - run: | - GIT_SHA="$(git --no-pager show -s --format="%H" origin/${{ env.GIT_REF }})" || GIT_SHA=${{ github.sha }} - echo -n '{"command": "${{ inputs.command }}", "git_ref": "${{ env.GIT_REF }}", "sha":"'${GIT_SHA}'", "user_inputs": "${{ inputs.user_inputs }}"}' > command.json - cat command.json - SIGNATURE="$(slab/scripts/hmac_calculator.sh command.json '${{ secrets.JOB_SECRET }}')" - curl -v -k \ - --fail-with-body \ - -H "Content-Type: application/json" \ - -H "X-Slab-Repository: ${{ github.repository }}" \ - -H "X-Slab-Command: start_aws" \ - -H "X-Hub-Signature-256: sha256=${SIGNATURE}" \ - -d @command.json \ - ${{ secrets.SLAB_URL }} diff --git a/ci/ec2_products_cost.json b/ci/ec2_products_cost.json index 436cabb13..d43366be8 100644 --- a/ci/ec2_products_cost.json +++ b/ci/ec2_products_cost.json @@ -1,4 +1,5 @@ { + "hpc7a.96xlarge": 7.200, "m7i.48xlarge": 9.677, "m7i.metal-48xl": 9.677, "m6i.metal": 7.168, diff --git a/ci/slab.toml b/ci/slab.toml index 0a42840d0..07e1a293e 100644 --- a/ci/slab.toml +++ b/ci/slab.toml @@ -1,136 +1,29 @@ -# This is the new version of Slab that handles multi backend providers. -[backend.aws.m7i-cpu-bench] +[backend.aws.cpu-test] region = "eu-west-1" image_id = "ami-002bdcd64b8472cf9" # Based on Ubuntu 22.4 -instance_type = "m7i.48xlarge" -security_group = ["sg-0e55cc31dfda0d8a7", ] - -[profile.m7i-cpu-bench] -region = "eu-west-1" -image_id = "ami-002bdcd64b8472cf9" # Based on Ubuntu 22.4 -instance_type = "m7i.48xlarge" -security_group= ["sg-0e55cc31dfda0d8a7", ] - -[profile.m7i-cpu-test] -region = "eu-west-1" -image_id = "ami-002bdcd64b8472cf9" instance_type = "m7i.16xlarge" -security_group= ["sg-0e55cc31dfda0d8a7", ] +security_group = ["sg-0e55cc31dfda0d8a7", ] -[profile.m7i-metal] +[backend.aws.cpu-bench] region = "eu-west-1" image_id = "ami-002bdcd64b8472cf9" -instance_type = "m7i.metal-24xl" -security_group= ["sg-0e55cc31dfda0d8a7", ] - -[profile.gpu-bench] -region = "us-east-1" -image_id = "ami-08e27480d79e82238" -instance_type = "p3.2xlarge" -subnet_id = "subnet-8123c9e7" -security_group= ["sg-017afab1f328af917", ] +instance_type = "hpc7a.96xlarge" -# Docker is well configured for test inside docker in this AMI -[profile.gpu-test] +[backend.aws.gpu-test] region = "us-east-1" image_id = "ami-0257c6ad39f902b5e" instance_type = "p3.2xlarge" subnet_id = "subnet-8123c9e7" security_group= ["sg-017afab1f328af917", ] -# It has CUDA Driver (<=12.5) and Docker installed -[profile.gpu-test-ubuntu22] -region = "us-east-1" -image_id = "ami-05385e0c3c574621f" -instance_type = "p3.2xlarge" -subnet_id = "subnet-8123c9e7" -security_group= ["sg-017afab1f328af917", ] - -[profile.slurm-cluster] +[backend.aws.slurm-cluster] region = "eu-west-3" image_id = "ami-0bb5bb9cb747b5ddd" instance_id = "i-0e5ae2a14134d6275" instance_type = "m6i.8xlarge" security_group= ["sg-02dd8470fa845f31b", ] -################################################# -# Compiler commands -################################################# - -[command.compiler-cpu-build] -workflow = "compiler_build_and_test_cpu.yml" -profile = "m7i-cpu-test" -check_run_name = "Compiler Build and Test (CPU)" - -[command.compiler-cpu-build-distributed] -workflow = "compiler_build_and_test_cpu_distributed.yml" -profile = "slurm-cluster" -check_run_name = "Compiler Distributed Build and Test (CPU)" -runner_name = "distributed-ci" - -[command.compiler-gpu-build] -workflow = "compiler_build_and_test_gpu.yml" -profile = "gpu-test" -check_run_name = "Compiler Build and Test (GPU)" - -[command.compiler-cpu-benchmark] -workflow = "compiler_benchmark.yml" -profile = "m7i-cpu-bench" -check_run_name = "Compiler Performances Benchmarks (CPU)" - -[command.compiler-gpu-benchmark] -workflow = "compiler_benchmark.yml" -profile = "gpu-bench" -check_run_name = "Compiler Performances Benchmarks (GPU)" - -# Trigger Docker images build -[command.compiler-publish-docker-images] -workflow = "compiler_publish_docker_images.yml" -profile = "m7i-cpu-test" -check_run_name = "Compiler - Docker images build & publish" - -# Trigger ML benchmarks by running each use cases subset in parallel. -[command.ml-bench] -workflow = "ml_benchmark_subset.yml" -profile = "m7i-cpu-bench" -matrix = [0,1,2,3,4,5,6,7,8,9,10] -max_parallel_jobs = 2 - -# Trigger ML tests with latest CP -[command.ml-test] -workflow = "concrete_ml_tests.yml" -profile = "m7i-cpu-test" -check_run_name = "Concrete ML Tests" - -################################################# -# Concrete Python Commands -################################################# - -[command.concrete-python-tests-linux] -workflow = "concrete_python_tests_linux.yml" -profile = "m7i-cpu-test" -check_run_name = "Concrete Python Tests (Linux)" - -[command.concrete-python-tests-linux-gpu] -workflow = "concrete_python_tests_linux.yml" -profile = "gpu-test" -check_run_name = "Concrete Python Tests (Linux Gpu)" - -################################################# -# Release Commands -################################################# - -[command.concrete-python-release] -workflow = "concrete_python_release.yml" -profile = "m7i-cpu-test" -check_run_name = "Concrete Python Release" - -[command.concrete-python-release-gpu] -workflow = "concrete_python_release_gpu.yml" -profile = "m7i-cpu-test" -check_run_name = "Concrete Python Release (GPU)" - -[command.concrete-python-test-gpu-wheel] -workflow = "concrete_python_test_gpu_wheel.yml" -profile = "gpu-test" -check_run_name = "Concrete Python Test GPU Wheel" +[backend.aws.release] +region = "eu-west-1" +image_id = "ami-002bdcd64b8472cf9" +instance_type = "hpc7a.96xlarge" diff --git a/compilers/concrete-compiler/compiler/Makefile b/compilers/concrete-compiler/compiler/Makefile index c67d5dd2d..58f5de526 100644 --- a/compilers/concrete-compiler/compiler/Makefile +++ b/compilers/concrete-compiler/compiler/Makefile @@ -479,33 +479,6 @@ else detected_OS := $(shell sh -c 'uname 2>/dev/null || echo Unknown') endif -PIP=$(Python3_EXECUTABLE) -m pip -PIP_WHEEL=$(PIP) wheel --no-deps -w $(BUILD_DIR)/wheels . -AUDIT_WHEEL_REPAIR=$(Python3_EXECUTABLE) -m auditwheel repair -w $(BUILD_DIR)/wheels - -linux-python-package: - $(PIP) install wheel auditwheel - # We need to run it twice: the first will generate the directories, so that - # the second run can find the packages via find_namespace_packages - $(PIP_WHEEL) - $(PIP_WHEEL) - GLIBC_VER=$(shell ldd --version | head -n 1 | grep -o '[^ ]*$$'|head|tr '.' '_'); \ - for PLATFORM in manylinux_$${GLIBC_VER}_x86_64 linux_x86_64; do \ - if $(AUDIT_WHEEL_REPAIR) $(BUILD_DIR)/wheels/*.whl --plat $$PLATFORM; then \ - echo Success for $$PLATFORM; \ - break; \ - else \ - echo No repair with $$PLATFORM; \ - fi \ - done - -darwin-python-package: - $(PIP) install wheel delocate - $(PIP_WHEEL) - delocate-wheel -v $(BUILD_DIR)/wheels/*macosx*.whl - -python-package: python-bindings $(OS)-python-package - @echo The python package is: $(BUILD_DIR)/wheels/*.whl install: concretecompiler install-deps $(info Install prefix set to $(INSTALL_PREFIX)) diff --git a/compilers/concrete-compiler/compiler/lib/Bindings/Python/requirements_dev.txt b/compilers/concrete-compiler/compiler/lib/Bindings/Python/requirements_dev.txt index 021be4705..199b6a550 100644 --- a/compilers/concrete-compiler/compiler/lib/Bindings/Python/requirements_dev.txt +++ b/compilers/concrete-compiler/compiler/lib/Bindings/Python/requirements_dev.txt @@ -1,3 +1,4 @@ black==24.4.0 pylint==2.11.1 mypy==1.11.2 +numpy>=1.23,<2.0 diff --git a/frontends/concrete-python/Makefile b/frontends/concrete-python/Makefile index ef0454580..7b7ace237 100644 --- a/frontends/concrete-python/Makefile +++ b/frontends/concrete-python/Makefile @@ -30,9 +30,6 @@ CONCRETE_VERSION?="" # empty mean latest venv: $(PYTHON) -m venv .venv . .venv/bin/activate -ifeq (,$(wildcard ${RUNTIME_LIBRARY})) - $(PIP) install --extra-index-url https://pypi.zama.ai/cpu "concrete-python$(CONCRETE_VERSION)" -endif $(PIP) install -r requirements.dev.txt $(PIP) install -r requirements.extra-full.txt $(PIP) install -r requirements.txt diff --git a/frontends/concrete-python/scripts/checks/checks.sh b/frontends/concrete-python/scripts/checks/checks.sh deleted file mode 100755 index 25a3bbb6c..000000000 --- a/frontends/concrete-python/scripts/checks/checks.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -ex - -cd frontends/concrete-python -make venv -source .venv/bin/activate -make pcc From 5d95e7b87285114d431dcc901a3ed53f0676c9be Mon Sep 17 00:00:00 2001 From: Quentin Bourgerie Date: Tue, 12 Nov 2024 13:59:23 +0100 Subject: [PATCH 4/4] chore(ci): Disable distributed test waiting slab update --- .../concrete_compiler_test_cpu_distributed.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/concrete_compiler_test_cpu_distributed.yml b/.github/workflows/concrete_compiler_test_cpu_distributed.yml index f09ca686b..e83614297 100644 --- a/.github/workflows/concrete_compiler_test_cpu_distributed.yml +++ b/.github/workflows/concrete_compiler_test_cpu_distributed.yml @@ -2,14 +2,15 @@ name: concrete-compiler test linux-cpu-distributed on: workflow_dispatch: - pull_request: - paths: - - .github/workflows/concrete_compiler_test_cpu_distributed.yml - - compilers/concrete-compiler/** - push: - branches: - - 'main' - - 'release/*' + # Temporary disabled since need slab update + # pull_request: + # paths: + # - .github/workflows/concrete_compiler_test_cpu_distributed.yml + # - compilers/concrete-compiler/** + # push: + # branches: + # - 'main' + # - 'release/*' env: ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}