|
6 | 6 | "source": [ |
7 | 7 | "<img src=\"https://github.com/thesps/conifer/blob/master/conifer_v1.png?raw=true\" width=\"250\" alt=\"conifer\" />\n", |
8 | 8 | "\n", |
9 | | - "In this notebook we will take the first steps with training a BDT with `xgboost`, then translating it to HLS code for FPGA with `conifer`\n", |
| 9 | + "In this notebook we will take the first steps with training a boosted decision tree (BDT) with `xgboost`, then translating it to HLS code for FPGA inference with `conifer`.\n", |
10 | 10 | "\n", |
11 | | - "Key concepts:\n", |
12 | | - "- model training\n", |
13 | | - "- model evaluation\n", |
14 | | - "- `conifer` configuration and conversion\n", |
15 | | - "- model emulation\n", |
16 | | - "- model synthesis\n", |
17 | | - "- accelerator creation\n", |
| 11 | + "## What is a Boosted Decision Tree?\n", |
18 | 12 | "\n", |
19 | | - "For some use cases, the Forest Processing Unit might be an easier entry point as no FPGA synthesis is required for supported boards. Read more about the FPU here: https://ssummers.web.cern.ch/conifer/fpu.html" |
| 13 | + "A Boosted Decision Tree (BDT) is an ensemble learning method that builds a strong classifier by combining many shallow decision trees. Each tree is trained to correct the residual errors of the previous ones. `XGBoost` is a particularly efficient and widely used gradient boosting framework that adds regularisation and second-order gradient information to improve generalisation and training speed. BDTs are popular in high-energy physics because they train quickly, are interpretable, and are often competitive with deep neural networks on tabular data. Their tree-structured computation also maps naturally to FPGA hardware: each tree can be evaluated in parallel, making BDTs well-suited for low-latency trigger and online inference applications.\n", |
| 14 | + "\n", |
| 15 | + "## Key notebook parts\n", |
| 16 | + "\n", |
| 17 | + "- **Model training**: train a multi-class `XGBClassifier` on the jet tagging dataset and compare its accuracy to the Keras/PyTorch baseline from Part 1\n", |
| 18 | + "- **Model evaluation**: measure classification performance using ROC and accuracy\n", |
| 19 | + "- **`conifer` configuration and conversion**: configure the `xilinxhls` backend and convert the trained XGBoost model into `conifer`'s intermediate representation, which generates synthesisable HLS C++ code\n", |
| 20 | + "- **Model emulation**: compile the generated HLS C++ on the CPU and run bit-accurate predictions to verify conversion correctness and numerical precision before FPGA synthesis\n", |
| 21 | + "- **Model synthesis**: run Vitis HLS C Synthesis followed by Vivado RTL synthesis\n", |
| 22 | + "- **Accelerator creation**: configure a board-specific deployment target and build a complete bitfile for a `pynq-z2` board, ready for on-device inference\n" |
20 | 23 | ] |
21 | 24 | }, |
22 | 25 | { |
|
27 | 30 | "source": [ |
28 | 31 | "import xgboost as xgb\n", |
29 | 32 | "import matplotlib.pyplot as plt\n", |
| 33 | + "import sys\n", |
| 34 | + "sys.path.append('..')\n", |
30 | 35 | "import plotting\n", |
31 | 36 | "import numpy as np\n", |
32 | 37 | "from scipy.special import softmax\n", |
33 | 38 | "from sklearn.preprocessing import LabelEncoder, OneHotEncoder\n", |
34 | 39 | "import conifer\n", |
35 | 40 | "import json\n", |
36 | 41 | "import os\n", |
37 | | - "import sys\n", |
38 | | - "\n", |
39 | | - "os.environ['PATH'] = os.environ['XILINX_VITIS'] + '/bin:' + os.environ['PATH']\n", |
40 | 42 | "\n", |
41 | | - "# enable more output from conifer\n", |
| 43 | + "# Enable more outputs from conifer\n", |
42 | 44 | "import logging\n", |
43 | 45 | "\n", |
44 | 46 | "logging.basicConfig(stream=sys.stdout, level=logging.WARNING)\n", |
45 | 47 | "logger = logging.getLogger('conifer')\n", |
46 | 48 | "logger.setLevel('DEBUG')\n", |
47 | 49 | "\n", |
48 | | - "# create a random seed at we use to make the results repeatable\n", |
| 50 | + "# Create a random seed at we use to make the results repeatable\n", |
49 | 51 | "seed = int('hls4ml-tutorial'.encode('utf-8').hex(), 16) % 2**31\n", |
50 | 52 | "\n", |
51 | 53 | "print(f'Using conifer version {conifer.__version__}')" |
52 | 54 | ] |
53 | 55 | }, |
| 56 | + { |
| 57 | + "cell_type": "code", |
| 58 | + "execution_count": null, |
| 59 | + "metadata": {}, |
| 60 | + "outputs": [], |
| 61 | + "source": [ |
| 62 | + "MODEL_TYPE = 'keras' # set to 'pytorch' if you used the PyTorch notebook in Part 1" |
| 63 | + ] |
| 64 | + }, |
54 | 65 | { |
55 | 66 | "cell_type": "markdown", |
56 | 67 | "metadata": {}, |
|
59 | 70 | "\n", |
60 | 71 | "Load the jet tagging dataset.\n", |
61 | 72 | "\n", |
62 | | - "**Note**: you need to run part1 first." |
| 73 | + "**Note**: you need to run part 1 first to generate the dataset files." |
63 | 74 | ] |
64 | 75 | }, |
65 | 76 | { |
|
68 | 79 | "metadata": {}, |
69 | 80 | "outputs": [], |
70 | 81 | "source": [ |
71 | | - "X_train_val = np.load('X_train_val.npy')\n", |
72 | | - "X_test = np.load('X_test.npy')\n", |
73 | | - "y_train_val_one_hot = np.load('y_train_val.npy')\n", |
74 | | - "y_test_one_hot = np.load('y_test.npy')\n", |
75 | | - "classes = np.load('classes.npy', allow_pickle=True)" |
| 82 | + "X_train_val = np.load('../data/X_train_val.npy')\n", |
| 83 | + "X_test = np.load('../data/X_test.npy')\n", |
| 84 | + "y_train_val_one_hot = np.load('../data/y_train_val.npy')\n", |
| 85 | + "y_test_one_hot = np.load('../data/y_test.npy')\n", |
| 86 | + "classes = np.load('../data/classes.npy', allow_pickle=True)" |
76 | 87 | ] |
77 | 88 | }, |
78 | 89 | { |
|
131 | 142 | "outputs": [], |
132 | 143 | "source": [ |
133 | 144 | "from sklearn.metrics import accuracy_score\n", |
134 | | - "from tensorflow.keras.models import load_model\n", |
135 | | - "\n", |
136 | | - "# load the KERAS model from part 1\n", |
137 | | - "model_ref = load_model('model_1/KERAS_check_best_model.h5')\n", |
138 | | - "y_ref = model_ref.predict(X_test)\n", |
139 | 145 | "\n", |
140 | | - "# compute predictions of the xgboost model\n", |
| 146 | + "if MODEL_TYPE == 'keras':\n", |
| 147 | + " from tensorflow.keras.models import load_model\n", |
| 148 | + " model_ref = load_model('../models/keras_model_part1.h5')\n", |
| 149 | + " y_ref = model_ref.predict(X_test)\n", |
| 150 | + "\n", |
| 151 | + "elif MODEL_TYPE == 'pytorch':\n", |
| 152 | + " import torch\n", |
| 153 | + " import torch.nn as nn\n", |
| 154 | + "\n", |
| 155 | + " class JetTagger(nn.Module):\n", |
| 156 | + " def __init__(self):\n", |
| 157 | + " super().__init__()\n", |
| 158 | + " self.fc1 = nn.Linear(16, 64)\n", |
| 159 | + " self.fc2 = nn.Linear(64, 32)\n", |
| 160 | + " self.fc3 = nn.Linear(32, 32)\n", |
| 161 | + " self.output = nn.Linear(32, 5)\n", |
| 162 | + "\n", |
| 163 | + " def forward(self, x):\n", |
| 164 | + " x = torch.relu(self.fc1(x))\n", |
| 165 | + " x = torch.relu(self.fc2(x))\n", |
| 166 | + " x = torch.relu(self.fc3(x))\n", |
| 167 | + " return torch.softmax(self.output(x), dim=1)\n", |
| 168 | + "\n", |
| 169 | + " model_ref = JetTagger()\n", |
| 170 | + " model_ref.load_state_dict(torch.load('../models/pytorch_weights_part1.pt'))\n", |
| 171 | + " model_ref.eval()\n", |
| 172 | + " with torch.no_grad():\n", |
| 173 | + " y_ref = model_ref(torch.FloatTensor(X_test)).numpy()\n", |
| 174 | + "\n", |
| 175 | + "# Compute predictions of the xgboost model\n", |
141 | 176 | "y_xgb = clf.predict_proba(X_test)\n", |
142 | | - "print(f'Accuracy baseline: {accuracy_score(np.argmax(y_test_one_hot, axis=1), np.argmax(y_ref, axis=1)):.5f}')\n", |
| 177 | + "print(f'Accuracy {MODEL_TYPE}: {accuracy_score(np.argmax(y_test_one_hot, axis=1), np.argmax(y_ref, axis=1)):.5f}')\n", |
143 | 178 | "print(f'Accuracy xgboost: {accuracy_score(np.argmax(y_test_one_hot, axis=1), np.argmax(y_xgb, axis=1)):.5f}')\n", |
144 | 179 | "\n", |
145 | 180 | "fig, ax = plt.subplots(figsize=(9, 9))\n", |
146 | 181 | "_ = plotting.makeRoc(y_test_one_hot, y_ref, classes, linestyle='--')\n", |
147 | | - "plt.gca().set_prop_cycle(None) # reset the colors\n", |
| 182 | + "plt.gca().set_prop_cycle(None)\n", |
148 | 183 | "_ = plotting.makeRoc(y_test_one_hot, y_xgb, classes, linestyle='-')\n", |
149 | 184 | "\n", |
150 | | - "# add a legend\n", |
151 | 185 | "from matplotlib.lines import Line2D\n", |
152 | | - "\n", |
153 | | - "lines = [\n", |
154 | | - " Line2D([0], [0], ls='--'),\n", |
155 | | - " Line2D([0], [0], ls='-'),\n", |
156 | | - "]\n", |
157 | 186 | "from matplotlib.legend import Legend\n", |
158 | | - "\n", |
159 | | - "leg = Legend(ax, lines, labels=['part1 Keras', 'xgboost'], loc='lower right', frameon=False)\n", |
| 187 | + "leg = Legend(ax, [Line2D([0], [0], ls='--'), Line2D([0], [0], ls='-')],\n", |
| 188 | + " labels=[f'part1 {MODEL_TYPE}', 'xgboost'], loc='lower right', frameon=False)\n", |
160 | 189 | "ax.add_artist(leg)" |
161 | 190 | ] |
162 | 191 | }, |
|
170 | 199 | "\n", |
171 | 200 | "We will print the configuration, modify it, and print it again. The modifications are:\n", |
172 | 201 | "- set the `OutputDirectory` to something descriptive\n", |
173 | | - "- set the `XilinxPart` to the part number of the FPGA on the Alveo U50" |
| 202 | + "- set the `XilinxPart` to the part number of the FPGA on the Alveo U250" |
174 | 203 | ] |
175 | 204 | }, |
176 | 205 | { |
|
181 | 210 | "source": [ |
182 | 211 | "cfg = conifer.backends.xilinxhls.auto_config()\n", |
183 | 212 | "\n", |
184 | | - "# print the config\n", |
| 213 | + "# Print the config\n", |
185 | 214 | "print('Default Configuration\\n' + '-' * 50)\n", |
186 | 215 | "plotting.print_dict(cfg)\n", |
187 | 216 | "print('-' * 50)\n", |
188 | 217 | "\n", |
189 | | - "# modify the config\n", |
190 | | - "cfg['OutputDir'] = 'model_5/'\n", |
| 218 | + "# Set output directory and target device\n", |
| 219 | + "cfg['OutputDir'] = '../hls4ml_prjs/conifer_prj_bdt_part6a'\n", |
191 | 220 | "cfg['XilinxPart'] = 'xcu250-figd2104-2L-e'\n", |
192 | 221 | "\n", |
193 | | - "# print the config again\n", |
| 222 | + "# Print the config again (to verify change)\n", |
194 | 223 | "print('Modified Configuration\\n' + '-' * 50)\n", |
195 | 224 | "plotting.print_dict(cfg)\n", |
196 | 225 | "print('-' * 50)" |
|
220 | 249 | "metadata": {}, |
221 | 250 | "outputs": [], |
222 | 251 | "source": [ |
223 | | - "# convert the model to the conifer representation\n", |
| 252 | + "# Convert the model to the conifer representation\n", |
224 | 253 | "conifer_model = conifer.converters.convert_from_xgboost(clf, cfg)\n", |
225 | | - "# print the help to see the API on the conifer_model\n", |
| 254 | + "\n", |
| 255 | + "# Print the help to see the API of the conifer_model\n", |
226 | 256 | "help(conifer_model)\n", |
227 | | - "# write the project (writing HLS project to disk)\n", |
| 257 | + "\n", |
| 258 | + "# Write the project (writing HLS project to disk)\n", |
228 | 259 | "conifer_model.write()\n", |
229 | | - "# save the conifer model - we can load this again later\n", |
230 | | - "clf.save_model('model_5/xgboost_model.json')" |
| 260 | + "\n", |
| 261 | + "# Save the xgboost model alongside the conifer project\n", |
| 262 | + "clf.save_model('../hls4ml_prjs/conifer_prj_bdt_part6a/xgboost_model.json')" |
231 | 263 | ] |
232 | 264 | }, |
233 | 265 | { |
|
237 | 269 | "## Explore\n", |
238 | 270 | "Browse the files in the newly created project directory to take a look at the HLS code.\n", |
239 | 271 | "\n", |
240 | | - "The output of `!tree model_5` is:\n", |
| 272 | + "The output of `!tree ../hls4ml_prjs/conifer_prj_bdt_part6a` is:\n", |
241 | 273 | "\n", |
242 | 274 | "```\n", |
243 | | - "model_5/\n", |
| 275 | + "conifer_prj_bdt_part6a/\n", |
244 | 276 | "├── bridge.cpp\n", |
245 | 277 | "├── build_hls.tcl\n", |
246 | 278 | "├── firmware\n", |
|
306 | 338 | "source": [ |
307 | 339 | "y_hls_proba = softmax(y_hls) # compute class probabilities from the raw predictions\n", |
308 | 340 | "\n", |
309 | | - "print(f'Accuracy baseline: {accuracy_score(np.argmax(y_test_one_hot, axis=1), np.argmax(y_ref, axis=1)):.5f}')\n", |
| 341 | + "print(f'Accuracy {MODEL_TYPE}: {accuracy_score(np.argmax(y_test_one_hot, axis=1), np.argmax(y_ref, axis=1)):.5f}')\n", |
310 | 342 | "print(f'Accuracy xgboost: {accuracy_score(np.argmax(y_test_one_hot, axis=1), np.argmax(y_xgb, axis=1)):.5f}')\n", |
311 | 343 | "print(f'Accuracy conifer: {accuracy_score(np.argmax(y_test_one_hot, axis=1), np.argmax(y_hls_proba, axis=1)):.5f}')\n", |
312 | 344 | "\n", |
313 | | - "\n", |
314 | 345 | "fig, ax = plt.subplots(figsize=(9, 9))\n", |
315 | 346 | "_ = plotting.makeRoc(y_test_one_hot, y_ref, classes, linestyle='--')\n", |
316 | | - "plt.gca().set_prop_cycle(None) # reset the colors\n", |
| 347 | + "plt.gca().set_prop_cycle(None)\n", |
317 | 348 | "_ = plotting.makeRoc(y_test_one_hot, y_xgb, classes, linestyle=':')\n", |
318 | | - "plt.gca().set_prop_cycle(None) # reset the colors\n", |
| 349 | + "plt.gca().set_prop_cycle(None)\n", |
319 | 350 | "_ = plotting.makeRoc(y_test_one_hot, y_hls_proba, classes, linestyle='-')\n", |
320 | 351 | "\n", |
321 | | - "# add a legend\n", |
322 | 352 | "from matplotlib.lines import Line2D\n", |
323 | | - "\n", |
324 | | - "lines = [\n", |
325 | | - " Line2D([0], [0], ls='--'),\n", |
326 | | - " Line2D([0], [0], ls=':'),\n", |
327 | | - " Line2D([0], [0], ls='-'),\n", |
328 | | - "]\n", |
329 | 353 | "from matplotlib.legend import Legend\n", |
330 | | - "\n", |
331 | | - "leg = Legend(ax, lines, labels=['part1 Keras', 'xgboost', 'conifer'], loc='lower right', frameon=False)\n", |
| 354 | + "leg = Legend(ax,\n", |
| 355 | + " [Line2D([0], [0], ls='--'), Line2D([0], [0], ls=':'), Line2D([0], [0], ls='-')],\n", |
| 356 | + " labels=[f'part1 {MODEL_TYPE}', 'xgboost', 'conifer'], loc='lower right', frameon=False)\n", |
332 | 357 | "ax.add_artist(leg)" |
333 | 358 | ] |
334 | 359 | }, |
|
337 | 362 | "metadata": {}, |
338 | 363 | "source": [ |
339 | 364 | "## Build\n", |
340 | | - "Now we'll run the Vitis HLS and Vivado synthesis. HLS C Synthesis compiles our C++ to RTL, performing scheduling and resource mapping. Vivado synthesis synthesizes the RTL from the previous step into a netlist, and produces a more realistic resource estimation. The latency can't change during Vivado synthesis, it's fixed in the RTL description.\n", |
| 365 | + "Now we'll run the Vitis HLS and Vivado synthesis. HLS C Synthesis compiles our C++ to RTL, performing scheduling and resource mapping. Vivado synthesis synthesizes the RTL from the previous step into a netlist, and produces a more realistic resource estimation. \n", |
341 | 366 | "\n", |
342 | 367 | "After the build completes we can also browse the new log files and reports that are generated.\n", |
343 | 368 | "\n", |
344 | | - "**Warning**: this step might take around 10 minutes" |
| 369 | + "**This step takes around 10 minutes.**" |
345 | 370 | ] |
346 | 371 | }, |
347 | 372 | { |
|
397 | 422 | "outputs": [], |
398 | 423 | "source": [ |
399 | 424 | "pynq_model_cfg = conifer.backends.xilinxhls.auto_config()\n", |
400 | | - "pynq_model_cfg['OutputDir'] = 'model_5_pynq' # choose a new project directory\n", |
| 425 | + "pynq_model_cfg['OutputDir'] = '../hls4ml_prjs/conifer_prj_bdt_part6a_pynq'\n", |
401 | 426 | "pynq_model_cfg['ProjectName'] = 'conifer_jettag'\n", |
402 | 427 | "pynq_model_cfg['AcceleratorConfig'] = {\n", |
403 | 428 | " 'Board': 'pynq-z2', # choose a pynq-z2 board\n", |
|
444 | 469 | "source": [ |
445 | 470 | "### Load the model\n", |
446 | 471 | "\n", |
447 | | - "We load the JSON for the conifer model we previously used, applying the new configuration just defined. We'll see that the FPGA part specified by the board overrides the `XilinxPart` specified in the default." |
| 472 | + "We load the JSON for the conifer model we previously saved, applying the new configuration just defined. We'll see that the FPGA part specified by the board overrides the `XilinxPart` specified in the default." |
448 | 473 | ] |
449 | 474 | }, |
450 | 475 | { |
|
453 | 478 | "metadata": {}, |
454 | 479 | "outputs": [], |
455 | 480 | "source": [ |
456 | | - "pynq_model = conifer.model.load_model('model_5/my_prj.json', new_config=pynq_model_cfg)\n", |
| 481 | + "pynq_model = conifer.model.load_model('../hls4ml_prjs/conifer_prj_bdt_part6a/my_prj.json', new_config=pynq_model_cfg)\n", |
457 | 482 | "pynq_model.write()" |
458 | 483 | ] |
459 | 484 | }, |
|
465 | 490 | "\n", |
466 | 491 | "Now we run `build` again, running HLS Synthesis, Logic Synthesis and Place & Route, finally producing a bitfile and an archive of files that we'll need to run inference on the pynq-z2 board. \n", |
467 | 492 | "\n", |
468 | | - "**Warning**: this step might take around 20 minutes to complete.\n", |
| 493 | + "**This step takes around 20 minutes.**\n", |
469 | 494 | "\n", |
470 | 495 | "The floorplan of the bitfile should like something like this, where the individual tree modules are highlighted in different colours:\n", |
471 | 496 | "\n", |
472 | | - "<img src=\"./images/part5_floorplan.png\" width=\"300\" />" |
| 497 | + "<img src=\"../images/part5_floorplan.png\" width=\"300\" />" |
473 | 498 | ] |
474 | 499 | }, |
475 | 500 | { |
|
488 | 513 | "## Inference on pynq-z2\n", |
489 | 514 | "\n", |
490 | 515 | "Running inference on the `pynq-z2` would look like this:\n", |
491 | | - "- download the `model_5/conifer_jettag.zip` archive from this notebook\n", |
492 | | - "- upload `conifer_jettag.zip` to the pynq-z2 device and unzip it\n", |
493 | | - "- start a jupyter notebook on the `pynq-z2` and run the following code:\n", |
| 516 | + "- Download the `conifer_bdt_pynq/conifer_jettag.zip` archive from this notebook\n", |
| 517 | + "- Upload `conifer_jettag.zip` to the pynq-z2 device and unzip it\n", |
| 518 | + "- Start a jupyter notebook on the `pynq-z2` and run the following code:\n", |
494 | 519 | "\n", |
495 | 520 | "```\n", |
496 | 521 | "import conifer\n", |
|
503 | 528 | ], |
504 | 529 | "metadata": { |
505 | 530 | "kernelspec": { |
506 | | - "display_name": "Python 3 (ipykernel)", |
| 531 | + "display_name": "hls4ml-tutorial", |
507 | 532 | "language": "python", |
508 | 533 | "name": "python3" |
509 | 534 | }, |
|
517 | 542 | "name": "python", |
518 | 543 | "nbconvert_exporter": "python", |
519 | 544 | "pygments_lexer": "ipython3", |
520 | | - "version": "3.10.10" |
| 545 | + "version": "3.10.16" |
521 | 546 | } |
522 | 547 | }, |
523 | 548 | "nbformat": 4, |
|
0 commit comments