Skip to content

Instantly share code, notes, and snippets.

@cathalmccabe
Created November 29, 2021 10:38
Show Gist options
  • Save cathalmccabe/9144ed8a286512b24a2915ee14662105 to your computer and use it in GitHub Desktop.
Save cathalmccabe/9144ed8a286512b24a2915ee14662105 to your computer and use it in GitHub Desktop.
PYNQ Alveo lab exercise solutions
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Example Solutions for Exercises\n",
"\n",
"This notebook is designed to give lab helpers guidance for possible ways to complete the exercises. It is not meant to be a definitive set of \"correct\" solutions. Possible variants are noted in the comments. This notebook is also not self contained - the cells will need to be run in the context of the notebooks.\n",
"\n",
"## Lab 1 - Introduction"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exercise 1\n",
"\n",
"This is a copy-paste exercise from the vadd code in the notebook"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vmult = ol.vmult_1\n",
"vmult.call(in_a, in_b, out_c, 1024*1024)\n",
"out_c.sync_from_device()\n",
"np.array_equal(in_a * in_b, out_c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exercise 2\n",
"\n",
"Main complexity here is creating the temporary buffer. This could be done inside `vmac` for more flexibility at the expense of performance."
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"temp = pynq.allocate((1024,1024), 'u4')\n",
"def vmac(in_a, in_b, acc):\n",
" vmult.call(in_a, in_b, temp, in_a.size)\n",
" vadd.call(acc, temp, acc, in_a.size)\n",
" \n",
"out_c[:] = 1\n",
"out_c.sync_to_device()\n",
"vmac(in_a, in_b, out_c)\n",
"out_c.sync_from_device()\n",
"np.array_equal(in_a*in_b + 1, out_c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exercise 3\n",
"\n",
"Look up the `np.random.randint` function and the rest of the code is pretty self explanatory. I've used the `vmac` function to drive the hardware as that way we test both at the same time"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"in_a[:] = np.random.randint(0, 100, (1024,1024))\n",
"in_b[:] = np.random.randint(0, 100, (1024,1024))\n",
"out_c[:] = 1\n",
"\n",
"in_a.sync_to_device()\n",
"in_b.sync_to_device()\n",
"out_c.sync_to_device()\n",
"\n",
"vmac(in_a, in_b, out_c)\n",
"out_c.sync_from_device()\n",
"np.array_equal(in_a * in_b + 1, out_c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Lab 2 Optimization\n",
"\n",
"### Exercise 1\n",
"\n",
"This is mostly a straight copy and paste from the previous notebook applied to the new vmac APIs. The only challenge might be computing the expected result."
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"True\n",
"True\n"
]
}
],
"source": [
"import numpy as np\n",
"\n",
"def test(f):\n",
" in_a[:] = np.random.randint(0, 100, (8,1024,1024))\n",
" in_b[:] = np.random.randint(0, 100, (8,1024,1024))\n",
" in_a.sync_to_device()\n",
" in_b.sync_to_device()\n",
" expected = sum([a * b for a, b in zip(in_a, in_b)])\n",
"\n",
" # Make sure the acculmulator is cleared\n",
" acc[:] = 0\n",
" acc.sync_to_device()\n",
"\n",
" f(in_a, in_b, acc)\n",
" \n",
" acc.sync_from_device()\n",
" return np.array_equal(expected, acc)\n",
" \n",
"print(test(vmac_plain))\n",
"print(test(vmac_overlapped))\n",
"print(test(vmac_waitfor))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exercise 2\n",
"\n",
"This is probably the most complicated of the exercises and involves really thinking about the timeline diagram for an overlapped vmac. Unfortunately at present this can't easily be done using `waitfor` but I'm hopefully we'll add that capability in 2.6"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def vmac_communication(a, b, acc):\n",
" # Perform the first data transfer\n",
" in_a[0].sync_to_device()\n",
" in_b[0].sync_to_device()\n",
" # Perform the first multiplication\n",
" wh = vmult.start(a[0], b[0], temp[0], 1024*1024)\n",
" # Copy the second block and the accumulator\n",
" in_a[1].sync_to_device()\n",
" in_b[1].sync_to_device()\n",
" acc.sync_to_device()\n",
" wh.wait()\n",
" # Loop over all of the additions\n",
" for i in range(8):\n",
" wh = vadd.start(acc, temp[i%2], acc, 1024*1024)\n",
" if i != 7:\n",
" wh2 = vmult.start(a[i+1], b[i+1], temp[(i+1)%2], 1024*1024)\n",
" if i != 6:\n",
" in_a[i+2].sync_to_device()\n",
" in_b[i+2].sync_to_device()\n",
" wh2.wait()\n",
" wh.wait()\n",
" acc.sync_from_device()\n",
" \n",
"in_a[:] = np.random.randint(0, 100, (8,1024,1024))\n",
"in_b[:] = np.random.randint(0, 100, (8,1024,1024))\n",
"expected = sum([a * b for a, b in zip(in_a, in_b)])\n",
"\n",
"acc[:] = 0\n",
"\n",
"vmac_communication(in_a, in_b, acc)\n",
" \n",
"np.array_equal(expected, acc)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exercise 3\n",
"\n",
"Main complexity here is picking a timing library and a plotting library. I've shown how to do this with `timeit` and `pandas` as I think this is the most succint approach"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<matplotlib.axes._subplots.AxesSubplot at 0x7ff2dc41d650>"
]
},
"execution_count": 34,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"import timeit\n",
"import functools\n",
"\n",
"def time_vmac(dim, f):\n",
" in_a = pynq.allocate((8, dim, dim), 'u4')\n",
" in_b = pynq.allocate((8, dim, dim), 'u4')\n",
" acc = pynq.allocate((dim, dim), 'u4')\n",
" \n",
" in_a[:] = 100\n",
" in_b[:] = 200\n",
" \n",
" reps, duration = timeit.Timer(functools.partial(f, in_a, in_b, acc)).autorange()\n",
" \n",
" return duration / reps\n",
"\n",
"import pandas as pd\n",
"df = pd.DataFrame(columns=['plain', 'overlapped', 'waitfor'])\n",
"for i in range(11):\n",
" dim = 2 ** i\n",
" df.loc[dim] = [\n",
" time_vmac(dim, vmac_plain),\n",
" time_vmac(dim, vmac_overlapped),\n",
" time_vmac(dim, vmac_waitfor)\n",
" ]\n",
" \n",
"df.plot(loglog=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Lab 3 Compression\n",
"\n",
"These exercises aren't too complex they just involve a fair number of moving parts. The main advice I would give is to focus on whole multiples of the buffer sizes rather than trying to handle arbitrary files.\n",
"\n",
"### Exercise 1\n",
"\n",
"This should be a case of applying the lessons of the previous lab to this one. The test data is big enough for 8 * 8 blocks which mirrors lab 2 exactly"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [],
"source": [
"in_buffers = pynq.allocate((8, 8, BLOCK_SIZE), 'u1', target=ol.bank1)\n",
"out_buffers = pynq.allocate((8, 8, BLOCK_SIZE), 'u1', target=ol.bank1)\n",
"compressed_size = pynq.allocate((8, 8), 'u4', target=ol.bank1)\n",
"\n",
"in_buffers.reshape((8*8*BLOCK_SIZE))[:] = memoryview(test_data[0:8*8*1024*1024])\n",
"\n",
"def sync_output(sizes, buffers):\n",
" sizes.sync_from_device()\n",
" for s, b in zip(sizes, buffers):\n",
" b[0:s].sync_from_device()\n",
"\n",
"def compress_overlapped():\n",
" in_buffers[0].sync_to_device()\n",
" for i in range(8):\n",
" wh = compress.start(in_buffers[i], out_buffers[i],\n",
" compressed_size[i], uncompressed_size,\n",
" 1024, 8*BLOCK_SIZE)\n",
" if i != 7:\n",
" in_buffers[i+1].sync_to_device()\n",
" if i != 0:\n",
" sync_output(compressed_size[i-1], out_buffers[i-1])\n",
" wh.wait()\n",
" sync_output(compressed_size[7], out_buffers[7])\n",
" \n",
"compress_overlapped()\n",
"\n",
"for i in range(8):\n",
" for j in range(8):\n",
" uncompressed = lz4.block.decompress(out_buffers[i,j,0:compressed_size[i,j]],\n",
" uncompressed_size=1024*1024)\n",
" if len(uncompressed) != BLOCK_SIZE:\n",
" print(\"Wrong block length\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exercise 2\n",
"\n",
"This one is mainly an exercise in code organisation. Classes are basically essential to avoid variable overload but the actual interleaving should be manageable"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"class Compressor:\n",
" def __init__(self, kernel, bank):\n",
" self._kernel = kernel\n",
" self._in = pynq.allocate((8, BLOCK_SIZE), 'u1', target=bank)\n",
" self._out = pynq.allocate((8, BLOCK_SIZE), 'u1', target=bank)\n",
" self._comp_size = pynq.allocate((8,), 'u4', target=bank)\n",
" self._uncomp_size = pynq.allocate((8,), 'u4', target=bank)\n",
" \n",
" self._uncomp_size[:] = BLOCK_SIZE\n",
" self._uncomp_size.sync_to_device\n",
" \n",
" def transfer_in(self, data):\n",
" self._in.reshape((8*BLOCK_SIZE))[:] = memoryview(data)\n",
" self._in.sync_to_device()\n",
" \n",
" def transfer_out(self):\n",
" sync_output(self._comp_size, self._out)\n",
" return [b[0:s].copy() for s, b in zip(self._comp_size, self._out)]\n",
" \n",
" def start(self):\n",
" return self._kernel.start(self._in, self._out, self._comp_size, self._uncomp_size, 1024, 8*BLOCK_SIZE)\n",
" \n",
"c1 = Compressor(ol.xilLz4Compress_1, ol.bank0)\n",
"c2 = Compressor(ol.xilLz4Compress_2, ol.bank1)\n",
"\n",
"c1.transfer_in(test_data[0:8*BLOCK_SIZE])\n",
"wh1 = c1.start()\n",
"c2.transfer_in(test_data[8*BLOCK_SIZE:16*BLOCK_SIZE])\n",
"wh2 = c2.start()\n",
"wh1.wait()\n",
"result1 = c1.transfer_out()\n",
"wh2.wait()\n",
"result2 = c2.transfer_out()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exercise 3\n",
"\n",
"Practise implementing an actual spec in Python and _hoepfully_ seeing it's not too bad. Each block is prefixed with a length and the file header is already provided"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import io\n",
"import struct\n",
"\n",
"stream = io.BytesIO()\n",
"\n",
"stream.write(LZ4_HEADER)\n",
"for i in range(8):\n",
" for j in range(8):\n",
" s = compressed_size[i,j]\n",
" subbuf = out_buffers[i,j,0:s]\n",
" stream.write(struct.pack('<I', s))\n",
" stream.write(subbuf)\n",
"stream.write(struct.pack('<I', 0))\n",
"\n",
"stream.seek(0)\n",
"\n",
"import lz4.frame\n",
"uncompressed = lz4.frame.decompress(stream.read())\n",
"uncompressed == test_data[0:64*BLOCK_SIZE]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment